summaryrefslogtreecommitdiff
path: root/spec
diff options
context:
space:
mode:
authorPhil Hughes <me@iamphill.com>2016-08-30 10:43:09 +0100
committerPhil Hughes <me@iamphill.com>2016-08-30 10:43:09 +0100
commit85f6244ce852fb6b788ea660c7d0cbe14ec10a20 (patch)
treeec29f6c01ea8e4ac774f3dae874a3a3abf97267b /spec
parent2bee8e7db927d2bc2c5912b98dfe52d3c3c40fbd (diff)
parent2778dec131c2afac9fcdb2c42365b69099a5ae5b (diff)
downloadgitlab-ce-85f6244ce852fb6b788ea660c7d0cbe14ec10a20.tar.gz
Merge branch 'master' into build-cancel-spinner
Diffstat (limited to 'spec')
-rw-r--r--spec/config/mail_room_spec.rb43
-rw-r--r--spec/controllers/admin/groups_controller_spec.rb25
-rw-r--r--spec/controllers/admin/impersonations_controller_spec.rb2
-rw-r--r--spec/controllers/admin/spam_logs_controller_spec.rb12
-rw-r--r--spec/controllers/admin/users_controller_spec.rb2
-rw-r--r--spec/controllers/application_controller_spec.rb6
-rw-r--r--spec/controllers/autocomplete_controller_spec.rb331
-rw-r--r--spec/controllers/commit_controller_spec.rb246
-rw-r--r--spec/controllers/groups/avatars_controller_spec.rb2
-rw-r--r--spec/controllers/groups/milestones_controller_spec.rb2
-rw-r--r--spec/controllers/groups_controller_spec.rb30
-rw-r--r--spec/controllers/help_controller_spec.rb22
-rw-r--r--spec/controllers/import/gitorious_controller_spec.rb69
-rw-r--r--spec/controllers/profiles/avatars_controller_spec.rb2
-rw-r--r--spec/controllers/profiles/keys_controller_spec.rb18
-rw-r--r--spec/controllers/projects/avatars_controller_spec.rb2
-rw-r--r--spec/controllers/projects/boards/issues_controller_spec.rb120
-rw-r--r--spec/controllers/projects/boards/lists_controller_spec.rb241
-rw-r--r--spec/controllers/projects/boards_controller_spec.rb43
-rw-r--r--spec/controllers/projects/commit_controller_spec.rb293
-rw-r--r--spec/controllers/projects/commits_controller_spec.rb2
-rw-r--r--spec/controllers/projects/compare_controller_spec.rb86
-rw-r--r--spec/controllers/projects/discussions_controller_spec.rb125
-rw-r--r--spec/controllers/projects/environments_controller_spec.rb58
-rw-r--r--spec/controllers/projects/forks_controller_spec.rb10
-rw-r--r--spec/controllers/projects/issues_controller_spec.rb161
-rw-r--r--spec/controllers/projects/merge_requests_controller_spec.rb392
-rw-r--r--spec/controllers/projects/milestones_controller_spec.rb2
-rw-r--r--spec/controllers/projects/notes_controller_spec.rb133
-rw-r--r--spec/controllers/projects/protected_branches_controller_spec.rb2
-rw-r--r--spec/controllers/projects/raw_controller_spec.rb2
-rw-r--r--spec/controllers/projects/services_controller_spec.rb4
-rw-r--r--spec/controllers/projects/tags_controller_spec.rb20
-rw-r--r--spec/controllers/projects/templates_controller_spec.rb48
-rw-r--r--spec/controllers/projects/todo_controller_spec.rb78
-rw-r--r--spec/controllers/projects/uploads_controller_spec.rb12
-rw-r--r--spec/controllers/projects_controller_spec.rb30
-rw-r--r--spec/factories/boards.rb5
-rw-r--r--spec/factories/broadcast_messages.rb4
-rw-r--r--spec/factories/ci/builds.rb33
-rw-r--r--spec/factories/ci/pipelines.rb20
-rw-r--r--spec/factories/ci/trigger_requests.rb3
-rw-r--r--spec/factories/commit_statuses.rb24
-rw-r--r--spec/factories/environments.rb1
-rw-r--r--spec/factories/issues.rb10
-rw-r--r--spec/factories/lists.rb20
-rw-r--r--spec/factories/project_hooks.rb11
-rw-r--r--spec/factories/projects.rb6
-rw-r--r--spec/factories/protected_branches.rb23
-rw-r--r--spec/factories/todos.rb4
-rw-r--r--spec/factories/user_agent_details.rb7
-rw-r--r--spec/factories_spec.rb2
-rw-r--r--spec/features/admin/admin_abuse_reports_spec.rb4
-rw-r--r--spec/features/admin/admin_builds_spec.rb36
-rw-r--r--spec/features/admin/admin_disables_git_access_protocol_spec.rb1
-rw-r--r--spec/features/admin/admin_hooks_spec.rb6
-rw-r--r--spec/features/admin/admin_projects_spec.rb6
-rw-r--r--spec/features/admin/admin_system_info_spec.rb47
-rw-r--r--spec/features/admin/admin_users_spec.rb28
-rw-r--r--spec/features/atom/dashboard_spec.rb6
-rw-r--r--spec/features/atom/issues_spec.rb4
-rw-r--r--spec/features/atom/users_spec.rb12
-rw-r--r--spec/features/boards/boards_spec.rb634
-rw-r--r--spec/features/builds_spec.rb76
-rw-r--r--spec/features/ci_lint_spec.rb2
-rw-r--r--spec/features/commits_spec.rb2
-rw-r--r--spec/features/compare_spec.rb42
-rw-r--r--spec/features/dashboard/label_filter_spec.rb2
-rw-r--r--spec/features/dashboard_issues_spec.rb6
-rw-r--r--spec/features/environments_spec.rb50
-rw-r--r--spec/features/expand_collapse_diffs_spec.rb248
-rw-r--r--spec/features/gitlab_flavored_markdown_spec.rb24
-rw-r--r--spec/features/groups/members/user_requests_access_spec.rb7
-rw-r--r--spec/features/groups_spec.rb20
-rw-r--r--spec/features/help_pages_spec.rb4
-rw-r--r--spec/features/issuables/default_sort_order_spec.rb195
-rw-r--r--spec/features/issues/award_emoji_spec.rb10
-rw-r--r--spec/features/issues/award_spec.rb8
-rw-r--r--spec/features/issues/bulk_assignment_labels_spec.rb8
-rw-r--r--spec/features/issues/filter_by_labels_spec.rb48
-rw-r--r--spec/features/issues/filter_by_milestone_spec.rb6
-rw-r--r--spec/features/issues/filter_issues_spec.rb40
-rw-r--r--spec/features/issues/issue_sidebar_spec.rb8
-rw-r--r--spec/features/issues/new_branch_button_spec.rb4
-rw-r--r--spec/features/issues/todo_spec.rb4
-rw-r--r--spec/features/issues/update_issues_spec.rb12
-rw-r--r--spec/features/issues/user_uses_slash_commands_spec.rb58
-rw-r--r--spec/features/issues_spec.rb77
-rw-r--r--spec/features/login_spec.rb43
-rw-r--r--spec/features/markdown_spec.rb12
-rw-r--r--spec/features/merge_requests/award_spec.rb8
-rw-r--r--spec/features/merge_requests/conflicts_spec.rb73
-rw-r--r--spec/features/merge_requests/create_new_mr_spec.rb21
-rw-r--r--spec/features/merge_requests/created_from_fork_spec.rb12
-rw-r--r--spec/features/merge_requests/diff_notes_resolve_spec.rb497
-rw-r--r--spec/features/merge_requests/diff_notes_spec.rb207
-rw-r--r--spec/features/merge_requests/diffs_spec.rb25
-rw-r--r--spec/features/merge_requests/edit_mr_spec.rb13
-rw-r--r--spec/features/merge_requests/filter_by_milestone_spec.rb6
-rw-r--r--spec/features/merge_requests/merge_request_versions_spec.rb37
-rw-r--r--spec/features/merge_requests/merge_when_build_succeeds_spec.rb2
-rw-r--r--spec/features/merge_requests/pipelines_spec.rb48
-rw-r--r--spec/features/merge_requests/user_lists_merge_requests_spec.rb33
-rw-r--r--spec/features/merge_requests/user_uses_slash_commands_spec.rb32
-rw-r--r--spec/features/milestone_spec.rb4
-rw-r--r--spec/features/notes_on_merge_requests_spec.rb37
-rw-r--r--spec/features/participants_autocomplete_spec.rb6
-rw-r--r--spec/features/pipelines_settings_spec.rb35
-rw-r--r--spec/features/profile_spec.rb4
-rw-r--r--spec/features/profiles/password_spec.rb45
-rw-r--r--spec/features/profiles/preferences_spec.rb4
-rw-r--r--spec/features/projects/badges/coverage_spec.rb82
-rw-r--r--spec/features/projects/badges/list_spec.rb46
-rw-r--r--spec/features/projects/branches/delete_spec.rb24
-rw-r--r--spec/features/projects/branches_spec.rb32
-rw-r--r--spec/features/projects/commits/cherry_pick_spec.rb31
-rw-r--r--spec/features/projects/files/editing_a_file_spec.rb34
-rw-r--r--spec/features/projects/files/files_sort_submodules_with_folders_spec.rb29
-rw-r--r--spec/features/projects/files/project_owner_creates_license_file_spec.rb5
-rw-r--r--spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb3
-rw-r--r--spec/features/projects/group_links_spec.rb32
-rw-r--r--spec/features/projects/import_export/import_file_spec.rb96
-rw-r--r--spec/features/projects/issuable_templates_spec.rb89
-rw-r--r--spec/features/projects/issues/list_spec.rb20
-rw-r--r--spec/features/projects/labels/update_prioritization_spec.rb6
-rw-r--r--spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb45
-rw-r--r--spec/features/projects/members/user_requests_access_spec.rb7
-rw-r--r--spec/features/projects/merge_requests/list_spec.rb20
-rw-r--r--spec/features/projects/pipelines_spec.rb (renamed from spec/features/pipelines_spec.rb)105
-rw-r--r--spec/features/projects/project_settings_spec.rb41
-rw-r--r--spec/features/projects/ref_switcher_spec.rb29
-rw-r--r--spec/features/projects/slack_service/slack_service_spec.rb26
-rw-r--r--spec/features/projects/wiki/markdown_preview_spec.rb140
-rw-r--r--spec/features/projects/wiki/user_creates_wiki_page_spec.rb48
-rw-r--r--spec/features/projects_spec.rb37
-rw-r--r--spec/features/protected_branches/access_control_ce_spec.rb71
-rw-r--r--spec/features/protected_branches_spec.rb14
-rw-r--r--spec/features/search_spec.rb48
-rw-r--r--spec/features/security/dashboard_access_spec.rb14
-rw-r--r--spec/features/security/project/internal_access_spec.rb19
-rw-r--r--spec/features/security/project/private_access_spec.rb19
-rw-r--r--spec/features/security/project/public_access_spec.rb19
-rw-r--r--spec/features/todos/todos_sorting_spec.rb67
-rw-r--r--spec/features/todos/todos_spec.rb23
-rw-r--r--spec/features/u2f_spec.rb119
-rw-r--r--spec/features/variables_spec.rb9
-rw-r--r--spec/finders/branches_finder_spec.rb80
-rw-r--r--spec/finders/merge_requests_finder_spec.rb4
-rw-r--r--spec/finders/move_to_project_finder_spec.rb97
-rw-r--r--spec/finders/notes_finder_spec.rb6
-rw-r--r--spec/finders/projects_finder_spec.rb73
-rw-r--r--spec/finders/todos_finder_spec.rb70
-rw-r--r--spec/fixtures/api/schemas/issue.json48
-rw-r--r--spec/fixtures/api/schemas/issues.json4
-rw-r--r--spec/fixtures/api/schemas/list.json39
-rw-r--r--spec/fixtures/api/schemas/lists.json4
-rw-r--r--spec/fixtures/config/redis_new_format_host.yml29
-rw-r--r--spec/fixtures/config/redis_new_format_socket.yml6
-rw-r--r--spec/fixtures/config/redis_old_format_host.yml5
-rw-r--r--spec/fixtures/config/redis_old_format_socket.yml3
-rw-r--r--spec/fixtures/domain_blacklist.txt3
-rw-r--r--spec/fixtures/emails/commands_in_reply.eml43
-rw-r--r--spec/fixtures/emails/commands_only_reply.eml41
-rw-r--r--spec/fixtures/emails/valid_new_issue.eml23
-rw-r--r--spec/fixtures/emails/valid_new_issue_empty.eml18
-rw-r--r--spec/fixtures/emails/wrong_authentication_token.eml18
-rw-r--r--spec/fixtures/emails/wrong_mail_key.eml (renamed from spec/fixtures/emails/wrong_reply_key.eml)0
-rw-r--r--spec/fixtures/markdown.md.erb4
-rw-r--r--spec/fixtures/parallel_diff_result.yml820
-rw-r--r--spec/fixtures/project_services/campfire/rooms.json22
-rw-r--r--spec/fixtures/project_services/campfire/rooms2.json22
-rw-r--r--spec/fixtures/video_sample.mp4bin0 -> 59122 bytes
-rw-r--r--spec/helpers/application_helper_spec.rb37
-rw-r--r--spec/helpers/blob_helper_spec.rb82
-rw-r--r--spec/helpers/ci_status_helper_spec.rb10
-rw-r--r--spec/helpers/diff_helper_spec.rb110
-rw-r--r--spec/helpers/emails_helper_spec.rb12
-rw-r--r--spec/helpers/events_helper_spec.rb16
-rw-r--r--spec/helpers/gitlab_markdown_helper_spec.rb20
-rw-r--r--spec/helpers/graph_helper_spec.rb2
-rw-r--r--spec/helpers/groups_helper_spec.rb4
-rw-r--r--spec/helpers/issuables_helper_spec.rb16
-rw-r--r--spec/helpers/issues_helper_spec.rb117
-rw-r--r--spec/helpers/members_helper_spec.rb48
-rw-r--r--spec/helpers/notes_helper_spec.rb60
-rw-r--r--spec/helpers/page_layout_helper_spec.rb9
-rw-r--r--spec/helpers/projects_helper_spec.rb38
-rw-r--r--spec/helpers/search_helper_spec.rb2
-rw-r--r--spec/helpers/submodule_helper_spec.rb28
-rw-r--r--spec/helpers/time_helper_spec.rb26
-rw-r--r--spec/helpers/tree_helper_spec.rb4
-rw-r--r--spec/initializers/6_validations_spec.rb37
-rw-r--r--spec/initializers/secret_token_spec.rb200
-rw-r--r--spec/initializers/trusted_proxies_spec.rb12
-rw-r--r--spec/javascripts/abuse_reports_spec.js.es641
-rw-r--r--spec/javascripts/application_spec.js32
-rw-r--r--spec/javascripts/application_spec.js.coffee30
-rw-r--r--spec/javascripts/awards_handler_spec.js261
-rw-r--r--spec/javascripts/awards_handler_spec.js.coffee200
-rw-r--r--spec/javascripts/behaviors/autosize_spec.js21
-rw-r--r--spec/javascripts/behaviors/autosize_spec.js.coffee11
-rw-r--r--spec/javascripts/behaviors/quick_submit_spec.js93
-rw-r--r--spec/javascripts/behaviors/quick_submit_spec.js.coffee70
-rw-r--r--spec/javascripts/behaviors/requires_input_spec.js44
-rw-r--r--spec/javascripts/behaviors/requires_input_spec.js.coffee49
-rw-r--r--spec/javascripts/boards/boards_store_spec.js.es6164
-rw-r--r--spec/javascripts/boards/issue_spec.js.es683
-rw-r--r--spec/javascripts/boards/list_spec.js.es689
-rw-r--r--spec/javascripts/boards/mock_data.js.es653
-rw-r--r--spec/javascripts/datetime_utility_spec.js.coffee50
-rw-r--r--spec/javascripts/diff_comments_store_spec.js.es6122
-rw-r--r--spec/javascripts/extensions/array_spec.js22
-rw-r--r--spec/javascripts/extensions/array_spec.js.coffee12
-rw-r--r--spec/javascripts/extensions/jquery_spec.js42
-rw-r--r--spec/javascripts/extensions/jquery_spec.js.coffee34
-rw-r--r--spec/javascripts/fixtures/abuse_reports.html.haml16
-rw-r--r--spec/javascripts/fixtures/awards_handler.html.haml2
-rw-r--r--spec/javascripts/fixtures/emoji_menu.coffee957
-rw-r--r--spec/javascripts/fixtures/emoji_menu.js4
-rw-r--r--spec/javascripts/fixtures/gl_dropdown.html.haml16
-rw-r--r--spec/javascripts/fixtures/issue_sidebar_label.html.haml16
-rw-r--r--spec/javascripts/fixtures/issues_show.html.haml2
-rw-r--r--spec/javascripts/fixtures/projects.json2
-rw-r--r--spec/javascripts/gl_dropdown_spec.js.es6119
-rw-r--r--spec/javascripts/issue_spec.js121
-rw-r--r--spec/javascripts/issue_spec.js.coffee109
-rw-r--r--spec/javascripts/labels_issue_sidebar_spec.js.es689
-rw-r--r--spec/javascripts/line_highlighter_spec.js229
-rw-r--r--spec/javascripts/line_highlighter_spec.js.coffee158
-rw-r--r--spec/javascripts/merge_request_spec.js28
-rw-r--r--spec/javascripts/merge_request_spec.js.coffee23
-rw-r--r--spec/javascripts/merge_request_tabs_spec.js106
-rw-r--r--spec/javascripts/merge_request_tabs_spec.js.coffee88
-rw-r--r--spec/javascripts/merge_request_widget_spec.js74
-rw-r--r--spec/javascripts/merge_request_widget_spec.js.coffee55
-rw-r--r--spec/javascripts/new_branch_spec.js170
-rw-r--r--spec/javascripts/new_branch_spec.js.coffee160
-rw-r--r--spec/javascripts/notes_spec.js41
-rw-r--r--spec/javascripts/notes_spec.js.coffee26
-rw-r--r--spec/javascripts/project_title_spec.js60
-rw-r--r--spec/javascripts/project_title_spec.js.coffee37
-rw-r--r--spec/javascripts/right_sidebar_spec.js70
-rw-r--r--spec/javascripts/right_sidebar_spec.js.coffee69
-rw-r--r--spec/javascripts/search_autocomplete_spec.js159
-rw-r--r--spec/javascripts/search_autocomplete_spec.js.coffee149
-rw-r--r--spec/javascripts/shortcuts_issuable_spec.js74
-rw-r--r--spec/javascripts/shortcuts_issuable_spec.js.coffee82
-rw-r--r--spec/javascripts/spec_helper.coffee47
-rw-r--r--spec/javascripts/spec_helper.js22
-rw-r--r--spec/javascripts/syntax_highlight_spec.js44
-rw-r--r--spec/javascripts/syntax_highlight_spec.js.coffee42
-rw-r--r--spec/javascripts/u2f/authenticate_spec.coffee52
-rw-r--r--spec/javascripts/u2f/authenticate_spec.js75
-rw-r--r--spec/javascripts/u2f/mock_u2f_device.js33
-rw-r--r--spec/javascripts/u2f/mock_u2f_device.js.coffee15
-rw-r--r--spec/javascripts/u2f/register_spec.js81
-rw-r--r--spec/javascripts/u2f/register_spec.js.coffee57
-rw-r--r--spec/javascripts/zen_mode_spec.js73
-rw-r--r--spec/javascripts/zen_mode_spec.js.coffee51
-rw-r--r--spec/lib/banzai/filter/autolink_filter_spec.rb20
-rw-r--r--spec/lib/banzai/filter/label_reference_filter_spec.rb110
-rw-r--r--spec/lib/banzai/filter/relative_link_filter_spec.rb64
-rw-r--r--spec/lib/banzai/filter/syntax_highlight_filter_spec.rb34
-rw-r--r--spec/lib/banzai/filter/table_of_contents_filter_spec.rb2
-rw-r--r--spec/lib/banzai/filter/upload_link_filter_spec.rb2
-rw-r--r--spec/lib/banzai/filter/video_link_filter_spec.rb50
-rw-r--r--spec/lib/banzai/object_renderer_spec.rb44
-rw-r--r--spec/lib/banzai/redactor_spec.rb24
-rw-r--r--spec/lib/banzai/reference_parser/issue_parser_spec.rb12
-rw-r--r--spec/lib/ci/charts_spec.rb20
-rw-r--r--spec/lib/ci/gitlab_ci_yaml_processor_spec.rb230
-rw-r--r--spec/lib/container_registry/blob_spec.rb52
-rw-r--r--spec/lib/container_registry/tag_spec.rb49
-rw-r--r--spec/lib/disable_email_interceptor_spec.rb2
-rw-r--r--spec/lib/extracts_path_spec.rb34
-rw-r--r--spec/lib/gitlab/akismet_helper_spec.rb35
-rw-r--r--spec/lib/gitlab/asciidoc_spec.rb4
-rw-r--r--spec/lib/gitlab/auth_spec.rb10
-rw-r--r--spec/lib/gitlab/badge/build/metadata_spec.rb27
-rw-r--r--spec/lib/gitlab/badge/build/status_spec.rb94
-rw-r--r--spec/lib/gitlab/badge/build/template_spec.rb82
-rw-r--r--spec/lib/gitlab/badge/build_spec.rb123
-rw-r--r--spec/lib/gitlab/badge/coverage/metadata_spec.rb30
-rw-r--r--spec/lib/gitlab/badge/coverage/report_spec.rb106
-rw-r--r--spec/lib/gitlab/badge/coverage/template_spec.rb130
-rw-r--r--spec/lib/gitlab/badge/shared/metadata.rb21
-rw-r--r--spec/lib/gitlab/bitbucket_import/client_spec.rb2
-rw-r--r--spec/lib/gitlab/changes_list_spec.rb30
-rw-r--r--spec/lib/gitlab/checks/change_access_spec.rb99
-rw-r--r--spec/lib/gitlab/ci/config/node/artifacts_spec.rb45
-rw-r--r--spec/lib/gitlab/ci/config/node/attributable_spec.rb43
-rw-r--r--spec/lib/gitlab/ci/config/node/commands_spec.rb49
-rw-r--r--spec/lib/gitlab/ci/config/node/factory_spec.rb38
-rw-r--r--spec/lib/gitlab/ci/config/node/global_spec.rb66
-rw-r--r--spec/lib/gitlab/ci/config/node/hidden_job_spec.rb58
-rw-r--r--spec/lib/gitlab/ci/config/node/job_spec.rb86
-rw-r--r--spec/lib/gitlab/ci/config/node/jobs_spec.rb87
-rw-r--r--spec/lib/gitlab/ci/config/node/null_spec.rb41
-rw-r--r--spec/lib/gitlab/ci/config/node/stage_spec.rb38
-rw-r--r--spec/lib/gitlab/ci/config/node/trigger_spec.rb56
-rw-r--r--spec/lib/gitlab/ci/config/node/undefined_spec.rb32
-rw-r--r--spec/lib/gitlab/ci/config/node/validatable_spec.rb4
-rw-r--r--spec/lib/gitlab/closing_issue_extractor_spec.rb11
-rw-r--r--spec/lib/gitlab/conflict/file_collection_spec.rb24
-rw-r--r--spec/lib/gitlab/conflict/file_spec.rb261
-rw-r--r--spec/lib/gitlab/conflict/parser_spec.rb193
-rw-r--r--spec/lib/gitlab/data_builder/build_spec.rb (renamed from spec/lib/gitlab/build_data_builder_spec.rb)6
-rw-r--r--spec/lib/gitlab/data_builder/note_spec.rb (renamed from spec/lib/gitlab/note_data_builder_spec.rb)4
-rw-r--r--spec/lib/gitlab/data_builder/pipeline_spec.rb36
-rw-r--r--spec/lib/gitlab/data_builder/push_spec.rb (renamed from spec/lib/gitlab/push_data_builder_spec.rb)2
-rw-r--r--spec/lib/gitlab/database/migration_helpers_spec.rb4
-rw-r--r--spec/lib/gitlab/diff/file_spec.rb20
-rw-r--r--spec/lib/gitlab/diff/highlight_spec.rb6
-rw-r--r--spec/lib/gitlab/diff/inline_diff_spec.rb28
-rw-r--r--spec/lib/gitlab/diff/line_mapper_spec.rb2
-rw-r--r--spec/lib/gitlab/diff/parallel_diff_spec.rb48
-rw-r--r--spec/lib/gitlab/diff/parser_spec.rb4
-rw-r--r--spec/lib/gitlab/diff/position_spec.rb66
-rw-r--r--spec/lib/gitlab/diff/position_tracer_spec.rb3
-rw-r--r--spec/lib/gitlab/downtime_check/message_spec.rb39
-rw-r--r--spec/lib/gitlab/downtime_check_spec.rb113
-rw-r--r--spec/lib/gitlab/email/attachment_uploader_spec.rb1
-rw-r--r--spec/lib/gitlab/email/email_shared_blocks.rb41
-rw-r--r--spec/lib/gitlab/email/handler/create_issue_handler_spec.rb79
-rw-r--r--spec/lib/gitlab/email/handler/create_note_handler_spec.rb177
-rw-r--r--spec/lib/gitlab/email/message/repository_push_spec.rb13
-rw-r--r--spec/lib/gitlab/email/receiver_spec.rb158
-rw-r--r--spec/lib/gitlab/git/hook_spec.rb1
-rw-r--r--spec/lib/gitlab/git_access_spec.rb353
-rw-r--r--spec/lib/gitlab/github_import/branch_formatter_spec.rb14
-rw-r--r--spec/lib/gitlab/github_import/client_spec.rb7
-rw-r--r--spec/lib/gitlab/github_import/hook_formatter_spec.rb65
-rw-r--r--spec/lib/gitlab/github_import/importer_spec.rb132
-rw-r--r--spec/lib/gitlab/github_import/pull_request_formatter_spec.rb56
-rw-r--r--spec/lib/gitlab/gitlab_import/importer_spec.rb53
-rw-r--r--spec/lib/gitlab/gitorious_import/project_creator_spec.rb26
-rw-r--r--spec/lib/gitlab/highlight_spec.rb12
-rw-r--r--spec/lib/gitlab/import_export/avatar_restorer_spec.rb25
-rw-r--r--spec/lib/gitlab/import_export/avatar_saver_spec.rb27
-rw-r--r--spec/lib/gitlab/import_export/import_export_spec.rb21
-rw-r--r--spec/lib/gitlab/import_export/members_mapper_spec.rb20
-rw-r--r--spec/lib/gitlab/import_export/project.json214
-rw-r--r--spec/lib/gitlab/import_export/project_tree_restorer_spec.rb66
-rw-r--r--spec/lib/gitlab/import_export/project_tree_saver_spec.rb37
-rw-r--r--spec/lib/gitlab/import_export/reader_spec.rb3
-rw-r--r--spec/lib/gitlab/import_export/version_checker_spec.rb30
-rw-r--r--spec/lib/gitlab/incoming_email_spec.rb4
-rw-r--r--spec/lib/gitlab/ldap/access_spec.rb4
-rw-r--r--spec/lib/gitlab/ldap/user_spec.rb4
-rw-r--r--spec/lib/gitlab/lfs/lfs_router_spec.rb730
-rw-r--r--spec/lib/gitlab/metrics/instrumentation_spec.rb12
-rw-r--r--spec/lib/gitlab/metrics/metric_spec.rb18
-rw-r--r--spec/lib/gitlab/metrics/rack_middleware_spec.rb30
-rw-r--r--spec/lib/gitlab/metrics/sidekiq_middleware_spec.rb24
-rw-r--r--spec/lib/gitlab/metrics/system_spec.rb12
-rw-r--r--spec/lib/gitlab/metrics/transaction_spec.rb73
-rw-r--r--spec/lib/gitlab/metrics_spec.rb30
-rw-r--r--spec/lib/gitlab/middleware/rails_queue_duration_spec.rb2
-rw-r--r--spec/lib/gitlab/o_auth/user_spec.rb6
-rw-r--r--spec/lib/gitlab/project_search_results_spec.rb12
-rw-r--r--spec/lib/gitlab/redis_spec.rb79
-rw-r--r--spec/lib/gitlab/saml/user_spec.rb10
-rw-r--r--spec/lib/gitlab/search_results_spec.rb12
-rw-r--r--spec/lib/gitlab/slash_commands/command_definition_spec.rb173
-rw-r--r--spec/lib/gitlab/slash_commands/dsl_spec.rb77
-rw-r--r--spec/lib/gitlab/slash_commands/extractor_spec.rb215
-rw-r--r--spec/lib/gitlab/template/gitignore_template_spec.rb (renamed from spec/lib/gitlab/template/gitignore_spec.rb)4
-rw-r--r--spec/lib/gitlab/template/gitlab_ci_yml_template_spec.rb41
-rw-r--r--spec/lib/gitlab/template/issue_template_spec.rb89
-rw-r--r--spec/lib/gitlab/template/merge_request_template_spec.rb89
-rw-r--r--spec/lib/gitlab/upgrader_spec.rb6
-rw-r--r--spec/lib/gitlab/user_access_spec.rb138
-rw-r--r--spec/lib/repository_cache_spec.rb13
-rw-r--r--spec/mailers/emails/merge_requests_spec.rb19
-rw-r--r--spec/mailers/emails/profile_spec.rb2
-rw-r--r--spec/mailers/notify_spec.rb28
-rw-r--r--spec/models/ability_spec.rb168
-rw-r--r--spec/models/application_setting_spec.rb61
-rw-r--r--spec/models/blob_spec.rb38
-rw-r--r--spec/models/board_spec.rb12
-rw-r--r--spec/models/broadcast_message_spec.rb8
-rw-r--r--spec/models/build_spec.rb472
-rw-r--r--spec/models/ci/pipeline_spec.rb514
-rw-r--r--spec/models/ci/trigger_spec.rb4
-rw-r--r--spec/models/ci/variable_spec.rb2
-rw-r--r--spec/models/commit_spec.rb42
-rw-r--r--spec/models/commit_status_spec.rb50
-rw-r--r--spec/models/compare_spec.rb77
-rw-r--r--spec/models/concerns/faster_cache_keys_spec.rb17
-rw-r--r--spec/models/concerns/has_status_spec.rb (renamed from spec/models/concerns/statuseable_spec.rb)6
-rw-r--r--spec/models/concerns/mentionable_spec.rb8
-rw-r--r--spec/models/concerns/milestoneish_spec.rb36
-rw-r--r--spec/models/concerns/spammable_spec.rb33
-rw-r--r--spec/models/concerns/token_authenticatable_spec.rb2
-rw-r--r--spec/models/deployment_spec.rb25
-rw-r--r--spec/models/diff_note_spec.rb302
-rw-r--r--spec/models/discussion_spec.rb615
-rw-r--r--spec/models/environment_spec.rb52
-rw-r--r--spec/models/forked_project_link_spec.rb12
-rw-r--r--spec/models/generic_commit_status_spec.rb10
-rw-r--r--spec/models/global_milestone_spec.rb18
-rw-r--r--spec/models/group_spec.rb16
-rw-r--r--spec/models/hooks/project_hook_spec.rb4
-rw-r--r--spec/models/hooks/system_hook_spec.rb2
-rw-r--r--spec/models/issue_spec.rb273
-rw-r--r--spec/models/key_spec.rb9
-rw-r--r--spec/models/label_spec.rb6
-rw-r--r--spec/models/legacy_diff_note_spec.rb39
-rw-r--r--spec/models/list_spec.rb117
-rw-r--r--spec/models/member_spec.rb28
-rw-r--r--spec/models/members/group_member_spec.rb4
-rw-r--r--spec/models/members/project_member_spec.rb11
-rw-r--r--spec/models/merge_request_diff_spec.rb68
-rw-r--r--spec/models/merge_request_spec.rb347
-rw-r--r--spec/models/milestone_spec.rb30
-rw-r--r--spec/models/namespace_spec.rb14
-rw-r--r--spec/models/network/graph_spec.rb12
-rw-r--r--spec/models/note_spec.rb141
-rw-r--r--spec/models/project_security_spec.rb20
-rw-r--r--spec/models/project_services/asana_service_spec.rb8
-rw-r--r--spec/models/project_services/assembla_service_spec.rb4
-rw-r--r--spec/models/project_services/buildkite_service_spec.rb6
-rw-r--r--spec/models/project_services/builds_email_service_spec.rb42
-rw-r--r--spec/models/project_services/campfire_service_spec.rb58
-rw-r--r--spec/models/project_services/drone_ci_service_spec.rb4
-rw-r--r--spec/models/project_services/external_wiki_service_spec.rb2
-rw-r--r--spec/models/project_services/flowdock_service_spec.rb4
-rw-r--r--spec/models/project_services/gemnasium_service_spec.rb4
-rw-r--r--spec/models/project_services/gitlab_issue_tracker_service_spec.rb4
-rw-r--r--spec/models/project_services/hipchat_service_spec.rb95
-rw-r--r--spec/models/project_services/irker_service_spec.rb13
-rw-r--r--spec/models/project_services/jira_service_spec.rb12
-rw-r--r--spec/models/project_services/pivotaltracker_service_spec.rb71
-rw-r--r--spec/models/project_services/pushover_service_spec.rb6
-rw-r--r--spec/models/project_services/slack_service/note_message_spec.rb1
-rw-r--r--spec/models/project_services/slack_service/wiki_page_message_spec.rb4
-rw-r--r--spec/models/project_services/slack_service_spec.rb103
-rw-r--r--spec/models/project_spec.rb487
-rw-r--r--spec/models/project_team_spec.rb82
-rw-r--r--spec/models/repository_spec.rb277
-rw-r--r--spec/models/service_spec.rb10
-rw-r--r--spec/models/user_agent_detail_spec.rb31
-rw-r--r--spec/models/user_spec.rb181
-rw-r--r--spec/models/wiki_page_spec.rb6
-rw-r--r--spec/requests/api/access_requests_spec.rb246
-rw-r--r--spec/requests/api/api_helpers_spec.rb84
-rw-r--r--spec/requests/api/award_emoji_spec.rb38
-rw-r--r--spec/requests/api/branches_spec.rb127
-rw-r--r--spec/requests/api/builds_spec.rb173
-rw-r--r--spec/requests/api/commit_statuses_spec.rb10
-rw-r--r--spec/requests/api/commits_spec.rb63
-rw-r--r--spec/requests/api/deploy_keys_spec.rb160
-rw-r--r--spec/requests/api/deployments_spec.rb60
-rw-r--r--spec/requests/api/environments_spec.rb130
-rw-r--r--spec/requests/api/files_spec.rb22
-rw-r--r--spec/requests/api/fork_spec.rb12
-rw-r--r--spec/requests/api/group_members_spec.rb199
-rw-r--r--spec/requests/api/groups_spec.rb56
-rw-r--r--spec/requests/api/internal_spec.rb94
-rw-r--r--spec/requests/api/issues_spec.rb137
-rw-r--r--spec/requests/api/keys_spec.rb6
-rw-r--r--spec/requests/api/labels_spec.rb60
-rw-r--r--spec/requests/api/members_spec.rb314
-rw-r--r--spec/requests/api/merge_request_diffs_spec.rb49
-rw-r--r--spec/requests/api/merge_requests_spec.rb93
-rw-r--r--spec/requests/api/milestones_spec.rb30
-rw-r--r--spec/requests/api/namespaces_spec.rb10
-rw-r--r--spec/requests/api/notes_spec.rb58
-rw-r--r--spec/requests/api/oauth_tokens_spec.rb33
-rw-r--r--spec/requests/api/pipelines_spec.rb133
-rw-r--r--spec/requests/api/project_hooks_spec.rb52
-rw-r--r--spec/requests/api/project_members_spec.rb166
-rw-r--r--spec/requests/api/project_snippets_spec.rb3
-rw-r--r--spec/requests/api/projects_spec.rb287
-rw-r--r--spec/requests/api/repositories_spec.rb40
-rw-r--r--spec/requests/api/runners_spec.rb98
-rw-r--r--spec/requests/api/services_spec.rb22
-rw-r--r--spec/requests/api/session_spec.rb23
-rw-r--r--spec/requests/api/settings_spec.rb4
-rw-r--r--spec/requests/api/system_hooks_spec.rb20
-rw-r--r--spec/requests/api/tags_spec.rb34
-rw-r--r--spec/requests/api/templates_spec.rb65
-rw-r--r--spec/requests/api/todos_spec.rb15
-rw-r--r--spec/requests/api/triggers_spec.rb47
-rw-r--r--spec/requests/api/users_spec.rb229
-rw-r--r--spec/requests/api/variables_spec.rb38
-rw-r--r--spec/requests/ci/api/builds_spec.rb191
-rw-r--r--spec/requests/ci/api/triggers_spec.rb19
-rw-r--r--spec/requests/git_http_spec.rb57
-rw-r--r--spec/requests/lfs_http_spec.rb797
-rw-r--r--spec/routing/admin_routing_spec.rb5
-rw-r--r--spec/routing/project_routing_spec.rb17
-rw-r--r--spec/routing/routing_spec.rb30
-rw-r--r--spec/services/auth/container_registry_authentication_service_spec.rb104
-rw-r--r--spec/services/boards/create_service_spec.rb35
-rw-r--r--spec/services/boards/issues/list_service_spec.rb73
-rw-r--r--spec/services/boards/issues/move_service_spec.rb140
-rw-r--r--spec/services/boards/lists/create_service_spec.rb54
-rw-r--r--spec/services/boards/lists/destroy_service_spec.rb47
-rw-r--r--spec/services/boards/lists/generate_service_spec.rb40
-rw-r--r--spec/services/boards/lists/move_service_spec.rb110
-rw-r--r--spec/services/ci/create_builds_service_spec.rb32
-rw-r--r--spec/services/ci/create_pipeline_service_spec.rb214
-rw-r--r--spec/services/ci/create_trigger_request_service_spec.rb7
-rw-r--r--spec/services/ci/image_for_build_service_spec.rb6
-rw-r--r--spec/services/ci/process_pipeline_service_spec.rb328
-rw-r--r--spec/services/ci/register_build_service_spec.rb2
-rw-r--r--spec/services/create_commit_builds_service_spec.rb240
-rw-r--r--spec/services/create_deployment_service_spec.rb16
-rw-r--r--spec/services/create_snippet_service_spec.rb4
-rw-r--r--spec/services/delete_user_service_spec.rb8
-rw-r--r--spec/services/destroy_group_service_spec.rb58
-rw-r--r--spec/services/event_create_service_spec.rb34
-rw-r--r--spec/services/files/update_service_spec.rb84
-rw-r--r--spec/services/git_hooks_service_spec.rb6
-rw-r--r--spec/services/git_push_service_spec.rb32
-rw-r--r--spec/services/import_export_clean_up_service_spec.rb64
-rw-r--r--spec/services/issues/bulk_update_service_spec.rb125
-rw-r--r--spec/services/issues/close_service_spec.rb24
-rw-r--r--spec/services/issues/create_service_spec.rb2
-rw-r--r--spec/services/issues/reopen_service_spec.rb25
-rw-r--r--spec/services/issues/update_service_spec.rb13
-rw-r--r--spec/services/merge_requests/build_service_spec.rb8
-rw-r--r--spec/services/merge_requests/close_service_spec.rb22
-rw-r--r--spec/services/merge_requests/create_service_spec.rb15
-rw-r--r--spec/services/merge_requests/get_urls_service_spec.rb134
-rw-r--r--spec/services/merge_requests/merge_request_diff_cache_service_spec.rb17
-rw-r--r--spec/services/merge_requests/merge_service_spec.rb17
-rw-r--r--spec/services/merge_requests/merge_when_build_succeeds_service_spec.rb10
-rw-r--r--spec/services/merge_requests/refresh_service_spec.rb15
-rw-r--r--spec/services/merge_requests/reopen_service_spec.rb27
-rw-r--r--spec/services/merge_requests/resolved_discussion_notification_service.rb46
-rw-r--r--spec/services/merge_requests/update_service_spec.rb13
-rw-r--r--spec/services/milestones/close_service_spec.rb2
-rw-r--r--spec/services/milestones/create_service_spec.rb2
-rw-r--r--spec/services/notes/create_service_spec.rb34
-rw-r--r--spec/services/notes/post_process_service_spec.rb2
-rw-r--r--spec/services/notes/slash_commands_service_spec.rb140
-rw-r--r--spec/services/notification_service_spec.rb158
-rw-r--r--spec/services/projects/autocomplete_service_spec.rb14
-rw-r--r--spec/services/projects/create_service_spec.rb6
-rw-r--r--spec/services/projects/download_service_spec.rb4
-rw-r--r--spec/services/projects/enable_deploy_key_service_spec.rb27
-rw-r--r--spec/services/projects/fork_service_spec.rb8
-rw-r--r--spec/services/projects/housekeeping_service_spec.rb44
-rw-r--r--spec/services/projects/update_service_spec.rb18
-rw-r--r--spec/services/projects/upload_service_spec.rb8
-rw-r--r--spec/services/repair_ldap_blocked_user_service_spec.rb4
-rw-r--r--spec/services/repository_archive_clean_up_service_spec.rb81
-rw-r--r--spec/services/search_service_spec.rb8
-rw-r--r--spec/services/slash_commands/interpret_service_spec.rb384
-rw-r--r--spec/services/system_note_service_spec.rb22
-rw-r--r--spec/services/test_hook_service_spec.rb4
-rw-r--r--spec/services/todo_service_spec.rb152
-rw-r--r--spec/simplecov_env.rb55
-rw-r--r--spec/spec_helper.rb15
-rw-r--r--spec/support/api/members_shared_examples.rb11
-rw-r--r--spec/support/api/schema_matcher.rb8
-rw-r--r--spec/support/api_helpers.rb14
-rw-r--r--spec/support/capybara_helpers.rb8
-rw-r--r--spec/support/email_helpers.rb10
-rw-r--r--spec/support/fake_u2f_device.rb9
-rw-r--r--spec/support/import_export/import_export.yml4
-rw-r--r--spec/support/issuable_create_service_slash_commands_shared_examples.rb83
-rw-r--r--spec/support/issuable_slash_commands_shared_examples.rb289
-rw-r--r--spec/support/issue_helpers.rb13
-rw-r--r--spec/support/login_helpers.rb34
-rw-r--r--spec/support/matchers/markdown_matchers.rb11
-rw-r--r--spec/support/merge_request_helpers.rb13
-rw-r--r--spec/support/omni_auth.rb1
-rw-r--r--spec/support/relative_url.rb8
-rw-r--r--spec/support/select2_helper.rb2
-rw-r--r--spec/support/test_env.rb39
-rw-r--r--spec/support/updating_mentions_shared_examples.rb32
-rw-r--r--spec/tasks/gitlab/backup_rake_spec.rb16
-rw-r--r--spec/tasks/gitlab/db_rake_spec.rb8
-rw-r--r--spec/teaspoon_env.rb10
-rw-r--r--spec/uploaders/file_uploader_spec.rb45
-rw-r--r--spec/views/admin/dashboard/index.html.haml_spec.rb20
-rw-r--r--spec/views/devise/shared/_signin_box.html.haml_spec.rb2
-rw-r--r--spec/views/layouts/_head.html.haml_spec.rb36
-rw-r--r--spec/views/projects/builds/show.html.haml_spec.rb39
-rw-r--r--spec/views/projects/issues/_related_branches.html.haml_spec.rb21
-rw-r--r--spec/views/projects/merge_requests/_heading.html.haml_spec.rb26
-rw-r--r--spec/views/projects/tree/show.html.haml_spec.rb37
-rw-r--r--spec/workers/build_email_worker_spec.rb2
-rw-r--r--spec/workers/email_receiver_worker_spec.rb2
-rw-r--r--spec/workers/emails_on_push_worker_spec.rb66
-rw-r--r--spec/workers/git_garbage_collect_worker_spec.rb27
-rw-r--r--spec/workers/group_destroy_worker_spec.rb19
-rw-r--r--spec/workers/post_receive_spec.rb14
-rw-r--r--spec/workers/project_destroy_worker_spec.rb24
-rw-r--r--spec/workers/remove_expired_group_links_worker_spec.rb24
-rw-r--r--spec/workers/remove_expired_members_worker_spec.rb58
-rw-r--r--spec/workers/repository_fork_worker_spec.rb10
595 files changed, 26714 insertions, 9407 deletions
diff --git a/spec/config/mail_room_spec.rb b/spec/config/mail_room_spec.rb
index 6fad7e2b9e7..c5d3cd70acc 100644
--- a/spec/config/mail_room_spec.rb
+++ b/spec/config/mail_room_spec.rb
@@ -1,53 +1,48 @@
-require "spec_helper"
+require 'spec_helper'
-describe "mail_room.yml" do
- let(:config_path) { "config/mail_room.yml" }
+describe 'mail_room.yml' do
+ let(:config_path) { 'config/mail_room.yml' }
let(:configuration) { YAML.load(ERB.new(File.read(config_path)).result) }
- context "when incoming email is disabled" do
+ context 'when incoming email is disabled' do
before do
- ENV["MAIL_ROOM_GITLAB_CONFIG_FILE"] = Rails.root.join("spec/fixtures/mail_room_disabled.yml").to_s
+ ENV['MAIL_ROOM_GITLAB_CONFIG_FILE'] = Rails.root.join('spec/fixtures/mail_room_disabled.yml').to_s
+ Gitlab::MailRoom.reset_config!
end
after do
- ENV["MAIL_ROOM_GITLAB_CONFIG_FILE"] = nil
+ ENV['MAIL_ROOM_GITLAB_CONFIG_FILE'] = nil
end
- it "contains no configuration" do
+ it 'contains no configuration' do
expect(configuration[:mailboxes]).to be_nil
end
end
- context "when incoming email is enabled" do
+ context 'when incoming email is enabled' do
before do
- ENV["MAIL_ROOM_GITLAB_CONFIG_FILE"] = Rails.root.join("spec/fixtures/mail_room_enabled.yml").to_s
+ ENV['MAIL_ROOM_GITLAB_CONFIG_FILE'] = Rails.root.join('spec/fixtures/mail_room_enabled.yml').to_s
+ Gitlab::MailRoom.reset_config!
end
after do
- ENV["MAIL_ROOM_GITLAB_CONFIG_FILE"] = nil
+ ENV['MAIL_ROOM_GITLAB_CONFIG_FILE'] = nil
end
- it "contains the intended configuration" do
+ it 'contains the intended configuration' do
expect(configuration[:mailboxes].length).to eq(1)
mailbox = configuration[:mailboxes].first
- expect(mailbox[:host]).to eq("imap.gmail.com")
+ expect(mailbox[:host]).to eq('imap.gmail.com')
expect(mailbox[:port]).to eq(993)
expect(mailbox[:ssl]).to eq(true)
expect(mailbox[:start_tls]).to eq(false)
- expect(mailbox[:email]).to eq("gitlab-incoming@gmail.com")
- expect(mailbox[:password]).to eq("[REDACTED]")
- expect(mailbox[:name]).to eq("inbox")
-
- redis_config_file = Rails.root.join('config', 'resque.yml')
-
- redis_url =
- if File.exist?(redis_config_file)
- YAML.load_file(redis_config_file)[Rails.env]
- else
- "redis://localhost:6379"
- end
+ expect(mailbox[:email]).to eq('gitlab-incoming@gmail.com')
+ expect(mailbox[:password]).to eq('[REDACTED]')
+ expect(mailbox[:name]).to eq('inbox')
+
+ redis_url = Gitlab::Redis.url
expect(mailbox[:delivery_options][:redis_url]).to eq(redis_url)
expect(mailbox[:arbitration_options][:redis_url]).to eq(redis_url)
diff --git a/spec/controllers/admin/groups_controller_spec.rb b/spec/controllers/admin/groups_controller_spec.rb
new file mode 100644
index 00000000000..602de72d23f
--- /dev/null
+++ b/spec/controllers/admin/groups_controller_spec.rb
@@ -0,0 +1,25 @@
+require 'spec_helper'
+
+describe Admin::GroupsController do
+ let(:group) { create(:group) }
+ let(:project) { create(:project, namespace: group) }
+ let(:admin) { create(:admin) }
+
+ before do
+ sign_in(admin)
+ end
+
+ describe 'DELETE #destroy' do
+ it 'schedules a group destroy' do
+ Sidekiq::Testing.fake! do
+ expect { delete :destroy, id: project.group.path }.to change(GroupDestroyWorker.jobs, :size).by(1)
+ end
+ end
+
+ it 'redirects to the admin group path' do
+ delete :destroy, id: project.group.path
+
+ expect(response).to redirect_to(admin_groups_path)
+ end
+ end
+end
diff --git a/spec/controllers/admin/impersonations_controller_spec.rb b/spec/controllers/admin/impersonations_controller_spec.rb
index d5f0b289b5b..8be662974a0 100644
--- a/spec/controllers/admin/impersonations_controller_spec.rb
+++ b/spec/controllers/admin/impersonations_controller_spec.rb
@@ -77,6 +77,8 @@ describe Admin::ImpersonationsController do
context "when the impersonator is not blocked" do
it "redirects to the impersonated user's page" do
+ expect(Gitlab::AppLogger).to receive(:info).with("User #{impersonator.username} has stopped impersonating #{user.username}").and_call_original
+
delete :destroy
expect(response).to redirect_to(admin_user_path(user))
diff --git a/spec/controllers/admin/spam_logs_controller_spec.rb b/spec/controllers/admin/spam_logs_controller_spec.rb
index 520a4f6f9c5..585ca31389d 100644
--- a/spec/controllers/admin/spam_logs_controller_spec.rb
+++ b/spec/controllers/admin/spam_logs_controller_spec.rb
@@ -34,4 +34,16 @@ describe Admin::SpamLogsController do
expect { User.find(user.id) }.to raise_error(ActiveRecord::RecordNotFound)
end
end
+
+ describe '#mark_as_ham' do
+ before do
+ allow_any_instance_of(AkismetService).to receive(:submit_ham).and_return(true)
+ end
+ it 'submits the log as ham' do
+ post :mark_as_ham, id: first_spam.id
+
+ expect(response).to have_http_status(302)
+ expect(SpamLog.find(first_spam.id).submitted_as_ham).to be_truthy
+ end
+ end
end
diff --git a/spec/controllers/admin/users_controller_spec.rb b/spec/controllers/admin/users_controller_spec.rb
index ab9aa65f7b9..33fe3c73822 100644
--- a/spec/controllers/admin/users_controller_spec.rb
+++ b/spec/controllers/admin/users_controller_spec.rb
@@ -39,7 +39,7 @@ describe Admin::UsersController do
user.ldap_block
end
- it 'will not unblock user' do
+ it 'does not unblock user' do
put :unblock, id: user.username
user.reload
expect(user.blocked?).to be_truthy
diff --git a/spec/controllers/application_controller_spec.rb b/spec/controllers/application_controller_spec.rb
index 8bd210cbc3d..98e912f000c 100644
--- a/spec/controllers/application_controller_spec.rb
+++ b/spec/controllers/application_controller_spec.rb
@@ -5,7 +5,7 @@ describe ApplicationController do
let(:user) { create(:user) }
let(:controller) { ApplicationController.new }
- it 'should redirect if the user is over their password expiry' do
+ it 'redirects if the user is over their password expiry' do
user.password_expires_at = Time.new(2002)
expect(user.ldap_user?).to be_falsey
allow(controller).to receive(:current_user).and_return(user)
@@ -14,7 +14,7 @@ describe ApplicationController do
controller.send(:check_password_expiration)
end
- it 'should not redirect if the user is under their password expiry' do
+ it 'does not redirect if the user is under their password expiry' do
user.password_expires_at = Time.now + 20010101
expect(user.ldap_user?).to be_falsey
allow(controller).to receive(:current_user).and_return(user)
@@ -22,7 +22,7 @@ describe ApplicationController do
controller.send(:check_password_expiration)
end
- it 'should not redirect if the user is over their password expiry but they are an ldap user' do
+ it 'does not redirect if the user is over their password expiry but they are an ldap user' do
user.password_expires_at = Time.new(2002)
allow(user).to receive(:ldap_user?).and_return(true)
allow(controller).to receive(:current_user).and_return(user)
diff --git a/spec/controllers/autocomplete_controller_spec.rb b/spec/controllers/autocomplete_controller_spec.rb
index 60c654f622d..a121cb2fc97 100644
--- a/spec/controllers/autocomplete_controller_spec.rb
+++ b/spec/controllers/autocomplete_controller_spec.rb
@@ -2,165 +2,312 @@ require 'spec_helper'
describe AutocompleteController do
let!(:project) { create(:project) }
- let!(:user) { create(:user) }
- let!(:user2) { create(:user) }
- let!(:non_member) { create(:user) }
+ let!(:user) { create(:user) }
- context 'project members' do
- before do
- sign_in(user)
- project.team << [user, :master]
- end
+ context 'users and members' do
+ let!(:user2) { create(:user) }
+ let!(:non_member) { create(:user) }
- describe 'GET #users with project ID' do
+ context 'project members' do
before do
- get(:users, project_id: project.id)
+ sign_in(user)
+ project.team << [user, :master]
end
- let(:body) { JSON.parse(response.body) }
+ describe 'GET #users with project ID' do
+ before do
+ get(:users, project_id: project.id)
+ end
- it { expect(body).to be_kind_of(Array) }
- it { expect(body.size).to eq 1 }
- it { expect(body.map { |u| u["username"] }).to include(user.username) }
+ let(:body) { JSON.parse(response.body) }
+
+ it { expect(body).to be_kind_of(Array) }
+ it { expect(body.size).to eq 1 }
+ it { expect(body.map { |u| u["username"] }).to include(user.username) }
+ end
+
+ describe 'GET #users with unknown project' do
+ before do
+ get(:users, project_id: 'unknown')
+ end
+
+ it { expect(response).to have_http_status(404) }
+ end
end
- describe 'GET #users with unknown project' do
+ context 'group members' do
+ let(:group) { create(:group) }
+
before do
- get(:users, project_id: 'unknown')
+ sign_in(user)
+ group.add_owner(user)
end
- it { expect(response).to have_http_status(404) }
- end
- end
+ let(:body) { JSON.parse(response.body) }
- context 'group members' do
- let(:group) { create(:group) }
+ describe 'GET #users with group ID' do
+ before do
+ get(:users, group_id: group.id)
+ end
- before do
- sign_in(user)
- group.add_owner(user)
+ it { expect(body).to be_kind_of(Array) }
+ it { expect(body.size).to eq 1 }
+ it { expect(body.first["username"]).to eq user.username }
+ end
+
+ describe 'GET #users with unknown group ID' do
+ before do
+ get(:users, group_id: 'unknown')
+ end
+
+ it { expect(response).to have_http_status(404) }
+ end
end
- let(:body) { JSON.parse(response.body) }
+ context 'non-member login for public project' do
+ let!(:project) { create(:project, :public) }
- describe 'GET #users with group ID' do
before do
- get(:users, group_id: group.id)
+ sign_in(non_member)
+ project.team << [user, :master]
end
- it { expect(body).to be_kind_of(Array) }
- it { expect(body.size).to eq 1 }
- it { expect(body.first["username"]).to eq user.username }
+ let(:body) { JSON.parse(response.body) }
+
+ describe 'GET #users with project ID' do
+ before do
+ get(:users, project_id: project.id, current_user: true)
+ end
+
+ it { expect(body).to be_kind_of(Array) }
+ it { expect(body.size).to eq 2 }
+ it { expect(body.map { |u| u['username'] }).to match_array([user.username, non_member.username]) }
+ end
end
- describe 'GET #users with unknown group ID' do
+ context 'all users' do
before do
- get(:users, group_id: 'unknown')
+ sign_in(user)
+ get(:users)
end
- it { expect(response).to have_http_status(404) }
+ let(:body) { JSON.parse(response.body) }
+
+ it { expect(body).to be_kind_of(Array) }
+ it { expect(body.size).to eq User.count }
end
- end
- context 'non-member login for public project' do
- let!(:project) { create(:project, :public) }
+ context 'unauthenticated user' do
+ let(:public_project) { create(:project, :public) }
+ let(:body) { JSON.parse(response.body) }
- before do
- sign_in(non_member)
- project.team << [user, :master]
- end
+ describe 'GET #users with public project' do
+ before do
+ public_project.team << [user, :guest]
+ get(:users, project_id: public_project.id)
+ end
+
+ it { expect(body).to be_kind_of(Array) }
+ it { expect(body.size).to eq 1 }
+ end
- let(:body) { JSON.parse(response.body) }
+ describe 'GET #users with project' do
+ before do
+ get(:users, project_id: project.id)
+ end
- describe 'GET #users with project ID' do
- before do
- get(:users, project_id: project.id, current_user: true)
+ it { expect(response).to have_http_status(404) }
end
- it { expect(body).to be_kind_of(Array) }
- it { expect(body.size).to eq 2 }
- it { expect(body.map { |u| u['username'] }).to match_array([user.username, non_member.username]) }
+ describe 'GET #users with unknown project' do
+ before do
+ get(:users, project_id: 'unknown')
+ end
+
+ it { expect(response).to have_http_status(404) }
+ end
+
+ describe 'GET #users with inaccessible group' do
+ before do
+ project.team << [user, :guest]
+ get(:users, group_id: user.namespace.id)
+ end
+
+ it { expect(response).to have_http_status(404) }
+ end
+
+ describe 'GET #users with no project' do
+ before do
+ get(:users)
+ end
+
+ it { expect(body).to be_kind_of(Array) }
+ it { expect(body.size).to eq 0 }
+ end
end
- end
- context 'all users' do
- before do
- sign_in(user)
- get(:users)
+ context 'author of issuable included' do
+ before do
+ sign_in(user)
+ end
+
+ let(:body) { JSON.parse(response.body) }
+
+ it 'includes the author' do
+ get(:users, author_id: non_member.id)
+
+ expect(body.first["username"]).to eq non_member.username
+ end
+
+ it 'rejects non existent user ids' do
+ get(:users, author_id: 99999)
+
+ expect(body.collect { |u| u['id'] }).not_to include(99999)
+ end
end
- let(:body) { JSON.parse(response.body) }
+ context 'skip_users parameter included' do
+ before { sign_in(user) }
- it { expect(body).to be_kind_of(Array) }
- it { expect(body.size).to eq User.count }
- end
+ it 'skips the user IDs passed' do
+ get(:users, skip_users: [user, user2].map(&:id))
- context 'unauthenticated user' do
- let(:public_project) { create(:project, :public) }
- let(:body) { JSON.parse(response.body) }
+ other_user_ids = [non_member, project.owner, project.creator].map(&:id)
+ response_user_ids = JSON.parse(response.body).map { |user| user['id'] }
- describe 'GET #users with public project' do
- before do
- public_project.team << [user, :guest]
- get(:users, project_id: public_project.id)
+ expect(response_user_ids).to contain_exactly(*other_user_ids)
end
+ end
+ end
- it { expect(body).to be_kind_of(Array) }
- it { expect(body.size).to eq 1 }
+ context 'projects' do
+ let(:authorized_project) { create(:project) }
+ let(:authorized_search_project) { create(:project, name: 'rugged') }
+
+ before do
+ sign_in(user)
+ project.team << [user, :master]
end
- describe 'GET #users with project' do
+ context 'authorized projects' do
before do
- get(:users, project_id: project.id)
+ authorized_project.team << [user, :master]
end
- it { expect(response).to have_http_status(404) }
+ describe 'GET #projects with project ID' do
+ before do
+ get(:projects, project_id: project.id)
+ end
+
+ let(:body) { JSON.parse(response.body) }
+
+ it do
+ expect(body).to be_kind_of(Array)
+ expect(body.size).to eq 2
+
+ expect(body.first['id']).to eq 0
+ expect(body.first['name_with_namespace']).to eq 'No project'
+
+ expect(body.last['id']).to eq authorized_project.id
+ expect(body.last['name_with_namespace']).to eq authorized_project.name_with_namespace
+ end
+ end
end
- describe 'GET #users with unknown project' do
+ context 'authorized projects and search' do
before do
- get(:users, project_id: 'unknown')
+ authorized_project.team << [user, :master]
+ authorized_search_project.team << [user, :master]
end
- it { expect(response).to have_http_status(404) }
+ describe 'GET #projects with project ID and search' do
+ before do
+ get(:projects, project_id: project.id, search: 'rugged')
+ end
+
+ let(:body) { JSON.parse(response.body) }
+
+ it do
+ expect(body).to be_kind_of(Array)
+ expect(body.size).to eq 2
+
+ expect(body.last['id']).to eq authorized_search_project.id
+ expect(body.last['name_with_namespace']).to eq authorized_search_project.name_with_namespace
+ end
+ end
end
- describe 'GET #users with inaccessible group' do
+ context 'authorized projects apply limit' do
before do
- project.team << [user, :guest]
- get(:users, group_id: user.namespace.id)
+ authorized_project2 = create(:project)
+ authorized_project3 = create(:project)
+
+ authorized_project.team << [user, :master]
+ authorized_project2.team << [user, :master]
+ authorized_project3.team << [user, :master]
+
+ stub_const 'MoveToProjectFinder::PAGE_SIZE', 2
end
- it { expect(response).to have_http_status(404) }
+ describe 'GET #projects with project ID' do
+ before do
+ get(:projects, project_id: project.id)
+ end
+
+ let(:body) { JSON.parse(response.body) }
+
+ it do
+ expect(body).to be_kind_of(Array)
+ expect(body.size).to eq 3 # Of a total of 4
+ end
+ end
end
- describe 'GET #users with no project' do
+ context 'authorized projects with offset' do
before do
- get(:users)
+ authorized_project2 = create(:project)
+ authorized_project3 = create(:project)
+
+ authorized_project.team << [user, :master]
+ authorized_project2.team << [user, :master]
+ authorized_project3.team << [user, :master]
end
- it { expect(body).to be_kind_of(Array) }
- it { expect(body.size).to eq 0 }
- end
- end
+ describe 'GET #projects with project ID and offset_id' do
+ before do
+ get(:projects, project_id: project.id, offset_id: authorized_project.id)
+ end
- context 'author of issuable included' do
- before do
- sign_in(user)
+ let(:body) { JSON.parse(response.body) }
+
+ it do
+ expect(body.detect { |item| item['id'] == 0 }).to be_nil # 'No project' is not there
+ expect(body.detect { |item| item['id'] == authorized_project.id }).to be_nil # Offset project is not there either
+ end
+ end
end
- let(:body) { JSON.parse(response.body) }
+ context 'authorized projects without admin_issue ability' do
+ before(:each) do
+ authorized_project.team << [user, :guest]
+
+ expect(user.can?(:admin_issue, authorized_project)).to eq(false)
+ end
- it 'includes the author' do
- get(:users, author_id: non_member.id)
+ describe 'GET #projects with project ID' do
+ before do
+ get(:projects, project_id: project.id)
+ end
- expect(body.first["username"]).to eq non_member.username
- end
+ let(:body) { JSON.parse(response.body) }
- it 'rejects non existent user ids' do
- get(:users, author_id: 99999)
+ it do
+ expect(body).to be_kind_of(Array)
+ expect(body.size).to eq 1 # 'No project'
- expect(body.collect { |u| u['id'] }).not_to include(99999)
+ expect(body.first['id']).to eq 0
+ end
+ end
end
end
end
diff --git a/spec/controllers/commit_controller_spec.rb b/spec/controllers/commit_controller_spec.rb
deleted file mode 100644
index a3a3309e15e..00000000000
--- a/spec/controllers/commit_controller_spec.rb
+++ /dev/null
@@ -1,246 +0,0 @@
-require 'spec_helper'
-
-describe Projects::CommitController do
- let(:project) { create(:project) }
- let(:user) { create(:user) }
- let(:commit) { project.commit("master") }
- let(:master_pickable_sha) { '7d3b0f7cff5f37573aea97cebfd5692ea1689924' }
- let(:master_pickable_commit) { project.commit(master_pickable_sha) }
-
- before do
- sign_in(user)
- project.team << [user, :master]
- end
-
- describe "#show" do
- shared_examples "export as" do |format|
- it "should generally work" do
- get(:show,
- namespace_id: project.namespace.to_param,
- project_id: project.to_param,
- id: commit.id,
- format: format)
-
- expect(response).to be_success
- end
-
- it "should generate it" do
- expect_any_instance_of(Commit).to receive(:"to_#{format}")
-
- get(:show,
- namespace_id: project.namespace.to_param,
- project_id: project.to_param,
- id: commit.id, format: format)
- end
-
- it "should render it" do
- get(:show,
- namespace_id: project.namespace.to_param,
- project_id: project.to_param,
- id: commit.id, format: format)
-
- expect(response.body).to eq(commit.send(:"to_#{format}"))
- end
-
- it "should not escape Html" do
- allow_any_instance_of(Commit).to receive(:"to_#{format}").
- and_return('HTML entities &<>" ')
-
- get(:show,
- namespace_id: project.namespace.to_param,
- project_id: project.to_param,
- id: commit.id, format: format)
-
- expect(response.body).not_to include('&amp;')
- expect(response.body).not_to include('&gt;')
- expect(response.body).not_to include('&lt;')
- expect(response.body).not_to include('&quot;')
- end
- end
-
- describe "as diff" do
- include_examples "export as", :diff
- let(:format) { :diff }
-
- it "should really only be a git diff" do
- get(:show,
- namespace_id: project.namespace.to_param,
- project_id: project.to_param,
- id: commit.id,
- format: format)
-
- expect(response.body).to start_with("diff --git")
- end
-
- it "should really only be a git diff without whitespace changes" do
- get(:show,
- namespace_id: project.namespace.to_param,
- project_id: project.to_param,
- id: '66eceea0db202bb39c4e445e8ca28689645366c5',
- # id: commit.id,
- format: format,
- w: 1)
-
- expect(response.body).to start_with("diff --git")
- # without whitespace option, there are more than 2 diff_splits
- diff_splits = assigns(:diffs).first.diff.split("\n")
- expect(diff_splits.length).to be <= 2
- end
- end
-
- describe "as patch" do
- include_examples "export as", :patch
- let(:format) { :patch }
-
- it "should really be a git email patch" do
- get(:show,
- namespace_id: project.namespace.to_param,
- project_id: project.to_param,
- id: commit.id,
- format: format)
-
- expect(response.body).to start_with("From #{commit.id}")
- end
-
- it "should contain a git diff" do
- get(:show,
- namespace_id: project.namespace.to_param,
- project_id: project.to_param,
- id: commit.id,
- format: format)
-
- expect(response.body).to match(/^diff --git/)
- end
- end
-
- context 'commit that removes a submodule' do
- render_views
-
- let(:fork_project) { create(:forked_project_with_submodules) }
- let(:commit) { fork_project.commit('remove-submodule') }
-
- before do
- fork_project.team << [user, :master]
- end
-
- it 'renders it' do
- get(:show,
- namespace_id: fork_project.namespace.to_param,
- project_id: fork_project.to_param,
- id: commit.id)
-
- expect(response).to be_success
- end
- end
- end
-
- describe "#branches" do
- it "contains branch and tags information" do
- get(:branches,
- namespace_id: project.namespace.to_param,
- project_id: project.to_param,
- id: commit.id)
-
- expect(assigns(:branches)).to include("master", "feature_conflict")
- expect(assigns(:tags)).to include("v1.1.0")
- end
- end
-
- describe '#revert' do
- context 'when target branch is not provided' do
- it 'should render the 404 page' do
- post(:revert,
- namespace_id: project.namespace.to_param,
- project_id: project.to_param,
- id: commit.id)
-
- expect(response).not_to be_success
- expect(response).to have_http_status(404)
- end
- end
-
- context 'when the revert was successful' do
- it 'should redirect to the commits page' do
- post(:revert,
- namespace_id: project.namespace.to_param,
- project_id: project.to_param,
- target_branch: 'master',
- id: commit.id)
-
- expect(response).to redirect_to namespace_project_commits_path(project.namespace, project, 'master')
- expect(flash[:notice]).to eq('The commit has been successfully reverted.')
- end
- end
-
- context 'when the revert failed' do
- before do
- post(:revert,
- namespace_id: project.namespace.to_param,
- project_id: project.to_param,
- target_branch: 'master',
- id: commit.id)
- end
-
- it 'should redirect to the commit page' do
- # Reverting a commit that has been already reverted.
- post(:revert,
- namespace_id: project.namespace.to_param,
- project_id: project.to_param,
- target_branch: 'master',
- id: commit.id)
-
- expect(response).to redirect_to namespace_project_commit_path(project.namespace, project, commit.id)
- expect(flash[:alert]).to match('Sorry, we cannot revert this commit automatically.')
- end
- end
- end
-
- describe '#cherry_pick' do
- context 'when target branch is not provided' do
- it 'should render the 404 page' do
- post(:cherry_pick,
- namespace_id: project.namespace.to_param,
- project_id: project.to_param,
- id: master_pickable_commit.id)
-
- expect(response).not_to be_success
- expect(response).to have_http_status(404)
- end
- end
-
- context 'when the cherry-pick was successful' do
- it 'should redirect to the commits page' do
- post(:cherry_pick,
- namespace_id: project.namespace.to_param,
- project_id: project.to_param,
- target_branch: 'master',
- id: master_pickable_commit.id)
-
- expect(response).to redirect_to namespace_project_commits_path(project.namespace, project, 'master')
- expect(flash[:notice]).to eq('The commit has been successfully cherry-picked.')
- end
- end
-
- context 'when the cherry_pick failed' do
- before do
- post(:cherry_pick,
- namespace_id: project.namespace.to_param,
- project_id: project.to_param,
- target_branch: 'master',
- id: master_pickable_commit.id)
- end
-
- it 'should redirect to the commit page' do
- # Cherry-picking a commit that has been already cherry-picked.
- post(:cherry_pick,
- namespace_id: project.namespace.to_param,
- project_id: project.to_param,
- target_branch: 'master',
- id: master_pickable_commit.id)
-
- expect(response).to redirect_to namespace_project_commit_path(project.namespace, project, master_pickable_commit.id)
- expect(flash[:alert]).to match('Sorry, we cannot cherry-pick this commit automatically.')
- end
- end
- end
-end
diff --git a/spec/controllers/groups/avatars_controller_spec.rb b/spec/controllers/groups/avatars_controller_spec.rb
index 91d639218e5..506aeee7d2a 100644
--- a/spec/controllers/groups/avatars_controller_spec.rb
+++ b/spec/controllers/groups/avatars_controller_spec.rb
@@ -9,7 +9,7 @@ describe Groups::AvatarsController do
sign_in(user)
end
- it 'destroy should remove avatar from DB' do
+ it 'removes avatar from DB calling destroy' do
delete :destroy, group_id: group.path
@group = assigns(:group)
expect(@group.avatar.present?).to be_falsey
diff --git a/spec/controllers/groups/milestones_controller_spec.rb b/spec/controllers/groups/milestones_controller_spec.rb
index b0793cb1655..8c52f615b8b 100644
--- a/spec/controllers/groups/milestones_controller_spec.rb
+++ b/spec/controllers/groups/milestones_controller_spec.rb
@@ -15,7 +15,7 @@ describe Groups::MilestonesController do
end
describe "#create" do
- it "should create group milestone with Chinese title" do
+ it "creates group milestone with Chinese title" do
post :create,
group_id: group.id,
milestone: { project_ids: [project.id, project2.id], title: title }
diff --git a/spec/controllers/groups_controller_spec.rb b/spec/controllers/groups_controller_spec.rb
index cd98fecd0c7..a763e2c5ba8 100644
--- a/spec/controllers/groups_controller_spec.rb
+++ b/spec/controllers/groups_controller_spec.rb
@@ -75,4 +75,34 @@ describe GroupsController do
end
end
end
+
+ describe 'DELETE #destroy' do
+ context 'as another user' do
+ it 'returns 404' do
+ sign_in(create(:user))
+
+ delete :destroy, id: group.path
+
+ expect(response.status).to eq(404)
+ end
+ end
+
+ context 'as the group owner' do
+ before do
+ sign_in(user)
+ end
+
+ it 'schedules a group destroy' do
+ Sidekiq::Testing.fake! do
+ expect { delete :destroy, id: group.path }.to change(GroupDestroyWorker.jobs, :size).by(1)
+ end
+ end
+
+ it 'redirects to the root path' do
+ delete :destroy, id: group.path
+
+ expect(response).to redirect_to(root_path)
+ end
+ end
+ end
end
diff --git a/spec/controllers/help_controller_spec.rb b/spec/controllers/help_controller_spec.rb
index 1f7fd517342..33c75e7584f 100644
--- a/spec/controllers/help_controller_spec.rb
+++ b/spec/controllers/help_controller_spec.rb
@@ -11,7 +11,7 @@ describe HelpController do
context 'for Markdown formats' do
context 'when requested file exists' do
before do
- get :show, category: 'ssh', file: 'README', format: :md
+ get :show, path: 'ssh/README', format: :md
end
it 'assigns to @markdown' do
@@ -26,7 +26,7 @@ describe HelpController do
context 'when requested file is missing' do
it 'renders not found' do
- get :show, category: 'foo', file: 'bar', format: :md
+ get :show, path: 'foo/bar', format: :md
expect(response).to be_not_found
end
end
@@ -36,8 +36,7 @@ describe HelpController do
context 'when requested file exists' do
it 'renders the raw file' do
get :show,
- category: 'workflow/protected_branches',
- file: 'protected_branches1',
+ path: 'user/project/img/labels_filter',
format: :png
expect(response).to be_success
expect(response.content_type).to eq 'image/png'
@@ -48,8 +47,7 @@ describe HelpController do
context 'when requested file is missing' do
it 'renders not found' do
get :show,
- category: 'foo',
- file: 'bar',
+ path: 'foo/bar',
format: :png
expect(response).to be_not_found
end
@@ -59,11 +57,19 @@ describe HelpController do
context 'for other formats' do
it 'always renders not found' do
get :show,
- category: 'ssh',
- file: 'README',
+ path: 'ssh/README',
format: :foo
expect(response).to be_not_found
end
end
end
+
+ describe 'GET #ui' do
+ context 'for UI Development Kit' do
+ it 'renders found' do
+ get :ui
+ expect(response).to have_http_status(200)
+ end
+ end
+ end
end
diff --git a/spec/controllers/import/gitorious_controller_spec.rb b/spec/controllers/import/gitorious_controller_spec.rb
deleted file mode 100644
index 4ae2b78e11c..00000000000
--- a/spec/controllers/import/gitorious_controller_spec.rb
+++ /dev/null
@@ -1,69 +0,0 @@
-require 'spec_helper'
-
-describe Import::GitoriousController do
- include ImportSpecHelper
-
- let(:user) { create(:user) }
-
- before do
- sign_in(user)
- end
-
- describe "GET new" do
- it "redirects to import endpoint on gitorious.org" do
- get :new
-
- expect(controller).to redirect_to("https://gitorious.org/gitlab-import?callback_url=http://test.host/import/gitorious/callback")
- end
- end
-
- describe "GET callback" do
- it "stores repo list in session" do
- get :callback, repos: 'foo/bar,baz/qux'
-
- expect(session[:gitorious_repos]).to eq('foo/bar,baz/qux')
- end
- end
-
- describe "GET status" do
- before do
- @repo = OpenStruct.new(full_name: 'asd/vim')
- end
-
- it "assigns variables" do
- @project = create(:project, import_type: 'gitorious', creator_id: user.id)
- stub_client(repos: [@repo])
-
- get :status
-
- expect(assigns(:already_added_projects)).to eq([@project])
- expect(assigns(:repos)).to eq([@repo])
- end
-
- it "does not show already added project" do
- @project = create(:project, import_type: 'gitorious', creator_id: user.id, import_source: 'asd/vim')
- stub_client(repos: [@repo])
-
- get :status
-
- expect(assigns(:already_added_projects)).to eq([@project])
- expect(assigns(:repos)).to eq([])
- end
- end
-
- describe "POST create" do
- before do
- @repo = Gitlab::GitoriousImport::Repository.new('asd/vim')
- end
-
- it "takes already existing namespace" do
- namespace = create(:namespace, name: "asd", owner: user)
- expect(Gitlab::GitoriousImport::ProjectCreator).
- to receive(:new).with(@repo, namespace, user).
- and_return(double(execute: true))
- stub_client(repo: @repo)
-
- post :create, format: :js
- end
- end
-end
diff --git a/spec/controllers/profiles/avatars_controller_spec.rb b/spec/controllers/profiles/avatars_controller_spec.rb
index ad5855df0a4..4fa0462ccdf 100644
--- a/spec/controllers/profiles/avatars_controller_spec.rb
+++ b/spec/controllers/profiles/avatars_controller_spec.rb
@@ -8,7 +8,7 @@ describe Profiles::AvatarsController do
controller.instance_variable_set(:@user, user)
end
- it 'destroy should remove avatar from DB' do
+ it 'removes avatar from DB by calling destroy' do
delete :destroy
@user = assigns(:user)
expect(@user.avatar.present?).to be_falsey
diff --git a/spec/controllers/profiles/keys_controller_spec.rb b/spec/controllers/profiles/keys_controller_spec.rb
index 3a82083717f..6bcfae0fc13 100644
--- a/spec/controllers/profiles/keys_controller_spec.rb
+++ b/spec/controllers/profiles/keys_controller_spec.rb
@@ -6,7 +6,7 @@ describe Profiles::KeysController do
describe '#new' do
before { sign_in(user) }
- it 'redirect to #index' do
+ it 'redirects to #index' do
get :new
expect(response).to redirect_to(profile_keys_path)
@@ -15,7 +15,7 @@ describe Profiles::KeysController do
describe "#get_keys" do
describe "non existant user" do
- it "should generally not work" do
+ it "does not generally work" do
get :get_keys, username: 'not-existent'
expect(response).not_to be_success
@@ -23,19 +23,19 @@ describe Profiles::KeysController do
end
describe "user with no keys" do
- it "should generally work" do
+ it "does generally work" do
get :get_keys, username: user.username
expect(response).to be_success
end
- it "should render all keys separated with a new line" do
+ it "renders all keys separated with a new line" do
get :get_keys, username: user.username
expect(response.body).to eq("")
end
- it "should respond with text/plain content type" do
+ it "responds with text/plain content type" do
get :get_keys, username: user.username
expect(response.content_type).to eq("text/plain")
end
@@ -47,13 +47,13 @@ describe Profiles::KeysController do
user.keys << create(:another_key)
end
- it "should generally work" do
+ it "does generally work" do
get :get_keys, username: user.username
expect(response).to be_success
end
- it "should render all keys separated with a new line" do
+ it "renders all keys separated with a new line" do
get :get_keys, username: user.username
expect(response.body).not_to eq("")
@@ -65,13 +65,13 @@ describe Profiles::KeysController do
expect(response.body).to match(/AQDmTillFzNTrrGgwaCKaSj/)
end
- it "should not render the comment of the key" do
+ it "does not render the comment of the key" do
get :get_keys, username: user.username
expect(response.body).not_to match(/dummy@gitlab.com/)
end
- it "should respond with text/plain content type" do
+ it "responds with text/plain content type" do
get :get_keys, username: user.username
expect(response.content_type).to eq("text/plain")
end
diff --git a/spec/controllers/projects/avatars_controller_spec.rb b/spec/controllers/projects/avatars_controller_spec.rb
index 4d724ca9ed0..f5ea097af8b 100644
--- a/spec/controllers/projects/avatars_controller_spec.rb
+++ b/spec/controllers/projects/avatars_controller_spec.rb
@@ -10,7 +10,7 @@ describe Projects::AvatarsController do
controller.instance_variable_set(:@project, project)
end
- it 'destroy should remove avatar from DB' do
+ it 'removes avatar from DB by calling destroy' do
delete :destroy, namespace_id: project.namespace.id, project_id: project.id
expect(project.avatar.present?).to be_falsey
expect(project).to be_valid
diff --git a/spec/controllers/projects/boards/issues_controller_spec.rb b/spec/controllers/projects/boards/issues_controller_spec.rb
new file mode 100644
index 00000000000..d0ad5e26dbd
--- /dev/null
+++ b/spec/controllers/projects/boards/issues_controller_spec.rb
@@ -0,0 +1,120 @@
+require 'spec_helper'
+
+describe Projects::Boards::IssuesController do
+ let(:project) { create(:project_with_board) }
+ let(:user) { create(:user) }
+
+ let(:planning) { create(:label, project: project, name: 'Planning') }
+ let(:development) { create(:label, project: project, name: 'Development') }
+
+ let!(:list1) { create(:list, board: project.board, label: planning, position: 0) }
+ let!(:list2) { create(:list, board: project.board, label: development, position: 1) }
+
+ before do
+ project.team << [user, :master]
+ end
+
+ describe 'GET index' do
+ context 'with valid list id' do
+ it 'returns issues that have the list label applied' do
+ johndoe = create(:user, avatar: fixture_file_upload(File.join(Rails.root, 'spec/fixtures/dk.png')))
+ create(:labeled_issue, project: project, labels: [planning])
+ create(:labeled_issue, project: project, labels: [development])
+ create(:labeled_issue, project: project, labels: [development], assignee: johndoe)
+
+ list_issues user: user, list_id: list2
+
+ parsed_response = JSON.parse(response.body)
+
+ expect(response).to match_response_schema('issues')
+ expect(parsed_response.length).to eq 2
+ end
+ end
+
+ context 'with invalid list id' do
+ it 'returns a not found 404 response' do
+ list_issues user: user, list_id: 999
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ context 'with unauthorized user' do
+ before do
+ allow(Ability.abilities).to receive(:allowed?).with(user, :read_project, project).and_return(true)
+ allow(Ability.abilities).to receive(:allowed?).with(user, :read_issue, project).and_return(false)
+ end
+
+ it 'returns a successful 403 response' do
+ list_issues user: user, list_id: list2
+
+ expect(response).to have_http_status(403)
+ end
+ end
+
+ def list_issues(user:, list_id:)
+ sign_in(user)
+
+ get :index, namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ list_id: list_id.to_param
+ end
+ end
+
+ describe 'PATCH update' do
+ let(:issue) { create(:labeled_issue, project: project, labels: [planning]) }
+
+ context 'with valid params' do
+ it 'returns a successful 200 response' do
+ move user: user, issue: issue, from_list_id: list1.id, to_list_id: list2.id
+
+ expect(response).to have_http_status(200)
+ end
+
+ it 'moves issue to the desired list' do
+ move user: user, issue: issue, from_list_id: list1.id, to_list_id: list2.id
+
+ expect(issue.reload.labels).to contain_exactly(development)
+ end
+ end
+
+ context 'with invalid params' do
+ it 'returns a unprocessable entity 422 response for invalid lists' do
+ move user: user, issue: issue, from_list_id: nil, to_list_id: nil
+
+ expect(response).to have_http_status(422)
+ end
+
+ it 'returns a not found 404 response for invalid issue id' do
+ move user: user, issue: 999, from_list_id: list1.id, to_list_id: list2.id
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ context 'with unauthorized user' do
+ let(:guest) { create(:user) }
+
+ before do
+ project.team << [guest, :guest]
+ end
+
+ it 'returns a successful 403 response' do
+ move user: guest, issue: issue, from_list_id: list1.id, to_list_id: list2.id
+
+ expect(response).to have_http_status(403)
+ end
+ end
+
+ def move(user:, issue:, from_list_id:, to_list_id:)
+ sign_in(user)
+
+ patch :update, namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ id: issue.to_param,
+ from_list_id: from_list_id,
+ to_list_id: to_list_id,
+ format: :json
+ end
+ end
+end
diff --git a/spec/controllers/projects/boards/lists_controller_spec.rb b/spec/controllers/projects/boards/lists_controller_spec.rb
new file mode 100644
index 00000000000..9496636e3cc
--- /dev/null
+++ b/spec/controllers/projects/boards/lists_controller_spec.rb
@@ -0,0 +1,241 @@
+require 'spec_helper'
+
+describe Projects::Boards::ListsController do
+ let(:project) { create(:project_with_board) }
+ let(:board) { project.board }
+ let(:user) { create(:user) }
+ let(:guest) { create(:user) }
+
+ before do
+ project.team << [user, :master]
+ project.team << [guest, :guest]
+ end
+
+ describe 'GET index' do
+ it 'returns a successful 200 response' do
+ read_board_list user: user
+
+ expect(response).to have_http_status(200)
+ expect(response.content_type).to eq 'application/json'
+ end
+
+ it 'returns a list of board lists' do
+ board = project.create_board
+ create(:backlog_list, board: board)
+ create(:list, board: board)
+ create(:done_list, board: board)
+
+ read_board_list user: user
+
+ parsed_response = JSON.parse(response.body)
+
+ expect(response).to match_response_schema('lists')
+ expect(parsed_response.length).to eq 3
+ end
+
+ context 'with unauthorized user' do
+ before do
+ allow(Ability.abilities).to receive(:allowed?).with(user, :read_project, project).and_return(true)
+ allow(Ability.abilities).to receive(:allowed?).with(user, :read_list, project).and_return(false)
+ end
+
+ it 'returns a successful 403 response' do
+ read_board_list user: user
+
+ expect(response).to have_http_status(403)
+ end
+ end
+
+ def read_board_list(user:)
+ sign_in(user)
+
+ get :index, namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ format: :json
+ end
+ end
+
+ describe 'POST create' do
+ let(:label) { create(:label, project: project, name: 'Development') }
+
+ context 'with valid params' do
+ it 'returns a successful 200 response' do
+ create_board_list user: user, label_id: label.id
+
+ expect(response).to have_http_status(200)
+ end
+
+ it 'returns the created list' do
+ create_board_list user: user, label_id: label.id
+
+ expect(response).to match_response_schema('list')
+ end
+ end
+
+ context 'with invalid params' do
+ it 'returns an error' do
+ create_board_list user: user, label_id: nil
+
+ parsed_response = JSON.parse(response.body)
+
+ expect(parsed_response['label']).to contain_exactly "can't be blank"
+ expect(response).to have_http_status(422)
+ end
+ end
+
+ context 'with unauthorized user' do
+ let(:label) { create(:label, project: project, name: 'Development') }
+
+ it 'returns a successful 403 response' do
+ create_board_list user: guest, label_id: label.id
+
+ expect(response).to have_http_status(403)
+ end
+ end
+
+ def create_board_list(user:, label_id:)
+ sign_in(user)
+
+ post :create, namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ list: { label_id: label_id },
+ format: :json
+ end
+ end
+
+ describe 'PATCH update' do
+ let!(:planning) { create(:list, board: board, position: 0) }
+ let!(:development) { create(:list, board: board, position: 1) }
+
+ context 'with valid position' do
+ it 'returns a successful 200 response' do
+ move user: user, list: planning, position: 1
+
+ expect(response).to have_http_status(200)
+ end
+
+ it 'moves the list to the desired position' do
+ move user: user, list: planning, position: 1
+
+ expect(planning.reload.position).to eq 1
+ end
+ end
+
+ context 'with invalid position' do
+ it 'returns a unprocessable entity 422 response' do
+ move user: user, list: planning, position: 6
+
+ expect(response).to have_http_status(422)
+ end
+ end
+
+ context 'with invalid list id' do
+ it 'returns a not found 404 response' do
+ move user: user, list: 999, position: 1
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ context 'with unauthorized user' do
+ it 'returns a successful 403 response' do
+ move user: guest, list: planning, position: 6
+
+ expect(response).to have_http_status(403)
+ end
+ end
+
+ def move(user:, list:, position:)
+ sign_in(user)
+
+ patch :update, namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ id: list.to_param,
+ list: { position: position },
+ format: :json
+ end
+ end
+
+ describe 'DELETE destroy' do
+ let!(:planning) { create(:list, board: board, position: 0) }
+
+ context 'with valid list id' do
+ it 'returns a successful 200 response' do
+ remove_board_list user: user, list: planning
+
+ expect(response).to have_http_status(200)
+ end
+
+ it 'removes list from board' do
+ expect { remove_board_list user: user, list: planning }.to change(board.lists, :size).by(-1)
+ end
+ end
+
+ context 'with invalid list id' do
+ it 'returns a not found 404 response' do
+ remove_board_list user: user, list: 999
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ context 'with unauthorized user' do
+ it 'returns a successful 403 response' do
+ remove_board_list user: guest, list: planning
+
+ expect(response).to have_http_status(403)
+ end
+ end
+
+ def remove_board_list(user:, list:)
+ sign_in(user)
+
+ delete :destroy, namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ id: list.to_param,
+ format: :json
+ end
+ end
+
+ describe 'POST generate' do
+ context 'when board lists is empty' do
+ it 'returns a successful 200 response' do
+ generate_default_board_lists user: user
+
+ expect(response).to have_http_status(200)
+ end
+
+ it 'returns the defaults lists' do
+ generate_default_board_lists user: user
+
+ expect(response).to match_response_schema('lists')
+ end
+ end
+
+ context 'when board lists is not empty' do
+ it 'returns a unprocessable entity 422 response' do
+ create(:list, board: board)
+
+ generate_default_board_lists user: user
+
+ expect(response).to have_http_status(422)
+ end
+ end
+
+ context 'with unauthorized user' do
+ it 'returns a successful 403 response' do
+ generate_default_board_lists user: guest
+
+ expect(response).to have_http_status(403)
+ end
+ end
+
+ def generate_default_board_lists(user:)
+ sign_in(user)
+
+ post :generate, namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ format: :json
+ end
+ end
+end
diff --git a/spec/controllers/projects/boards_controller_spec.rb b/spec/controllers/projects/boards_controller_spec.rb
new file mode 100644
index 00000000000..75a6d39e82c
--- /dev/null
+++ b/spec/controllers/projects/boards_controller_spec.rb
@@ -0,0 +1,43 @@
+require 'spec_helper'
+
+describe Projects::BoardsController do
+ let(:project) { create(:empty_project) }
+ let(:user) { create(:user) }
+
+ before do
+ project.team << [user, :master]
+ sign_in(user)
+ end
+
+ describe 'GET show' do
+ it 'creates a new board when project does not have one' do
+ expect { read_board }.to change(Board, :count).by(1)
+ end
+
+ it 'renders HTML template' do
+ read_board
+
+ expect(response).to render_template :show
+ expect(response.content_type).to eq 'text/html'
+ end
+
+ context 'with unauthorized user' do
+ before do
+ allow(Ability.abilities).to receive(:allowed?).with(user, :read_project, project).and_return(true)
+ allow(Ability.abilities).to receive(:allowed?).with(user, :read_board, project).and_return(false)
+ end
+
+ it 'returns a successful 404 response' do
+ read_board
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ def read_board(format: :html)
+ get :show, namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ format: format
+ end
+ end
+end
diff --git a/spec/controllers/projects/commit_controller_spec.rb b/spec/controllers/projects/commit_controller_spec.rb
index 6e3db10e451..7e440193d7b 100644
--- a/spec/controllers/projects/commit_controller_spec.rb
+++ b/spec/controllers/projects/commit_controller_spec.rb
@@ -1,21 +1,32 @@
-require 'rails_helper'
+require 'spec_helper'
describe Projects::CommitController do
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+ let(:commit) { project.commit("master") }
+ let(:master_pickable_sha) { '7d3b0f7cff5f37573aea97cebfd5692ea1689924' }
+ let(:master_pickable_commit) { project.commit(master_pickable_sha) }
+
+ before do
+ sign_in(user)
+ project.team << [user, :master]
+ end
+
describe 'GET show' do
render_views
- let(:project) { create(:project) }
-
- before do
- user = create(:user)
- project.team << [user, :master]
+ def go(extra_params = {})
+ params = {
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param
+ }
- sign_in(user)
+ get :show, params.merge(extra_params)
end
context 'with valid id' do
it 'responds with 200' do
- go id: project.commit.id
+ go(id: commit.id)
expect(response).to be_ok
end
@@ -23,27 +34,275 @@ describe Projects::CommitController do
context 'with invalid id' do
it 'responds with 404' do
- go id: project.commit.id.reverse
+ go(id: commit.id.reverse)
expect(response).to be_not_found
end
end
it 'handles binary files' do
- get(:show,
+ go(id: TestEnv::BRANCH_SHA['binary-encoding'], format: 'html')
+
+ expect(response).to be_success
+ end
+
+ shared_examples "export as" do |format|
+ it "does generally work" do
+ go(id: commit.id, format: format)
+
+ expect(response).to be_success
+ end
+
+ it "generates it" do
+ expect_any_instance_of(Commit).to receive(:"to_#{format}")
+
+ go(id: commit.id, format: format)
+ end
+
+ it "renders it" do
+ go(id: commit.id, format: format)
+
+ expect(response.body).to eq(commit.send(:"to_#{format}"))
+ end
+
+ it "does not escape Html" do
+ allow_any_instance_of(Commit).to receive(:"to_#{format}").
+ and_return('HTML entities &<>" ')
+
+ go(id: commit.id, format: format)
+
+ expect(response.body).not_to include('&amp;')
+ expect(response.body).not_to include('&gt;')
+ expect(response.body).not_to include('&lt;')
+ expect(response.body).not_to include('&quot;')
+ end
+ end
+
+ describe "as diff" do
+ include_examples "export as", :diff
+ let(:format) { :diff }
+
+ it "should really only be a git diff" do
+ go(id: '66eceea0db202bb39c4e445e8ca28689645366c5', format: format)
+
+ expect(response.body).to start_with("diff --git")
+ end
+
+ it "is only be a git diff without whitespace changes" do
+ go(id: '66eceea0db202bb39c4e445e8ca28689645366c5', format: format, w: 1)
+
+ expect(response.body).to start_with("diff --git")
+
+ # without whitespace option, there are more than 2 diff_splits for other formats
+ diff_splits = assigns(:diffs).diff_files.first.diff.diff.split("\n")
+ expect(diff_splits.length).to be <= 2
+ end
+ end
+
+ describe "as patch" do
+ include_examples "export as", :patch
+ let(:format) { :patch }
+
+ it "is a git email patch" do
+ go(id: commit.id, format: format)
+
+ expect(response.body).to start_with("From #{commit.id}")
+ end
+
+ it "contains a git diff" do
+ go(id: commit.id, format: format)
+
+ expect(response.body).to match(/^diff --git/)
+ end
+ end
+
+ context 'commit that removes a submodule' do
+ render_views
+
+ let(:fork_project) { create(:forked_project_with_submodules, visibility_level: 20) }
+ let(:commit) { fork_project.commit('remove-submodule') }
+
+ it 'renders it' do
+ get(:show,
+ namespace_id: fork_project.namespace.to_param,
+ project_id: fork_project.to_param,
+ id: commit.id)
+
+ expect(response).to be_success
+ end
+ end
+ end
+
+ describe "GET branches" do
+ it "contains branch and tags information" do
+ get(:branches,
namespace_id: project.namespace.to_param,
project_id: project.to_param,
- id: TestEnv::BRANCH_SHA['binary-encoding'],
- format: "html")
+ id: commit.id)
- expect(response).to be_success
+ expect(assigns(:branches)).to include("master", "feature_conflict")
+ expect(assigns(:tags)).to include("v1.1.0")
+ end
+ end
+
+ describe 'POST revert' do
+ context 'when target branch is not provided' do
+ it 'renders the 404 page' do
+ post(:revert,
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ id: commit.id)
+
+ expect(response).not_to be_success
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ context 'when the revert was successful' do
+ it 'redirects to the commits page' do
+ post(:revert,
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ target_branch: 'master',
+ id: commit.id)
+
+ expect(response).to redirect_to namespace_project_commits_path(project.namespace, project, 'master')
+ expect(flash[:notice]).to eq('The commit has been successfully reverted.')
+ end
+ end
+
+ context 'when the revert failed' do
+ before do
+ post(:revert,
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ target_branch: 'master',
+ id: commit.id)
+ end
+
+ it 'redirects to the commit page' do
+ # Reverting a commit that has been already reverted.
+ post(:revert,
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ target_branch: 'master',
+ id: commit.id)
+
+ expect(response).to redirect_to namespace_project_commit_path(project.namespace, project, commit.id)
+ expect(flash[:alert]).to match('Sorry, we cannot revert this commit automatically.')
+ end
+ end
+ end
+
+ describe 'POST cherry_pick' do
+ context 'when target branch is not provided' do
+ it 'renders the 404 page' do
+ post(:cherry_pick,
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ id: master_pickable_commit.id)
+
+ expect(response).not_to be_success
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ context 'when the cherry-pick was successful' do
+ it 'redirects to the commits page' do
+ post(:cherry_pick,
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ target_branch: 'master',
+ id: master_pickable_commit.id)
+
+ expect(response).to redirect_to namespace_project_commits_path(project.namespace, project, 'master')
+ expect(flash[:notice]).to eq('The commit has been successfully cherry-picked.')
+ end
end
- def go(id:)
- get :show,
+ context 'when the cherry_pick failed' do
+ before do
+ post(:cherry_pick,
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ target_branch: 'master',
+ id: master_pickable_commit.id)
+ end
+
+ it 'redirects to the commit page' do
+ # Cherry-picking a commit that has been already cherry-picked.
+ post(:cherry_pick,
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ target_branch: 'master',
+ id: master_pickable_commit.id)
+
+ expect(response).to redirect_to namespace_project_commit_path(project.namespace, project, master_pickable_commit.id)
+ expect(flash[:alert]).to match('Sorry, we cannot cherry-pick this commit automatically.')
+ end
+ end
+ end
+
+ describe 'GET diff_for_path' do
+ def diff_for_path(extra_params = {})
+ params = {
namespace_id: project.namespace.to_param,
- project_id: project.to_param,
- id: id
+ project_id: project.to_param
+ }
+
+ get :diff_for_path, params.merge(extra_params)
+ end
+
+ let(:existing_path) { '.gitmodules' }
+
+ context 'when the commit exists' do
+ context 'when the user has access to the project' do
+ context 'when the path exists in the diff' do
+ it 'enables diff notes' do
+ diff_for_path(id: commit.id, old_path: existing_path, new_path: existing_path)
+
+ expect(assigns(:diff_notes_disabled)).to be_falsey
+ expect(assigns(:comments_target)).to eq(noteable_type: 'Commit',
+ commit_id: commit.id)
+ end
+
+ it 'only renders the diffs for the path given' do
+ expect(controller).to receive(:render_diff_for_path).and_wrap_original do |meth, diffs|
+ expect(diffs.diff_files.map(&:new_path)).to contain_exactly(existing_path)
+ meth.call(diffs)
+ end
+
+ diff_for_path(id: commit.id, old_path: existing_path, new_path: existing_path)
+ end
+ end
+
+ context 'when the path does not exist in the diff' do
+ before { diff_for_path(id: commit.id, old_path: existing_path.succ, new_path: existing_path.succ) }
+
+ it 'returns a 404' do
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
+
+ context 'when the user does not have access to the project' do
+ before do
+ project.team.truncate
+ diff_for_path(id: commit.id, old_path: existing_path, new_path: existing_path)
+ end
+
+ it 'returns a 404' do
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
+
+ context 'when the commit does not exist' do
+ before { diff_for_path(id: commit.id.succ, old_path: existing_path, new_path: existing_path) }
+
+ it 'returns a 404' do
+ expect(response).to have_http_status(404)
+ end
end
end
end
diff --git a/spec/controllers/projects/commits_controller_spec.rb b/spec/controllers/projects/commits_controller_spec.rb
index 7d8089c4bc6..2518a48e336 100644
--- a/spec/controllers/projects/commits_controller_spec.rb
+++ b/spec/controllers/projects/commits_controller_spec.rb
@@ -11,7 +11,7 @@ describe Projects::CommitsController do
describe "GET show" do
context "as atom feed" do
- it "should render as atom" do
+ it "renders as atom" do
get(:show,
namespace_id: project.namespace.to_param,
project_id: project.to_param,
diff --git a/spec/controllers/projects/compare_controller_spec.rb b/spec/controllers/projects/compare_controller_spec.rb
index 4018dac95a2..7a57801c437 100644
--- a/spec/controllers/projects/compare_controller_spec.rb
+++ b/spec/controllers/projects/compare_controller_spec.rb
@@ -11,7 +11,7 @@ describe Projects::CompareController do
project.team << [user, :master]
end
- it 'compare should show some diffs' do
+ it 'compare shows some diffs' do
get(:show,
namespace_id: project.namespace.to_param,
project_id: project.to_param,
@@ -19,11 +19,11 @@ describe Projects::CompareController do
to: ref_to)
expect(response).to be_success
- expect(assigns(:diffs).first).not_to be_nil
+ expect(assigns(:diffs).diff_files.first).not_to be_nil
expect(assigns(:commits).length).to be >= 1
end
- it 'compare should show some diffs with ignore whitespace change option' do
+ it 'compare shows some diffs with ignore whitespace change option' do
get(:show,
namespace_id: project.namespace.to_param,
project_id: project.to_param,
@@ -32,15 +32,16 @@ describe Projects::CompareController do
w: 1)
expect(response).to be_success
- expect(assigns(:diffs).first).not_to be_nil
+ diff_file = assigns(:diffs).diff_files.first
+ expect(diff_file).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_splits = diff_file.diff.diff.split("\n")
expect(diff_splits.length).to be <= 2
end
describe 'non-existent refs' do
- it 'invalid source ref' do
+ it 'uses invalid source ref' do
get(:show,
namespace_id: project.namespace.to_param,
project_id: project.to_param,
@@ -48,11 +49,11 @@ describe Projects::CompareController do
to: ref_to)
expect(response).to be_success
- expect(assigns(:diffs).to_a).to eq([])
+ expect(assigns(:diffs).diff_files.to_a).to eq([])
expect(assigns(:commits)).to eq([])
end
- it 'invalid target ref' do
+ it 'uses invalid target ref' do
get(:show,
namespace_id: project.namespace.to_param,
project_id: project.to_param,
@@ -64,4 +65,73 @@ describe Projects::CompareController do
expect(assigns(:commits)).to eq(nil)
end
end
+
+ describe 'GET diff_for_path' do
+ def diff_for_path(extra_params = {})
+ params = {
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param
+ }
+
+ get :diff_for_path, params.merge(extra_params)
+ end
+
+ let(:existing_path) { 'files/ruby/feature.rb' }
+
+ context 'when the from and to refs exist' do
+ context 'when the user has access to the project' do
+ context 'when the path exists in the diff' do
+ it 'disables diff notes' do
+ diff_for_path(from: ref_from, to: ref_to, old_path: existing_path, new_path: existing_path)
+
+ expect(assigns(:diff_notes_disabled)).to be_truthy
+ end
+
+ it 'only renders the diffs for the path given' do
+ expect(controller).to receive(:render_diff_for_path).and_wrap_original do |meth, diffs|
+ expect(diffs.diff_files.map(&:new_path)).to contain_exactly(existing_path)
+ meth.call(diffs)
+ end
+
+ diff_for_path(from: ref_from, to: ref_to, old_path: existing_path, new_path: existing_path)
+ end
+ end
+
+ context 'when the path does not exist in the diff' do
+ before { diff_for_path(from: ref_from, to: ref_to, old_path: existing_path.succ, new_path: existing_path.succ) }
+
+ it 'returns a 404' do
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
+
+ context 'when the user does not have access to the project' do
+ before do
+ project.team.truncate
+ diff_for_path(from: ref_from, to: ref_to, old_path: existing_path, new_path: existing_path)
+ end
+
+ it 'returns a 404' do
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
+
+ context 'when the from ref does not exist' do
+ before { diff_for_path(from: ref_from.succ, to: ref_to, old_path: existing_path, new_path: existing_path) }
+
+ it 'returns a 404' do
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ context 'when the to ref does not exist' do
+ before { diff_for_path(from: ref_from, to: ref_to.succ, old_path: existing_path, new_path: existing_path) }
+
+ it 'returns a 404' do
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
end
diff --git a/spec/controllers/projects/discussions_controller_spec.rb b/spec/controllers/projects/discussions_controller_spec.rb
new file mode 100644
index 00000000000..ff617fea847
--- /dev/null
+++ b/spec/controllers/projects/discussions_controller_spec.rb
@@ -0,0 +1,125 @@
+require 'spec_helper'
+
+describe Projects::DiscussionsController do
+ let(:user) { create(:user) }
+ let(:project) { create(:project) }
+ let(:merge_request) { create(:merge_request, source_project: project) }
+ let(:note) { create(:diff_note_on_merge_request, noteable: merge_request, project: project) }
+ let(:discussion) { note.discussion }
+
+ let(:request_params) do
+ {
+ namespace_id: project.namespace,
+ project_id: project,
+ merge_request_id: merge_request,
+ id: note.discussion_id
+ }
+ end
+
+ describe 'POST resolve' do
+ before do
+ sign_in user
+ end
+
+ context "when the user is not authorized to resolve the discussion" do
+ it "returns status 404" do
+ post :resolve, request_params
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ context "when the user is authorized to resolve the discussion" do
+ before do
+ project.team << [user, :developer]
+ end
+
+ context "when the discussion is not resolvable" do
+ before do
+ note.update(system: true)
+ end
+
+ it "returns status 404" do
+ post :resolve, request_params
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ context "when the discussion is resolvable" do
+ it "resolves the discussion" do
+ post :resolve, request_params
+
+ expect(note.reload.discussion.resolved?).to be true
+ expect(note.reload.discussion.resolved_by).to eq(user)
+ end
+
+ it "sends notifications if all discussions are resolved" do
+ expect_any_instance_of(MergeRequests::ResolvedDiscussionNotificationService).to receive(:execute).with(merge_request)
+
+ post :resolve, request_params
+ end
+
+ it "returns the name of the resolving user" do
+ post :resolve, request_params
+
+ expect(JSON.parse(response.body)["resolved_by"]).to eq(user.name)
+ end
+
+ it "returns status 200" do
+ post :resolve, request_params
+
+ expect(response).to have_http_status(200)
+ end
+ end
+ end
+ end
+
+ describe 'DELETE unresolve' do
+ before do
+ sign_in user
+
+ note.discussion.resolve!(user)
+ end
+
+ context "when the user is not authorized to resolve the discussion" do
+ it "returns status 404" do
+ delete :unresolve, request_params
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ context "when the user is authorized to resolve the discussion" do
+ before do
+ project.team << [user, :developer]
+ end
+
+ context "when the discussion is not resolvable" do
+ before do
+ note.update(system: true)
+ end
+
+ it "returns status 404" do
+ delete :unresolve, request_params
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ context "when the discussion is resolvable" do
+ it "unresolves the discussion" do
+ delete :unresolve, request_params
+
+ expect(note.reload.discussion.resolved?).to be false
+ end
+
+ it "returns status 200" do
+ delete :unresolve, request_params
+
+ expect(response).to have_http_status(200)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/controllers/projects/environments_controller_spec.rb b/spec/controllers/projects/environments_controller_spec.rb
new file mode 100644
index 00000000000..768105cae95
--- /dev/null
+++ b/spec/controllers/projects/environments_controller_spec.rb
@@ -0,0 +1,58 @@
+require 'spec_helper'
+
+describe Projects::EnvironmentsController do
+ let(:environment) { create(:environment) }
+ let(:project) { environment.project }
+ let(:user) { create(:user) }
+
+ before do
+ project.team << [user, :master]
+
+ sign_in(user)
+ end
+
+ describe 'GET show' do
+ context 'with valid id' do
+ it 'responds with a status code 200' do
+ get :show, environment_params
+
+ expect(response).to be_ok
+ end
+ end
+
+ context 'with invalid id' do
+ it 'responds with a status code 404' do
+ params = environment_params
+ params[:id] = 12345
+ get :show, params
+
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
+
+ describe 'GET edit' do
+ it 'responds with a status code 200' do
+ get :edit, environment_params
+
+ expect(response).to be_ok
+ end
+ end
+
+ describe 'PATCH #update' do
+ it 'responds with a 302' do
+ patch_params = environment_params.merge(environment: { external_url: 'https://git.gitlab.com' })
+ patch :update, patch_params
+
+ expect(response).to have_http_status(302)
+ end
+ end
+
+ def environment_params
+ {
+ namespace_id: project.namespace,
+ project_id: project,
+ id: environment.id
+ }
+ end
+end
diff --git a/spec/controllers/projects/forks_controller_spec.rb b/spec/controllers/projects/forks_controller_spec.rb
index f66bcb8099c..ac3469cb8a9 100644
--- a/spec/controllers/projects/forks_controller_spec.rb
+++ b/spec/controllers/projects/forks_controller_spec.rb
@@ -16,7 +16,7 @@ describe Projects::ForksController do
context 'when fork is public' do
before { forked_project.update_attribute(:visibility_level, Project::PUBLIC) }
- it 'should be visible for non logged in users' do
+ it 'is visible for non logged in users' do
get_forks
expect(assigns[:forks]).to be_present
@@ -28,7 +28,7 @@ describe Projects::ForksController do
forked_project.update_attributes(visibility_level: Project::PRIVATE, group: group)
end
- it 'should not be visible for non logged in users' do
+ it 'is not be visible for non logged in users' do
get_forks
expect(assigns[:forks]).to be_blank
@@ -38,7 +38,7 @@ describe Projects::ForksController do
before { sign_in(project.creator) }
context 'when user is not a Project member neither a group member' do
- it 'should not see the Project listed' do
+ it 'does not see the Project listed' do
get_forks
expect(assigns[:forks]).to be_blank
@@ -48,7 +48,7 @@ describe Projects::ForksController do
context 'when user is a member of the Project' do
before { forked_project.team << [project.creator, :developer] }
- it 'should see the project listed' do
+ it 'sees the project listed' do
get_forks
expect(assigns[:forks]).to be_present
@@ -58,7 +58,7 @@ describe Projects::ForksController do
context 'when user is a member of the Group' do
before { forked_project.group.add_developer(project.creator) }
- it 'should see the project listed' do
+ it 'sees the project listed' do
get_forks
expect(assigns[:forks]).to be_present
diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb
index 7cf09fa4a4a..16929767ddf 100644
--- a/spec/controllers/projects/issues_controller_spec.rb
+++ b/spec/controllers/projects/issues_controller_spec.rb
@@ -6,37 +6,65 @@ describe Projects::IssuesController do
let(:issue) { create(:issue, project: project) }
describe "GET #index" do
- before do
- sign_in(user)
- project.team << [user, :developer]
- end
+ context 'external issue tracker' do
+ it 'redirects to the external issue tracker' do
+ external = double(project_path: 'https://example.com/project')
+ allow(project).to receive(:external_issue_tracker).and_return(external)
+ controller.instance_variable_set(:@project, project)
- it "returns index" do
- get :index, namespace_id: project.namespace.path, project_id: project.path
+ get :index, namespace_id: project.namespace.path, project_id: project
- expect(response).to have_http_status(200)
+ expect(response).to redirect_to('https://example.com/project')
+ end
end
- it "return 301 if request path doesn't match project path" do
- get :index, namespace_id: project.namespace.path, project_id: project.path.upcase
+ context 'internal issue tracker' do
+ before do
+ sign_in(user)
+ project.team << [user, :developer]
+ end
- expect(response).to redirect_to(namespace_project_issues_path(project.namespace, project))
- end
+ it "returns index" do
+ get :index, namespace_id: project.namespace.path, project_id: project.path
- it "returns 404 when issues are disabled" do
- project.issues_enabled = false
- project.save
+ expect(response).to have_http_status(200)
+ end
+
+ it "returns 301 if request path doesn't match project path" do
+ get :index, namespace_id: project.namespace.path, project_id: project.path.upcase
+
+ expect(response).to redirect_to(namespace_project_issues_path(project.namespace, project))
+ end
- get :index, namespace_id: project.namespace.path, project_id: project.path
- expect(response).to have_http_status(404)
+ it "returns 404 when issues are disabled" do
+ project.issues_enabled = false
+ project.save
+
+ get :index, namespace_id: project.namespace.path, project_id: project.path
+ expect(response).to have_http_status(404)
+ end
+
+ it "returns 404 when external issue tracker is enabled" do
+ controller.instance_variable_set(:@project, project)
+ allow(project).to receive(:default_issues_tracker?).and_return(false)
+
+ get :index, namespace_id: project.namespace.path, project_id: project.path
+ expect(response).to have_http_status(404)
+ end
end
+ end
+
+ describe 'GET #new' do
+ context 'external issue tracker' do
+ it 'redirects to the external issue tracker' do
+ external = double(new_issue_path: 'https://example.com/issues/new')
+ allow(project).to receive(:external_issue_tracker).and_return(external)
+ controller.instance_variable_set(:@project, project)
- it "returns 404 when external issue tracker is enabled" do
- controller.instance_variable_set(:@project, project)
- allow(project).to receive(:default_issues_tracker?).and_return(false)
+ get :new, namespace_id: project.namespace.path, project_id: project
- get :index, namespace_id: project.namespace.path, project_id: project.path
- expect(response).to have_http_status(404)
+ expect(response).to redirect_to('https://example.com/issues/new')
+ end
end
end
@@ -91,21 +119,21 @@ describe Projects::IssuesController do
let!(:request_forgery_timing_attack) { create(:issue, :confidential, project: project, assignee: assignee) }
describe 'GET #index' do
- it 'should not list confidential issues for guests' do
+ it 'does not list confidential issues for guests' do
sign_out(:user)
get_issues
expect(assigns(:issues)).to eq [issue]
end
- it 'should not list confidential issues for non project members' do
+ it 'does not list confidential issues for non project members' do
sign_in(non_member)
get_issues
expect(assigns(:issues)).to eq [issue]
end
- it 'should not list confidential issues for project members with guest role' do
+ it 'does not list confidential issues for project members with guest role' do
sign_in(member)
project.team << [member, :guest]
@@ -114,7 +142,7 @@ describe Projects::IssuesController do
expect(assigns(:issues)).to eq [issue]
end
- it 'should list confidential issues for author' do
+ it 'lists confidential issues for author' do
sign_in(author)
get_issues
@@ -122,7 +150,7 @@ describe Projects::IssuesController do
expect(assigns(:issues)).not_to include request_forgery_timing_attack
end
- it 'should list confidential issues for assignee' do
+ it 'lists confidential issues for assignee' do
sign_in(assignee)
get_issues
@@ -130,7 +158,7 @@ describe Projects::IssuesController do
expect(assigns(:issues)).to include request_forgery_timing_attack
end
- it 'should list confidential issues for project members' do
+ it 'lists confidential issues for project members' do
sign_in(member)
project.team << [member, :developer]
@@ -140,7 +168,7 @@ describe Projects::IssuesController do
expect(assigns(:issues)).to include request_forgery_timing_attack
end
- it 'should list confidential issues for admin' do
+ it 'lists confidential issues for admin' do
sign_in(admin)
get_issues
@@ -243,6 +271,83 @@ describe Projects::IssuesController do
end
end
+ describe 'POST #create' do
+ context 'Akismet is enabled' do
+ before do
+ allow_any_instance_of(SpamService).to receive(:check_for_spam?).and_return(true)
+ allow_any_instance_of(AkismetService).to receive(:is_spam?).and_return(true)
+ end
+
+ def post_spam_issue
+ sign_in(user)
+ spam_project = create(:empty_project, :public)
+ post :create, {
+ namespace_id: spam_project.namespace.to_param,
+ project_id: spam_project.to_param,
+ issue: { title: 'Spam Title', description: 'Spam lives here' }
+ }
+ end
+
+ it 'rejects an issue recognized as spam' do
+ expect{ post_spam_issue }.not_to change(Issue, :count)
+ expect(response).to render_template(:new)
+ end
+
+ it 'creates a spam log' do
+ post_spam_issue
+ spam_logs = SpamLog.all
+ expect(spam_logs.count).to eq(1)
+ expect(spam_logs[0].title).to eq('Spam Title')
+ end
+ end
+
+ context 'user agent details are saved' do
+ before do
+ request.env['action_dispatch.remote_ip'] = '127.0.0.1'
+ end
+
+ def post_new_issue
+ sign_in(user)
+ project = create(:empty_project, :public)
+ post :create, {
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ issue: { title: 'Title', description: 'Description' }
+ }
+ end
+
+ it 'creates a user agent detail' do
+ expect{ post_new_issue }.to change(UserAgentDetail, :count).by(1)
+ end
+ end
+ end
+
+ describe 'POST #mark_as_spam' do
+ context 'properly submits to Akismet' do
+ before do
+ allow_any_instance_of(AkismetService).to receive_messages(submit_spam: true)
+ allow_any_instance_of(ApplicationSetting).to receive_messages(akismet_enabled: true)
+ end
+
+ def post_spam
+ admin = create(:admin)
+ create(:user_agent_detail, subject: issue)
+ project.team << [admin, :master]
+ sign_in(admin)
+ post :mark_as_spam, {
+ namespace_id: project.namespace.path,
+ project_id: project.path,
+ id: issue.iid
+ }
+ end
+
+ it 'updates issue' do
+ post_spam
+ expect(issue.submittable_as_spam?).to be_falsey
+ end
+ end
+ end
+
describe "DELETE #destroy" do
context "when the user is a developer" do
before { sign_in(user) }
diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb
index c4b57e77804..c64c2b075c5 100644
--- a/spec/controllers/projects/merge_requests_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests_controller_spec.rb
@@ -4,13 +4,18 @@ describe Projects::MergeRequestsController do
let(:project) { create(:project) }
let(:user) { create(:user) }
let(:merge_request) { create(:merge_request_with_diffs, target_project: project, source_project: project) }
+ let(:merge_request_with_conflicts) do
+ create(:merge_request, source_branch: 'conflict-resolvable', target_branch: 'conflict-start', source_project: project) do |mr|
+ mr.mark_as_unmergeable
+ end
+ end
before do
sign_in(user)
project.team << [user, :master]
end
- describe '#new' do
+ describe 'GET new' do
context 'merge request that removes a submodule' do
render_views
@@ -34,9 +39,9 @@ describe Projects::MergeRequestsController do
end
end
- describe "#show" do
+ describe "GET show" do
shared_examples "export merge as" do |format|
- it "should generally work" do
+ it "does generally work" do
get(:show,
namespace_id: project.namespace.to_param,
project_id: project.to_param,
@@ -46,7 +51,7 @@ describe Projects::MergeRequestsController do
expect(response).to be_success
end
- it "should generate it" do
+ it "generates it" do
expect_any_instance_of(MergeRequest).to receive(:"to_#{format}")
get(:show,
@@ -56,7 +61,7 @@ describe Projects::MergeRequestsController do
format: format)
end
- it "should render it" do
+ it "renders it" do
get(:show,
namespace_id: project.namespace.to_param,
project_id: project.to_param,
@@ -66,7 +71,7 @@ describe Projects::MergeRequestsController do
expect(response.body).to eq(merge_request.send(:"to_#{format}").to_s)
end
- it "should not escape Html" do
+ it "does not escape Html" do
allow_any_instance_of(MergeRequest).to receive(:"to_#{format}").
and_return('HTML entities &<>" ')
@@ -108,7 +113,7 @@ describe Projects::MergeRequestsController do
end
end
- describe 'GET #index' do
+ describe 'GET index' do
def get_merge_requests
get :index,
namespace_id: project.namespace.to_param,
@@ -118,7 +123,7 @@ describe Projects::MergeRequestsController do
context 'when filtering by opened state' do
context 'with opened merge requests' do
- it 'should list those merge requests' do
+ it 'lists those merge requests' do
get_merge_requests
expect(assigns(:merge_requests)).to include(merge_request)
@@ -131,7 +136,7 @@ describe Projects::MergeRequestsController do
merge_request.reopen!
end
- it 'should list those merge requests' do
+ it 'lists those merge requests' do
get_merge_requests
expect(assigns(:merge_requests)).to include(merge_request)
@@ -140,7 +145,7 @@ describe Projects::MergeRequestsController do
end
end
- describe 'PUT #update' do
+ describe 'PUT update' do
context 'there is no source project' do
let(:project) { create(:project) }
let(:fork_project) { create(:forked_project_with_submodules) }
@@ -168,7 +173,7 @@ describe Projects::MergeRequestsController do
end
end
- describe 'POST #merge' do
+ describe 'POST merge' do
let(:base_params) do
{
namespace_id: project.namespace.path,
@@ -266,7 +271,7 @@ describe Projects::MergeRequestsController do
end
end
- describe "DELETE #destroy" do
+ 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
@@ -290,96 +295,210 @@ describe Projects::MergeRequestsController do
end
describe 'GET diffs' do
- def go(format: 'html')
- get :diffs,
- namespace_id: project.namespace.to_param,
- project_id: project.to_param,
- id: merge_request.iid,
- format: format
+ def go(extra_params = {})
+ params = {
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ id: merge_request.iid
+ }
+
+ get :diffs, params.merge(extra_params)
end
- context 'as html' do
- it 'renders the diff template' do
- go
+ context 'with default params' do
+ context 'as html' do
+ before { go(format: 'html') }
- expect(response).to render_template('diffs')
+ it 'renders the diff template' do
+ expect(response).to render_template('diffs')
+ end
end
- end
- context 'as json' do
- it 'renders the diffs template to a string' do
- go format: 'json'
+ context 'as json' do
+ before { go(format: 'json') }
- expect(response).to render_template('projects/merge_requests/show/_diffs')
- expect(JSON.parse(response.body)).to have_key('html')
+ it 'renders the diffs template to a string' do
+ expect(response).to render_template('projects/merge_requests/show/_diffs')
+ expect(JSON.parse(response.body)).to have_key('html')
+ end
end
- end
- context 'with forked projects with submodules' do
- render_views
+ context 'with forked projects with submodules' do
+ render_views
- let(:project) { create(:project) }
- let(:fork_project) { create(:forked_project_with_submodules) }
- let(:merge_request) { create(:merge_request_with_diffs, source_project: fork_project, source_branch: 'add-submodule-version-bump', target_branch: 'master', target_project: project) }
+ let(:project) { create(:project) }
+ let(:fork_project) { create(:forked_project_with_submodules) }
+ let(:merge_request) { create(:merge_request_with_diffs, source_project: fork_project, source_branch: 'add-submodule-version-bump', target_branch: 'master', target_project: project) }
- before do
- fork_project.build_forked_project_link(forked_to_project_id: fork_project.id, forked_from_project_id: project.id)
- fork_project.save
- merge_request.reload
- end
-
- it 'renders' do
- go format: 'json'
+ before do
+ fork_project.build_forked_project_link(forked_to_project_id: fork_project.id, forked_from_project_id: project.id)
+ fork_project.save
+ merge_request.reload
+ go(format: 'json')
+ end
- expect(response).to be_success
- expect(response.body).to have_content('Subproject commit')
+ it 'renders' do
+ expect(response).to be_success
+ expect(response.body).to have_content('Subproject commit')
+ end
end
end
- end
- describe 'GET diffs with ignore_whitespace_change' do
- def go(format: 'html')
- get :diffs,
- namespace_id: project.namespace.to_param,
- project_id: project.to_param,
- id: merge_request.iid,
- format: format,
- w: 1
- end
+ context 'with ignore_whitespace_change' do
+ context 'as html' do
+ before { go(format: 'html', w: 1) }
- context 'as html' do
- it 'renders the diff template' do
- go
+ it 'renders the diff template' do
+ expect(response).to render_template('diffs')
+ end
+ end
+
+ context 'as json' do
+ before { go(format: 'json', w: 1) }
- expect(response).to render_template('diffs')
+ it 'renders the diffs template to a string' do
+ expect(response).to render_template('projects/merge_requests/show/_diffs')
+ expect(JSON.parse(response.body)).to have_key('html')
+ end
end
end
- context 'as json' do
- it 'renders the diffs template to a string' do
- go format: 'json'
+ context 'with view' do
+ before { go(view: 'parallel') }
- expect(response).to render_template('projects/merge_requests/show/_diffs')
- expect(JSON.parse(response.body)).to have_key('html')
+ it 'saves the preferred diff view in a cookie' do
+ expect(response.cookies['diff_view']).to eq('parallel')
end
end
end
- describe 'GET diffs with view' do
- def go(extra_params = {})
+ describe 'GET diff_for_path' do
+ def diff_for_path(extra_params = {})
params = {
namespace_id: project.namespace.to_param,
- project_id: project.to_param,
- id: merge_request.iid
+ project_id: project.to_param
}
- get :diffs, params.merge(extra_params)
+ get :diff_for_path, params.merge(extra_params)
+ end
+
+ context 'when an ID param is passed' do
+ let(:existing_path) { 'files/ruby/popen.rb' }
+
+ context 'when the merge request exists' do
+ context 'when the user can view the merge request' do
+ context 'when the path exists in the diff' do
+ it 'enables diff notes' do
+ diff_for_path(id: merge_request.iid, old_path: existing_path, new_path: existing_path)
+
+ expect(assigns(:diff_notes_disabled)).to be_falsey
+ expect(assigns(:comments_target)).to eq(noteable_type: 'MergeRequest',
+ noteable_id: merge_request.id)
+ end
+
+ it 'only renders the diffs for the path given' do
+ expect(controller).to receive(:render_diff_for_path).and_wrap_original do |meth, diffs|
+ expect(diffs.diff_files.map(&:new_path)).to contain_exactly(existing_path)
+ meth.call(diffs)
+ end
+
+ diff_for_path(id: merge_request.iid, old_path: existing_path, new_path: existing_path)
+ end
+ end
+
+ context 'when the path does not exist in the diff' do
+ before { diff_for_path(id: merge_request.iid, old_path: 'files/ruby/nopen.rb', new_path: 'files/ruby/nopen.rb') }
+
+ it 'returns a 404' do
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
+
+ context 'when the user cannot view the merge request' do
+ before do
+ project.team.truncate
+ diff_for_path(id: merge_request.iid, old_path: existing_path, new_path: existing_path)
+ end
+
+ it 'returns a 404' do
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
+
+ context 'when the merge request does not exist' do
+ before { diff_for_path(id: merge_request.iid.succ, old_path: existing_path, new_path: existing_path) }
+
+ it 'returns a 404' do
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ context 'when the merge request belongs to a different project' do
+ let(:other_project) { create(:empty_project) }
+
+ before do
+ other_project.team << [user, :master]
+ diff_for_path(id: merge_request.iid, old_path: existing_path, new_path: existing_path, project_id: other_project.to_param)
+ end
+
+ it 'returns a 404' do
+ expect(response).to have_http_status(404)
+ end
+ end
end
- it 'saves the preferred diff view in a cookie' do
- go view: 'parallel'
+ context 'when source and target params are passed' do
+ let(:existing_path) { 'files/ruby/feature.rb' }
+
+ context 'when both branches are in the same project' do
+ it 'disables diff notes' do
+ diff_for_path(old_path: existing_path, new_path: existing_path, merge_request: { source_branch: 'feature', target_branch: 'master' })
+
+ expect(assigns(:diff_notes_disabled)).to be_truthy
+ end
+
+ it 'only renders the diffs for the path given' do
+ expect(controller).to receive(:render_diff_for_path).and_wrap_original do |meth, diffs|
+ expect(diffs.diff_files.map(&:new_path)).to contain_exactly(existing_path)
+ meth.call(diffs)
+ end
+
+ diff_for_path(old_path: existing_path, new_path: existing_path, merge_request: { source_branch: 'feature', target_branch: 'master' })
+ end
+ end
+
+ context 'when the source branch is in a different project to the target' do
+ let(:other_project) { create(:project) }
+
+ before { other_project.team << [user, :master] }
+
+ context 'when the path exists in the diff' do
+ it 'disables diff notes' do
+ diff_for_path(old_path: existing_path, new_path: existing_path, merge_request: { source_project: other_project, source_branch: 'feature', target_branch: 'master' })
+
+ expect(assigns(:diff_notes_disabled)).to be_truthy
+ end
+
+ it 'only renders the diffs for the path given' do
+ expect(controller).to receive(:render_diff_for_path).and_wrap_original do |meth, diffs|
+ expect(diffs.diff_files.map(&:new_path)).to contain_exactly(existing_path)
+ meth.call(diffs)
+ end
+
+ diff_for_path(old_path: existing_path, new_path: existing_path, merge_request: { source_project: other_project, source_branch: 'feature', target_branch: 'master' })
+ end
+ end
+
+ context 'when the path does not exist in the diff' do
+ before { diff_for_path(old_path: 'files/ruby/nopen.rb', new_path: 'files/ruby/nopen.rb', merge_request: { source_project: other_project, source_branch: 'feature', target_branch: 'master' }) }
- expect(response.cookies['diff_view']).to eq('parallel')
+ it 'returns a 404' do
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
end
end
@@ -409,4 +528,135 @@ describe Projects::MergeRequestsController do
end
end
end
+
+ describe 'GET conflicts' do
+ let(:json_response) { JSON.parse(response.body) }
+
+ context 'when the conflicts cannot be resolved in the UI' do
+ before do
+ allow_any_instance_of(Gitlab::Conflict::Parser).
+ to receive(:parse).and_raise(Gitlab::Conflict::Parser::UnexpectedDelimiter)
+
+ get :conflicts,
+ namespace_id: merge_request_with_conflicts.project.namespace.to_param,
+ project_id: merge_request_with_conflicts.project.to_param,
+ id: merge_request_with_conflicts.iid,
+ format: 'json'
+ end
+
+ it 'returns a 200 status code' do
+ expect(response).to have_http_status(:ok)
+ end
+
+ it 'returns JSON with a message' do
+ expect(json_response.keys).to contain_exactly('message', 'type')
+ end
+ end
+
+ context 'with valid conflicts' do
+ before do
+ get :conflicts,
+ namespace_id: merge_request_with_conflicts.project.namespace.to_param,
+ project_id: merge_request_with_conflicts.project.to_param,
+ id: merge_request_with_conflicts.iid,
+ format: 'json'
+ end
+
+ it 'includes meta info about the MR' do
+ expect(json_response['commit_message']).to include('Merge branch')
+ expect(json_response['commit_sha']).to match(/\h{40}/)
+ expect(json_response['source_branch']).to eq(merge_request_with_conflicts.source_branch)
+ expect(json_response['target_branch']).to eq(merge_request_with_conflicts.target_branch)
+ end
+
+ it 'includes each file that has conflicts' do
+ filenames = json_response['files'].map { |file| file['new_path'] }
+
+ expect(filenames).to contain_exactly('files/ruby/popen.rb', 'files/ruby/regex.rb')
+ end
+
+ it 'splits files into sections with lines' do
+ json_response['files'].each do |file|
+ file['sections'].each do |section|
+ expect(section).to include('conflict', 'lines')
+
+ section['lines'].each do |line|
+ if section['conflict']
+ expect(line['type']).to be_in(['old', 'new'])
+ expect(line.values_at('old_line', 'new_line')).to contain_exactly(nil, a_kind_of(Integer))
+ else
+ if line['type'].nil?
+ expect(line['old_line']).not_to eq(nil)
+ expect(line['new_line']).not_to eq(nil)
+ else
+ expect(line['type']).to eq('match')
+ expect(line['old_line']).to eq(nil)
+ expect(line['new_line']).to eq(nil)
+ end
+ end
+ end
+ end
+ end
+ end
+
+ it 'has unique section IDs across files' do
+ section_ids = json_response['files'].flat_map do |file|
+ file['sections'].map { |section| section['id'] }.compact
+ end
+
+ expect(section_ids.uniq).to eq(section_ids)
+ end
+ end
+ end
+
+ context 'POST resolve_conflicts' do
+ let(:json_response) { JSON.parse(response.body) }
+ let!(:original_head_sha) { merge_request_with_conflicts.diff_head_sha }
+
+ def resolve_conflicts(sections)
+ post :resolve_conflicts,
+ namespace_id: merge_request_with_conflicts.project.namespace.to_param,
+ project_id: merge_request_with_conflicts.project.to_param,
+ id: merge_request_with_conflicts.iid,
+ format: 'json',
+ sections: sections,
+ commit_message: 'Commit message'
+ end
+
+ context 'with valid params' do
+ before do
+ resolve_conflicts('2f6fcd96b88b36ce98c38da085c795a27d92a3dd_14_14' => 'head',
+ '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_9_9' => 'head',
+ '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_21_21' => 'origin',
+ '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_49_49' => 'origin')
+ end
+
+ it 'creates a new commit on the branch' do
+ expect(original_head_sha).not_to eq(merge_request_with_conflicts.source_branch_head.sha)
+ expect(merge_request_with_conflicts.source_branch_head.message).to include('Commit message')
+ end
+
+ it 'returns an OK response' do
+ expect(response).to have_http_status(:ok)
+ end
+ end
+
+ context 'when sections are missing' do
+ before do
+ resolve_conflicts('2f6fcd96b88b36ce98c38da085c795a27d92a3dd_14_14' => 'head')
+ end
+
+ it 'returns a 400 error' do
+ expect(response).to have_http_status(:bad_request)
+ end
+
+ it 'has a message with the name of the first missing section' do
+ expect(json_response['message']).to include('6eb14e00385d2fb284765eb1cd8d420d33d63fc9_9_9')
+ end
+
+ it 'does not create a new commit' do
+ expect(original_head_sha).to eq(merge_request_with_conflicts.source_branch_head.sha)
+ end
+ end
+ end
end
diff --git a/spec/controllers/projects/milestones_controller_spec.rb b/spec/controllers/projects/milestones_controller_spec.rb
index d173bb350f1..4e3ef5dc6fa 100644
--- a/spec/controllers/projects/milestones_controller_spec.rb
+++ b/spec/controllers/projects/milestones_controller_spec.rb
@@ -14,7 +14,7 @@ describe Projects::MilestonesController do
end
describe "#destroy" do
- it "should remove milestone" do
+ it "removes milestone" do
expect(issue.milestone_id).to eq(milestone.id)
delete :destroy, namespace_id: project.namespace.id, project_id: project.id, id: milestone.iid, format: :js
diff --git a/spec/controllers/projects/notes_controller_spec.rb b/spec/controllers/projects/notes_controller_spec.rb
index 75590c1ed4f..92e38b02615 100644
--- a/spec/controllers/projects/notes_controller_spec.rb
+++ b/spec/controllers/projects/notes_controller_spec.rb
@@ -1,4 +1,4 @@
-require('spec_helper')
+require 'spec_helper'
describe Projects::NotesController do
let(:user) { create(:user) }
@@ -6,7 +6,15 @@ describe Projects::NotesController do
let(:issue) { create(:issue, project: project) }
let(:note) { create(:note, noteable: issue, project: project) }
- describe 'POST #toggle_award_emoji' do
+ let(:request_params) do
+ {
+ namespace_id: project.namespace,
+ project_id: project,
+ id: note
+ }
+ end
+
+ describe 'POST toggle_award_emoji' do
before do
sign_in(user)
project.team << [user, :developer]
@@ -14,23 +22,132 @@ describe Projects::NotesController do
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")
+ post(:toggle_award_emoji, request_params.merge(name: "thumbsup"))
end.to change { note.award_emoji.count }.by(1)
expect(response).to have_http_status(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")
+ post(:toggle_award_emoji, request_params.merge(name: "thumbsup"))
expect do
- post(:toggle_award_emoji, namespace_id: project.namespace.path,
- project_id: project.path, id: note.id, name: "thumbsup")
+ post(:toggle_award_emoji, request_params.merge(name: "thumbsup"))
end.to change { AwardEmoji.count }.by(-1)
expect(response).to have_http_status(200)
end
end
+
+ describe "resolving and unresolving" do
+ let(:merge_request) { create(:merge_request, source_project: project) }
+ let(:note) { create(:diff_note_on_merge_request, noteable: merge_request, project: project) }
+
+ describe 'POST resolve' do
+ before do
+ sign_in user
+ end
+
+ context "when the user is not authorized to resolve the note" do
+ it "returns status 404" do
+ post :resolve, request_params
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ context "when the user is authorized to resolve the note" do
+ before do
+ project.team << [user, :developer]
+ end
+
+ context "when the note is not resolvable" do
+ before do
+ note.update(system: true)
+ end
+
+ it "returns status 404" do
+ post :resolve, request_params
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ context "when the note is resolvable" do
+ it "resolves the note" do
+ post :resolve, request_params
+
+ expect(note.reload.resolved?).to be true
+ expect(note.reload.resolved_by).to eq(user)
+ end
+
+ it "sends notifications if all discussions are resolved" do
+ expect_any_instance_of(MergeRequests::ResolvedDiscussionNotificationService).to receive(:execute).with(merge_request)
+
+ post :resolve, request_params
+ end
+
+ it "returns the name of the resolving user" do
+ post :resolve, request_params
+
+ expect(JSON.parse(response.body)["resolved_by"]).to eq(user.name)
+ end
+
+ it "returns status 200" do
+ post :resolve, request_params
+
+ expect(response).to have_http_status(200)
+ end
+ end
+ end
+ end
+
+ describe 'DELETE unresolve' do
+ before do
+ sign_in user
+
+ note.resolve!(user)
+ end
+
+ context "when the user is not authorized to resolve the note" do
+ it "returns status 404" do
+ delete :unresolve, request_params
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ context "when the user is authorized to resolve the note" do
+ before do
+ project.team << [user, :developer]
+ end
+
+ context "when the note is not resolvable" do
+ before do
+ note.update(system: true)
+ end
+
+ it "returns status 404" do
+ delete :unresolve, request_params
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ context "when the note is resolvable" do
+ it "unresolves the note" do
+ delete :unresolve, request_params
+
+ expect(note.reload.resolved?).to be false
+ end
+
+ it "returns status 200" do
+ delete :unresolve, request_params
+
+ expect(response).to have_http_status(200)
+ end
+ end
+ end
+ end
+ end
end
diff --git a/spec/controllers/projects/protected_branches_controller_spec.rb b/spec/controllers/projects/protected_branches_controller_spec.rb
index 596d8d34b7c..da6112a13f7 100644
--- a/spec/controllers/projects/protected_branches_controller_spec.rb
+++ b/spec/controllers/projects/protected_branches_controller_spec.rb
@@ -3,7 +3,7 @@ require('spec_helper')
describe Projects::ProtectedBranchesController do
describe "GET #index" do
let(:project) { create(:project_empty_repo, :public) }
- it "redirect empty repo to projects page" do
+ it "redirects empty repo to projects page" do
get(:index, namespace_id: project.namespace.to_param, project_id: project.to_param)
end
end
diff --git a/spec/controllers/projects/raw_controller_spec.rb b/spec/controllers/projects/raw_controller_spec.rb
index 48f799d8ca1..04bd9a01f7b 100644
--- a/spec/controllers/projects/raw_controller_spec.rb
+++ b/spec/controllers/projects/raw_controller_spec.rb
@@ -24,7 +24,7 @@ describe Projects::RawController do
context 'image header' do
let(:id) { 'master/files/images/6049019_460s.jpg' }
- it 'set image content type header' do
+ it 'sets image content type header' do
get(:show,
namespace_id: public_project.namespace.to_param,
project_id: public_project.to_param,
diff --git a/spec/controllers/projects/services_controller_spec.rb b/spec/controllers/projects/services_controller_spec.rb
index ccd8c741c83..cccd492ef06 100644
--- a/spec/controllers/projects/services_controller_spec.rb
+++ b/spec/controllers/projects/services_controller_spec.rb
@@ -19,7 +19,7 @@ describe Projects::ServicesController do
describe "#test" do
context 'success' do
- it "should redirect and show success message" do
+ it "redirects and show success message" do
expect(service).to receive(:test).and_return({ success: true, result: 'done' })
get :test, namespace_id: project.namespace.id, project_id: project.id, id: service.id, format: :html
expect(response.status).to redirect_to('/')
@@ -28,7 +28,7 @@ describe Projects::ServicesController do
end
context 'failure' do
- it "should redirect and show failure message" do
+ it "redirects and show failure message" do
expect(service).to receive(:test).and_return({ success: false, result: 'Bad test' })
get :test, namespace_id: project.namespace.id, project_id: project.id, id: service.id, format: :html
expect(response.status).to redirect_to('/')
diff --git a/spec/controllers/projects/tags_controller_spec.rb b/spec/controllers/projects/tags_controller_spec.rb
new file mode 100644
index 00000000000..a6995145cc1
--- /dev/null
+++ b/spec/controllers/projects/tags_controller_spec.rb
@@ -0,0 +1,20 @@
+require 'spec_helper'
+
+describe Projects::TagsController do
+ let(:project) { create(:project, :public) }
+ let!(:release) { create(:release, project: project) }
+ let!(:invalid_release) { create(:release, project: project, tag: 'does-not-exist') }
+
+ describe 'GET index' do
+ before { get :index, namespace_id: project.namespace.to_param, project_id: project.to_param }
+
+ it 'returns the tags for the page' do
+ expect(assigns(:tags).map(&:name)).to eq(['v1.1.0', 'v1.0.0'])
+ end
+
+ it 'returns releases matching those tags' do
+ expect(assigns(:releases)).to include(release)
+ expect(assigns(:releases)).not_to include(invalid_release)
+ end
+ end
+end
diff --git a/spec/controllers/projects/templates_controller_spec.rb b/spec/controllers/projects/templates_controller_spec.rb
new file mode 100644
index 00000000000..7b3a26d7ca7
--- /dev/null
+++ b/spec/controllers/projects/templates_controller_spec.rb
@@ -0,0 +1,48 @@
+require 'spec_helper'
+
+describe Projects::TemplatesController do
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+ let(:user2) { create(:user) }
+ let(:file_path_1) { '.gitlab/issue_templates/bug.md' }
+ let(:body) { JSON.parse(response.body) }
+
+ before do
+ project.team << [user, :developer]
+ sign_in(user)
+ end
+
+ before do
+ project.team.add_user(user, Gitlab::Access::MASTER)
+ project.repository.commit_file(user, file_path_1, "something valid", "test 3", "master", false)
+ end
+
+ describe '#show' do
+ it 'renders template name and content as json' do
+ get(:show, namespace_id: project.namespace.to_param, template_type: "issue", key: "bug", project_id: project.path, format: :json)
+
+ expect(response.status).to eq(200)
+ expect(body["name"]).to eq("bug")
+ expect(body["content"]).to eq("something valid")
+ end
+
+ it 'renders 404 when unauthorized' do
+ sign_in(user2)
+ get(:show, namespace_id: project.namespace.to_param, template_type: "issue", key: "bug", project_id: project.path, format: :json)
+
+ expect(response.status).to eq(404)
+ end
+
+ it 'renders 404 when template type is not found' do
+ sign_in(user)
+ get(:show, namespace_id: project.namespace.to_param, template_type: "dont_exist", key: "bug", project_id: project.path, format: :json)
+
+ expect(response.status).to eq(404)
+ end
+
+ it 'renders 404 without errors' do
+ sign_in(user)
+ expect { get(:show, namespace_id: project.namespace.to_param, template_type: "dont_exist", key: "bug", project_id: project.path, format: :json) }.not_to raise_error
+ end
+ end
+end
diff --git a/spec/controllers/projects/todo_controller_spec.rb b/spec/controllers/projects/todo_controller_spec.rb
index 5a8bba28594..936320a3709 100644
--- a/spec/controllers/projects/todo_controller_spec.rb
+++ b/spec/controllers/projects/todo_controller_spec.rb
@@ -1,6 +1,8 @@
require('spec_helper')
describe Projects::TodosController do
+ include ApiHelpers
+
let(:user) { create(:user) }
let(:project) { create(:project) }
let(:issue) { create(:issue, project: project) }
@@ -8,43 +10,51 @@ describe Projects::TodosController do
context 'Issues' do
describe 'POST create' do
+ def go
+ post :create,
+ namespace_id: project.namespace.path,
+ project_id: project.path,
+ issuable_id: issue.id,
+ issuable_type: 'issue',
+ format: 'html'
+ end
+
context 'when authorized' do
before do
sign_in(user)
project.team << [user, :developer]
end
- it 'should create todo for issue' do
+ it 'creates todo for issue' do
expect do
- post(:create, namespace_id: project.namespace.path,
- project_id: project.path,
- issuable_id: issue.id,
- issuable_type: 'issue')
+ go
end.to change { user.todos.count }.by(1)
expect(response).to have_http_status(200)
end
+
+ it 'returns todo path and pending count' do
+ go
+
+ expect(response).to have_http_status(200)
+ expect(json_response['count']).to eq 1
+ expect(json_response['delete_path']).to match(/\/dashboard\/todos\/\d{1}/)
+ end
end
context 'when not authorized' do
- it 'should not create todo for issue that user has no access to' do
+ it 'does not create todo for issue that user has no access to' do
sign_in(user)
expect do
- post(:create, namespace_id: project.namespace.path,
- project_id: project.path,
- issuable_id: issue.id,
- issuable_type: 'issue')
+ go
end.to change { user.todos.count }.by(0)
expect(response).to have_http_status(404)
end
- it 'should not create todo for issue when user not logged in' do
+ it 'does not create todo for issue when user not logged in' do
expect do
- post(:create, namespace_id: project.namespace.path,
- project_id: project.path,
- issuable_id: issue.id,
- issuable_type: 'issue')
+ go
end.to change { user.todos.count }.by(0)
expect(response).to have_http_status(302)
@@ -55,43 +65,51 @@ describe Projects::TodosController do
context 'Merge Requests' do
describe 'POST create' do
+ def go
+ post :create,
+ namespace_id: project.namespace.path,
+ project_id: project.path,
+ issuable_id: merge_request.id,
+ issuable_type: 'merge_request',
+ format: 'html'
+ end
+
context 'when authorized' do
before do
sign_in(user)
project.team << [user, :developer]
end
- it 'should create todo for merge request' do
+ it 'creates todo for merge request' do
expect do
- post(:create, namespace_id: project.namespace.path,
- project_id: project.path,
- issuable_id: merge_request.id,
- issuable_type: 'merge_request')
+ go
end.to change { user.todos.count }.by(1)
expect(response).to have_http_status(200)
end
+
+ it 'returns todo path and pending count' do
+ go
+
+ expect(response).to have_http_status(200)
+ expect(json_response['count']).to eq 1
+ expect(json_response['delete_path']).to match(/\/dashboard\/todos\/\d{1}/)
+ end
end
context 'when not authorized' do
- it 'should not create todo for merge request user has no access to' do
+ it 'does not create todo for merge request user has no access to' do
sign_in(user)
expect do
- post(:create, namespace_id: project.namespace.path,
- project_id: project.path,
- issuable_id: merge_request.id,
- issuable_type: 'merge_request')
+ go
end.to change { user.todos.count }.by(0)
expect(response).to have_http_status(404)
end
- it 'should not create todo for merge request user has no access to' do
+ it 'does not create todo for merge request user has no access to' do
expect do
- post(:create, namespace_id: project.namespace.path,
- project_id: project.path,
- issuable_id: merge_request.id,
- issuable_type: 'merge_request')
+ go
end.to change { user.todos.count }.by(0)
expect(response).to have_http_status(302)
diff --git a/spec/controllers/projects/uploads_controller_spec.rb b/spec/controllers/projects/uploads_controller_spec.rb
index 0893ee89f6a..71d0e4be834 100644
--- a/spec/controllers/projects/uploads_controller_spec.rb
+++ b/spec/controllers/projects/uploads_controller_spec.rb
@@ -14,9 +14,9 @@ describe Projects::UploadsController do
context "without params['file']" do
it "returns an error" do
- post :create,
+ post :create,
namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ project_id: project.to_param,
format: :json
expect(response).to have_http_status(422)
end
@@ -34,23 +34,21 @@ describe Projects::UploadsController do
it 'returns a content with original filename, new link, and correct type.' do
expect(response.body).to match '\"alt\":\"rails_sample\"'
expect(response.body).to match "\"url\":\"/uploads"
- expect(response.body).to match '\"is_image\":true'
end
end
context 'with valid non-image file' do
before do
- post :create,
+ post :create,
namespace_id: project.namespace.to_param,
- project_id: project.to_param,
- file: txt,
+ project_id: project.to_param,
+ file: txt,
format: :json
end
it 'returns a content with original filename, new link, and correct type.' do
expect(response.body).to match '\"alt\":\"doc_sample.txt\"'
expect(response.body).to match "\"url\":\"/uploads"
- expect(response.body).to match '\"is_image\":false'
end
end
end
diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb
index 1b1b1bdf52d..ffe0641ddd7 100644
--- a/spec/controllers/projects_controller_spec.rb
+++ b/spec/controllers/projects_controller_spec.rb
@@ -43,6 +43,26 @@ describe ProjectsController do
end
end
+ context "project with empty repo" do
+ let(:empty_project) { create(:project_empty_repo, :public) }
+
+ before { sign_in(user) }
+
+ User.project_views.keys.each do |project_view|
+ context "with #{project_view} view set" do
+ before do
+ user.update_attributes(project_view: project_view)
+
+ get :show, namespace_id: empty_project.namespace.path, id: empty_project.path
+ end
+
+ it "renders the empty project view" do
+ expect(response).to render_template('empty')
+ end
+ end
+ end
+ end
+
context "rendering default project view" do
render_views
@@ -108,7 +128,7 @@ describe ProjectsController do
context "when the url contains .atom" do
let(:public_project_with_dot_atom) { build(:project, :public, name: 'my.atom', path: 'my.atom') }
- it 'expect an error creating the project' do
+ it 'expects an error creating the project' do
expect(public_project_with_dot_atom).not_to be_valid
end
end
@@ -202,7 +222,7 @@ describe ProjectsController do
create(:forked_project_link, forked_to_project: project_fork)
end
- it 'should remove fork from project' do
+ it 'removes fork from project' do
delete(:remove_fork,
namespace_id: project_fork.namespace.to_param,
id: project_fork.to_param, format: :js)
@@ -216,7 +236,7 @@ describe ProjectsController do
context 'when project not forked' do
let(:unforked_project) { create(:project, namespace: user.namespace) }
- it 'should do nothing if project was not forked' do
+ it 'does nothing if project was not forked' do
delete(:remove_fork,
namespace_id: unforked_project.namespace.to_param,
id: unforked_project.to_param, format: :js)
@@ -236,7 +256,7 @@ describe ProjectsController do
end
describe "GET refs" do
- it "should get a list of branches and tags" do
+ it "gets a list of branches and tags" do
get :refs, namespace_id: public_project.namespace.path, id: public_project.path
parsed_body = JSON.parse(response.body)
@@ -245,7 +265,7 @@ describe ProjectsController do
expect(parsed_body["Commits"]).to be_nil
end
- it "should get a list of branches, tags and commits" do
+ it "gets a list of branches, tags and commits" do
get :refs, namespace_id: public_project.namespace.path, id: public_project.path, ref: "123456"
parsed_body = JSON.parse(response.body)
diff --git a/spec/factories/boards.rb b/spec/factories/boards.rb
new file mode 100644
index 00000000000..35c4a0b6f08
--- /dev/null
+++ b/spec/factories/boards.rb
@@ -0,0 +1,5 @@
+FactoryGirl.define do
+ factory :board do
+ project factory: :empty_project
+ end
+end
diff --git a/spec/factories/broadcast_messages.rb b/spec/factories/broadcast_messages.rb
index efe9803b1a7..c2fdf89213a 100644
--- a/spec/factories/broadcast_messages.rb
+++ b/spec/factories/broadcast_messages.rb
@@ -1,8 +1,8 @@
FactoryGirl.define do
factory :broadcast_message do
message "MyText"
- starts_at Date.yesterday
- ends_at Date.tomorrow
+ starts_at 1.day.ago
+ ends_at 1.day.from_now
trait :expired do
starts_at 5.days.ago
diff --git a/spec/factories/ci/builds.rb b/spec/factories/ci/builds.rb
index fe05a0cfc00..0c93bbdfe26 100644
--- a/spec/factories/ci/builds.rb
+++ b/spec/factories/ci/builds.rb
@@ -3,8 +3,11 @@ include ActionDispatch::TestProcess
FactoryGirl.define do
factory :ci_build, class: Ci::Build do
name 'test'
+ stage 'test'
+ stage_idx 0
ref 'master'
tag false
+ status 'pending'
created_at 'Di 29. Okt 09:50:00 CET 2013'
started_at 'Di 29. Okt 09:51:28 CET 2013'
finished_at 'Di 29. Okt 09:53:28 CET 2013'
@@ -15,6 +18,11 @@ FactoryGirl.define do
services: ["postgres"]
}
end
+ yaml_variables do
+ [
+ { key: :DB_NAME, value: 'postgres', public: true }
+ ]
+ end
pipeline factory: :ci_pipeline
@@ -38,6 +46,15 @@ FactoryGirl.define do
status 'pending'
end
+ trait :created do
+ status 'created'
+ end
+
+ trait :manual do
+ status 'skipped'
+ self.when 'manual'
+ end
+
trait :allowed_to_fail do
allow_failure true
end
@@ -78,5 +95,21 @@ FactoryGirl.define do
build.save!
end
end
+
+ trait :artifacts_expired do
+ after(:create) do |build, _|
+ build.artifacts_file =
+ fixture_file_upload(Rails.root.join('spec/fixtures/ci_build_artifacts.zip'),
+ 'application/zip')
+
+ build.artifacts_metadata =
+ fixture_file_upload(Rails.root.join('spec/fixtures/ci_build_artifacts_metadata.gz'),
+ 'application/x-gzip')
+
+ build.artifacts_expire_at = 1.minute.ago
+
+ build.save!
+ end
+ end
end
end
diff --git a/spec/factories/ci/pipelines.rb b/spec/factories/ci/pipelines.rb
index a039bef6f3c..ac2a1ba5dff 100644
--- a/spec/factories/ci/pipelines.rb
+++ b/spec/factories/ci/pipelines.rb
@@ -1,24 +1,8 @@
-# == Schema Information
-#
-# Table name: commits
-#
-# id :integer not null, primary key
-# project_id :integer
-# ref :string(255)
-# sha :string(255)
-# before_sha :string(255)
-# push_data :text
-# created_at :datetime
-# updated_at :datetime
-# tag :boolean default(FALSE)
-# yaml_errors :text
-# committed_at :datetime
-# gl_project_id :integer
-#
-
FactoryGirl.define do
factory :ci_empty_pipeline, class: Ci::Pipeline do
+ ref 'master'
sha '97de212e80737a608d939f648d959671fb0a0142'
+ status 'pending'
project factory: :empty_project
diff --git a/spec/factories/ci/trigger_requests.rb b/spec/factories/ci/trigger_requests.rb
index 6d47d05f8ad..b8d8fab0e0b 100644
--- a/spec/factories/ci/trigger_requests.rb
+++ b/spec/factories/ci/trigger_requests.rb
@@ -5,7 +5,8 @@ FactoryGirl.define do
variables do
{
- TRIGGER_KEY: 'TRIGGER_VALUE'
+ TRIGGER_KEY_1: 'TRIGGER_VALUE_1',
+ TRIGGER_KEY_2: 'TRIGGER_VALUE_2'
}
end
end
diff --git a/spec/factories/commit_statuses.rb b/spec/factories/commit_statuses.rb
index 1e5c479616c..995f2080f10 100644
--- a/spec/factories/commit_statuses.rb
+++ b/spec/factories/commit_statuses.rb
@@ -7,6 +7,30 @@ FactoryGirl.define do
started_at 'Tue, 26 Jan 2016 08:21:42 +0100'
finished_at 'Tue, 26 Jan 2016 08:23:42 +0100'
+ trait :success do
+ status 'success'
+ end
+
+ trait :failed do
+ status 'failed'
+ end
+
+ trait :canceled do
+ status 'canceled'
+ end
+
+ trait :running do
+ status 'running'
+ end
+
+ trait :pending do
+ status 'pending'
+ end
+
+ trait :created do
+ status 'created'
+ end
+
after(:build) do |build, evaluator|
build.project = build.pipeline.project
end
diff --git a/spec/factories/environments.rb b/spec/factories/environments.rb
index 07265c26ca3..846cccfc7fa 100644
--- a/spec/factories/environments.rb
+++ b/spec/factories/environments.rb
@@ -3,5 +3,6 @@ FactoryGirl.define do
sequence(:name) { |n| "environment#{n}" }
project factory: :empty_project
+ sequence(:external_url) { |n| "https://env#{n}.example.gitlab.com" }
end
end
diff --git a/spec/factories/issues.rb b/spec/factories/issues.rb
index e72aa9479b7..2c0a2dd94ca 100644
--- a/spec/factories/issues.rb
+++ b/spec/factories/issues.rb
@@ -18,5 +18,15 @@ FactoryGirl.define do
factory :closed_issue, traits: [:closed]
factory :reopened_issue, traits: [:reopened]
+
+ factory :labeled_issue do
+ transient do
+ labels []
+ end
+
+ after(:create) do |issue, evaluator|
+ issue.update_attributes(labels: evaluator.labels)
+ end
+ end
end
end
diff --git a/spec/factories/lists.rb b/spec/factories/lists.rb
new file mode 100644
index 00000000000..9e3f06c682c
--- /dev/null
+++ b/spec/factories/lists.rb
@@ -0,0 +1,20 @@
+FactoryGirl.define do
+ factory :list do
+ board
+ label
+ list_type :label
+ sequence(:position)
+ end
+
+ factory :backlog_list, parent: :list do
+ list_type :backlog
+ label nil
+ position nil
+ end
+
+ factory :done_list, parent: :list do
+ list_type :done
+ label nil
+ position nil
+ end
+end
diff --git a/spec/factories/project_hooks.rb b/spec/factories/project_hooks.rb
index 3195fb3ddcc..424ecc65759 100644
--- a/spec/factories/project_hooks.rb
+++ b/spec/factories/project_hooks.rb
@@ -5,5 +5,16 @@ FactoryGirl.define do
trait :token do
token { SecureRandom.hex(10) }
end
+
+ trait :all_events_enabled do
+ push_events true
+ merge_requests_events true
+ tag_push_events true
+ issues_events true
+ note_events true
+ build_events true
+ pipeline_events true
+ wiki_page_events true
+ end
end
end
diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb
index b682ced75ac..f82d68a1816 100644
--- a/spec/factories/projects.rb
+++ b/spec/factories/projects.rb
@@ -83,4 +83,10 @@ FactoryGirl.define do
)
end
end
+
+ factory :project_with_board, parent: :empty_project do
+ after(:create) do |project|
+ project.create_board
+ end
+ end
end
diff --git a/spec/factories/protected_branches.rb b/spec/factories/protected_branches.rb
index 28ed8078157..b2695e0482a 100644
--- a/spec/factories/protected_branches.rb
+++ b/spec/factories/protected_branches.rb
@@ -2,5 +2,28 @@ FactoryGirl.define do
factory :protected_branch do
name
project
+
+ after(:build) do |protected_branch|
+ protected_branch.push_access_levels.new(access_level: Gitlab::Access::MASTER)
+ protected_branch.merge_access_levels.new(access_level: Gitlab::Access::MASTER)
+ end
+
+ trait :developers_can_push do
+ after(:create) do |protected_branch|
+ protected_branch.push_access_levels.first.update!(access_level: Gitlab::Access::DEVELOPER)
+ end
+ end
+
+ trait :developers_can_merge do
+ after(:create) do |protected_branch|
+ protected_branch.merge_access_levels.first.update!(access_level: Gitlab::Access::DEVELOPER)
+ end
+ end
+
+ trait :no_one_can_push do
+ after(:create) do |protected_branch|
+ protected_branch.push_access_levels.first.update!(access_level: Gitlab::Access::NO_ACCESS)
+ end
+ end
end
end
diff --git a/spec/factories/todos.rb b/spec/factories/todos.rb
index 7fc20cd5555..866e663f026 100644
--- a/spec/factories/todos.rb
+++ b/spec/factories/todos.rb
@@ -23,6 +23,10 @@ FactoryGirl.define do
action { Todo::BUILD_FAILED }
end
+ trait :approval_required do
+ action { Todo::APPROVAL_REQUIRED }
+ end
+
trait :done do
state :done
end
diff --git a/spec/factories/user_agent_details.rb b/spec/factories/user_agent_details.rb
new file mode 100644
index 00000000000..9763cc0cf15
--- /dev/null
+++ b/spec/factories/user_agent_details.rb
@@ -0,0 +1,7 @@
+FactoryGirl.define do
+ factory :user_agent_detail do
+ ip_address '127.0.0.1'
+ user_agent 'AppleWebKit/537.36'
+ association :subject, factory: :issue
+ end
+end
diff --git a/spec/factories_spec.rb b/spec/factories_spec.rb
index 675d9bd18b7..786e1456f5f 100644
--- a/spec/factories_spec.rb
+++ b/spec/factories_spec.rb
@@ -9,7 +9,7 @@ describe 'factories' do
expect { entity }.not_to raise_error
end
- it 'should be valid', if: factory.build_class < ActiveRecord::Base do
+ it 'is valid', if: factory.build_class < ActiveRecord::Base do
expect(entity).to be_valid
end
end
diff --git a/spec/features/admin/admin_abuse_reports_spec.rb b/spec/features/admin/admin_abuse_reports_spec.rb
index 16baf7e9516..c1731e6414a 100644
--- a/spec/features/admin/admin_abuse_reports_spec.rb
+++ b/spec/features/admin/admin_abuse_reports_spec.rb
@@ -11,7 +11,7 @@ describe "Admin::AbuseReports", feature: true, js: true do
end
describe 'in the abuse report view' do
- it "should present a link to the user's profile" do
+ it "presents a link to the user's profile" do
visit admin_abuse_reports_path
expect(page).to have_link user.name, href: user_path(user)
@@ -19,7 +19,7 @@ describe "Admin::AbuseReports", feature: true, js: true do
end
describe 'in the profile page of the user' do
- it 'should show a link to the admin view of the user' do
+ it 'shows a link to the admin view of the user' do
visit user_path(user)
expect(page).to have_link '', href: admin_user_path(user)
diff --git a/spec/features/admin/admin_builds_spec.rb b/spec/features/admin/admin_builds_spec.rb
index a6198389f04..e177059d959 100644
--- a/spec/features/admin/admin_builds_spec.rb
+++ b/spec/features/admin/admin_builds_spec.rb
@@ -36,12 +36,45 @@ describe 'Admin Builds' do
end
end
+ context 'Pending tab' do
+ context 'when have pending builds' do
+ it 'shows pending builds' do
+ build1 = create(:ci_build, pipeline: pipeline, status: :pending)
+ build2 = create(:ci_build, pipeline: pipeline, status: :running)
+ build3 = create(:ci_build, pipeline: pipeline, status: :success)
+ build4 = create(:ci_build, pipeline: pipeline, status: :failed)
+
+ visit admin_builds_path(scope: :pending)
+
+ expect(page).to have_selector('.nav-links li.active', text: 'Pending')
+ expect(page.find('.build-link')).to have_content(build1.id)
+ expect(page.find('.build-link')).not_to have_content(build2.id)
+ expect(page.find('.build-link')).not_to have_content(build3.id)
+ expect(page.find('.build-link')).not_to have_content(build4.id)
+ expect(page).to have_link 'Cancel all'
+ end
+ end
+
+ context 'when have no builds pending' do
+ it 'shows a message' do
+ create(:ci_build, pipeline: pipeline, status: :success)
+
+ visit admin_builds_path(scope: :pending)
+
+ expect(page).to have_selector('.nav-links li.active', text: 'Pending')
+ expect(page).to have_content 'No builds to show'
+ expect(page).not_to have_link 'Cancel all'
+ end
+ end
+ end
+
context 'Running tab' do
context 'when have running builds' do
it 'shows running builds' do
- build1 = create(:ci_build, pipeline: pipeline, status: :pending)
+ build1 = create(:ci_build, pipeline: pipeline, status: :running)
build2 = create(:ci_build, pipeline: pipeline, status: :success)
build3 = create(:ci_build, pipeline: pipeline, status: :failed)
+ build4 = create(:ci_build, pipeline: pipeline, status: :pending)
visit admin_builds_path(scope: :running)
@@ -49,6 +82,7 @@ describe 'Admin Builds' do
expect(page.find('.build-link')).to have_content(build1.id)
expect(page.find('.build-link')).not_to have_content(build2.id)
expect(page.find('.build-link')).not_to have_content(build3.id)
+ expect(page.find('.build-link')).not_to have_content(build4.id)
expect(page).to have_link 'Cancel all'
end
end
diff --git a/spec/features/admin/admin_disables_git_access_protocol_spec.rb b/spec/features/admin/admin_disables_git_access_protocol_spec.rb
index 5b1c0460274..66044b44495 100644
--- a/spec/features/admin/admin_disables_git_access_protocol_spec.rb
+++ b/spec/features/admin/admin_disables_git_access_protocol_spec.rb
@@ -45,7 +45,6 @@ feature 'Admin disables Git access protocol', feature: true do
expect(page).to have_content("git clone #{project.ssh_url_to_repo}")
expect(page).to have_selector('#clone-dropdown')
end
-
end
def visit_project
diff --git a/spec/features/admin/admin_hooks_spec.rb b/spec/features/admin/admin_hooks_spec.rb
index 7964951ae99..b3ce72b1452 100644
--- a/spec/features/admin/admin_hooks_spec.rb
+++ b/spec/features/admin/admin_hooks_spec.rb
@@ -9,7 +9,7 @@ describe "Admin::Hooks", feature: true do
end
describe "GET /admin/hooks" do
- it "should be ok" do
+ it "is ok" do
visit admin_root_path
page.within ".layout-nav" do
@@ -19,7 +19,7 @@ describe "Admin::Hooks", feature: true do
expect(current_path).to eq(admin_hooks_path)
end
- it "should have hooks list" do
+ it "has hooks list" do
visit admin_hooks_path
expect(page).to have_content(@system_hook.url)
end
@@ -33,7 +33,7 @@ describe "Admin::Hooks", feature: true do
expect { click_button "Add System Hook" }.to change(SystemHook, :count).by(1)
end
- it "should open new hook popup" do
+ it "opens new hook popup" do
expect(current_path).to eq(admin_hooks_path)
expect(page).to have_content(@url)
end
diff --git a/spec/features/admin/admin_projects_spec.rb b/spec/features/admin/admin_projects_spec.rb
index 101d955d693..30ded9202a4 100644
--- a/spec/features/admin/admin_projects_spec.rb
+++ b/spec/features/admin/admin_projects_spec.rb
@@ -11,11 +11,11 @@ describe "Admin::Projects", feature: true do
visit admin_namespaces_projects_path
end
- it "should be ok" do
+ it "is ok" do
expect(current_path).to eq(admin_namespaces_projects_path)
end
- it "should have projects list" do
+ it "has projects list" do
expect(page).to have_content(@project.name)
end
end
@@ -26,7 +26,7 @@ describe "Admin::Projects", feature: true do
click_link "#{@project.name}"
end
- it "should have project info" do
+ it "has project info" do
expect(page).to have_content(@project.path)
expect(page).to have_content(@project.name)
end
diff --git a/spec/features/admin/admin_system_info_spec.rb b/spec/features/admin/admin_system_info_spec.rb
index f4e5c26b519..1df972843e2 100644
--- a/spec/features/admin/admin_system_info_spec.rb
+++ b/spec/features/admin/admin_system_info_spec.rb
@@ -6,12 +6,49 @@ describe 'Admin System Info' do
end
describe 'GET /admin/system_info' do
- it 'shows system info page' do
- visit admin_system_info_path
+ let(:cpu) { double(:cpu, length: 2) }
+ let(:memory) { double(:memory, active_bytes: 4294967296, total_bytes: 17179869184) }
- expect(page).to have_content 'CPU'
- expect(page).to have_content 'Memory'
- expect(page).to have_content 'Disks'
+ context 'when all info is available' do
+ before do
+ allow(Vmstat).to receive(:cpu).and_return(cpu)
+ allow(Vmstat).to receive(:memory).and_return(memory)
+ visit admin_system_info_path
+ end
+
+ it 'shows system info page' do
+ expect(page).to have_content 'CPU 2 cores'
+ expect(page).to have_content 'Memory 4 GB / 16 GB'
+ expect(page).to have_content 'Disks'
+ end
+ end
+
+ context 'when CPU info is not available' do
+ before do
+ allow(Vmstat).to receive(:cpu).and_raise(Errno::ENOENT)
+ allow(Vmstat).to receive(:memory).and_return(memory)
+ visit admin_system_info_path
+ end
+
+ it 'shows system info page with no CPU info' do
+ expect(page).to have_content 'CPU Unable to collect CPU info'
+ expect(page).to have_content 'Memory 4 GB / 16 GB'
+ expect(page).to have_content 'Disks'
+ end
+ end
+
+ context 'when memory info is not available' do
+ before do
+ allow(Vmstat).to receive(:cpu).and_return(cpu)
+ allow(Vmstat).to receive(:memory).and_raise(Errno::ENOENT)
+ visit admin_system_info_path
+ end
+
+ it 'shows system info page with no CPU info' do
+ expect(page).to have_content 'CPU 2 cores'
+ expect(page).to have_content 'Memory Unable to collect memory info'
+ expect(page).to have_content 'Disks'
+ end
end
end
end
diff --git a/spec/features/admin/admin_users_spec.rb b/spec/features/admin/admin_users_spec.rb
index 767504df251..cb3191dfdde 100644
--- a/spec/features/admin/admin_users_spec.rb
+++ b/spec/features/admin/admin_users_spec.rb
@@ -8,11 +8,11 @@ describe "Admin::Users", feature: true do
visit admin_users_path
end
- it "should be ok" do
+ it "is ok" do
expect(current_path).to eq(admin_users_path)
end
- it "should have users list" do
+ it "has users list" do
expect(page).to have_content(@user.email)
expect(page).to have_content(@user.name)
end
@@ -66,11 +66,11 @@ describe "Admin::Users", feature: true do
fill_in "user_email", with: "bigbang@mail.com"
end
- it "should create new user" do
+ it "creates new user" do
expect { click_button "Create user" }.to change {User.count}.by(1)
end
- it "should apply defaults to user" do
+ it "applies defaults to user" do
click_button "Create user"
user = User.find_by(username: 'bang')
expect(user.projects_limit).
@@ -79,20 +79,20 @@ describe "Admin::Users", feature: true do
to eq(Gitlab.config.gitlab.default_can_create_group)
end
- it "should create user with valid data" do
+ it "creates user with valid data" do
click_button "Create user"
user = User.find_by(username: 'bang')
expect(user.name).to eq('Big Bang')
expect(user.email).to eq('bigbang@mail.com')
end
- it "should call send mail" do
+ it "calls send mail" do
expect_any_instance_of(NotificationService).to receive(:new_user)
click_button "Create user"
end
- it "should send valid email to user with email & password" do
+ it "sends valid email to user with email & password" do
perform_enqueued_jobs do
click_button "Create user"
end
@@ -106,7 +106,7 @@ describe "Admin::Users", feature: true do
end
describe "GET /admin/users/:id" do
- it "should have user info" do
+ it "has user info" do
visit admin_users_path
click_link @user.name
@@ -123,13 +123,13 @@ describe "Admin::Users", feature: true do
expect(page).to have_content('Impersonate')
end
- it 'should not show impersonate button for admin itself' do
+ it 'does not show impersonate button for admin itself' do
visit admin_user_path(@user)
expect(page).not_to have_content('Impersonate')
end
- it 'should not show impersonate button for blocked user' do
+ it 'does not show impersonate button for blocked user' do
another_user.block
visit admin_user_path(another_user)
@@ -153,7 +153,7 @@ describe "Admin::Users", feature: true do
expect(icon).not_to eql nil
end
- it 'can log out of impersonated user back to original user' do
+ it 'logs out of impersonated user back to original user' do
find(:css, 'li.impersonation a').click
expect(page.find(:css, '.header-user .profile-link')['data-user']).to eql(@user.username)
@@ -197,7 +197,7 @@ describe "Admin::Users", feature: true do
click_link "edit_user_#{@simple_user.id}"
end
- it "should have user edit page" do
+ it "has user edit page" do
expect(page).to have_content('Name')
expect(page).to have_content('Password')
end
@@ -212,12 +212,12 @@ describe "Admin::Users", feature: true do
click_button "Save changes"
end
- it "should show page with new data" do
+ it "shows page with new data" do
expect(page).to have_content('bigbang@mail.com')
expect(page).to have_content('Big Bang')
end
- it "should change user entry" do
+ it "changes user entry" do
@simple_user.reload
expect(@simple_user.name).to eq('Big Bang')
expect(@simple_user.is_admin?).to be_truthy
diff --git a/spec/features/atom/dashboard_spec.rb b/spec/features/atom/dashboard_spec.rb
index f81a3c117ff..746df36bb25 100644
--- a/spec/features/atom/dashboard_spec.rb
+++ b/spec/features/atom/dashboard_spec.rb
@@ -5,7 +5,7 @@ describe "Dashboard Feed", feature: true do
let!(:user) { create(:user, name: "Jonh") }
context "projects atom feed via private token" do
- it "should render projects atom feed" do
+ it "renders projects atom feed" do
visit dashboard_projects_path(:atom, private_token: user.private_token)
expect(body).to have_selector('feed title')
end
@@ -23,11 +23,11 @@ describe "Dashboard Feed", feature: true do
visit dashboard_projects_path(:atom, private_token: user.private_token)
end
- it "should have issue opened event" do
+ it "has issue opened event" do
expect(body).to have_content("#{user.name} opened issue ##{issue.iid}")
end
- it "should have issue comment event" do
+ it "has issue comment event" do
expect(body).
to have_content("#{user.name} commented on issue ##{issue.iid}")
end
diff --git a/spec/features/atom/issues_spec.rb b/spec/features/atom/issues_spec.rb
index baa7814e96a..09c140868fb 100644
--- a/spec/features/atom/issues_spec.rb
+++ b/spec/features/atom/issues_spec.rb
@@ -9,7 +9,7 @@ describe 'Issues Feed', feature: true do
before { project.team << [user, :developer] }
context 'when authenticated' do
- it 'should render atom feed' do
+ it 'renders atom feed' do
login_with user
visit namespace_project_issues_path(project.namespace, project, :atom)
@@ -22,7 +22,7 @@ describe 'Issues Feed', feature: true do
end
context 'when authenticated via private token' do
- it 'should render atom feed' do
+ it 'renders atom feed' do
visit namespace_project_issues_path(project.namespace, project, :atom,
private_token: user.private_token)
diff --git a/spec/features/atom/users_spec.rb b/spec/features/atom/users_spec.rb
index 91704377a07..a8833194421 100644
--- a/spec/features/atom/users_spec.rb
+++ b/spec/features/atom/users_spec.rb
@@ -5,7 +5,7 @@ describe "User Feed", feature: true do
let!(:user) { create(:user) }
context 'user atom feed via private token' do
- it "should render user atom feed" do
+ it "renders user atom feed" do
visit user_path(user, :atom, private_token: user.private_token)
expect(body).to have_selector('feed title')
end
@@ -43,24 +43,24 @@ describe "User Feed", feature: true do
visit user_path(user, :atom, private_token: user.private_token)
end
- it 'should have issue opened event' do
+ it 'has issue opened event' do
expect(body).to have_content("#{safe_name} opened issue ##{issue.iid}")
end
- it 'should have issue comment event' do
+ it 'has issue comment event' do
expect(body).
to have_content("#{safe_name} commented on issue ##{issue.iid}")
end
- it 'should have XHTML summaries in issue descriptions' do
+ it 'has XHTML summaries in issue descriptions' do
expect(body).to match /we have a bug!<\/p>\n\n<hr ?\/>\n\n<p>I guess/
end
- it 'should have XHTML summaries in notes' do
+ it 'has XHTML summaries in notes' do
expect(body).to match /Bug confirmed <img[^>]*\/>/
end
- it 'should have XHTML summaries in merge request descriptions' do
+ it 'has XHTML summaries in merge request descriptions' do
expect(body).to match /Here is the fix: <\/p><div[^>]*><a[^>]*><img[^>]*\/><\/a><\/div>/
end
end
diff --git a/spec/features/boards/boards_spec.rb b/spec/features/boards/boards_spec.rb
new file mode 100644
index 00000000000..5d777895542
--- /dev/null
+++ b/spec/features/boards/boards_spec.rb
@@ -0,0 +1,634 @@
+require 'rails_helper'
+
+describe 'Issue Boards', feature: true, js: true do
+ include WaitForAjax
+
+ let(:project) { create(:empty_project, :public) }
+ let(:user) { create(:user) }
+ let!(:user2) { create(:user) }
+
+ before do
+ project.create_board
+ project.board.lists.create(list_type: :backlog)
+ project.board.lists.create(list_type: :done)
+
+ project.team << [user, :master]
+ project.team << [user2, :master]
+
+ login_as(user)
+ end
+
+ context 'no lists' do
+ before do
+ visit namespace_project_board_path(project.namespace, project)
+ wait_for_vue_resource
+ expect(page).to have_selector('.board', count: 3)
+ end
+
+ it 'shows blank state' do
+ expect(page).to have_content('Welcome to your Issue Board!')
+ end
+
+ it 'hides the blank state when clicking nevermind button' do
+ page.within(find('.board-blank-state')) do
+ click_button("Nevermind, I'll use my own")
+ end
+ expect(page).to have_selector('.board', count: 2)
+ end
+
+ it 'creates default lists' do
+ lists = ['Backlog', 'Development', 'Testing', 'Production', 'Ready', 'Done']
+
+ page.within(find('.board-blank-state')) do
+ click_button('Add default lists')
+ end
+ wait_for_vue_resource
+
+ expect(page).to have_selector('.board', count: 6)
+
+ page.all('.board').each_with_index do |list, i|
+ expect(list.find('.board-title')).to have_content(lists[i])
+ end
+ end
+ end
+
+ context 'with lists' do
+ let(:milestone) { create(:milestone, project: project) }
+
+ let(:planning) { create(:label, project: project, name: 'Planning') }
+ let(:development) { create(:label, project: project, name: 'Development') }
+ let(:testing) { create(:label, project: project, name: 'Testing') }
+ let(:bug) { create(:label, project: project, name: 'Bug') }
+ let!(:backlog) { create(:label, project: project, name: 'Backlog') }
+ let!(:done) { create(:label, project: project, name: 'Done') }
+
+ let!(:list1) { create(:list, board: project.board, label: planning, position: 0) }
+ let!(:list2) { create(:list, board: project.board, label: development, position: 1) }
+
+ let!(:confidential_issue) { create(:issue, :confidential, project: project, author: user) }
+ let!(:issue1) { create(:issue, project: project, assignee: user) }
+ let!(:issue2) { create(:issue, project: project, author: user2) }
+ let!(:issue3) { create(:issue, project: project) }
+ let!(:issue4) { create(:issue, project: project) }
+ let!(:issue5) { create(:labeled_issue, project: project, labels: [planning], milestone: milestone) }
+ let!(:issue6) { create(:labeled_issue, project: project, labels: [planning, development]) }
+ let!(:issue7) { create(:labeled_issue, project: project, labels: [development]) }
+ let!(:issue8) { create(:closed_issue, project: project) }
+ let!(:issue9) { create(:labeled_issue, project: project, labels: [testing, bug]) }
+
+ before do
+ visit namespace_project_board_path(project.namespace, project)
+
+ wait_for_vue_resource
+
+ expect(page).to have_selector('.board', count: 4)
+ expect(find('.board:nth-child(1)')).to have_selector('.card')
+ expect(find('.board:nth-child(2)')).to have_selector('.card')
+ expect(find('.board:nth-child(3)')).to have_selector('.card')
+ expect(find('.board:nth-child(4)')).to have_selector('.card')
+ end
+
+ it 'shows lists' do
+ expect(page).to have_selector('.board', count: 4)
+ end
+
+ it 'shows issues in lists' do
+ page.within(find('.board:nth-child(2)')) do
+ expect(page.find('.board-header')).to have_content('2')
+ expect(page).to have_selector('.card', count: 2)
+ end
+
+ page.within(find('.board:nth-child(3)')) do
+ expect(page.find('.board-header')).to have_content('2')
+ expect(page).to have_selector('.card', count: 2)
+ end
+ end
+
+ it 'shows confidential issues with icon' do
+ page.within(find('.board', match: :first)) do
+ expect(page).to have_selector('.confidential-icon', count: 1)
+ end
+ end
+
+ it 'allows user to delete board' do
+ page.within(find('.board:nth-child(2)')) do
+ find('.board-delete').click
+ end
+
+ wait_for_vue_resource
+
+ expect(page).to have_selector('.board', count: 3)
+ end
+
+ it 'removes checkmark in new list dropdown after deleting' do
+ click_button 'Create new list'
+ wait_for_ajax
+
+ page.within(find('.board:nth-child(2)')) do
+ find('.board-delete').click
+ end
+
+ wait_for_vue_resource
+
+ expect(page).to have_selector('.board', count: 3)
+ expect(find(".js-board-list-#{planning.id}", visible: false)).not_to have_css('.is-active')
+ end
+
+ it 'infinite scrolls list' do
+ 50.times do
+ create(:issue, project: project)
+ end
+
+ visit namespace_project_board_path(project.namespace, project)
+ wait_for_vue_resource
+
+ page.within(find('.board', match: :first)) do
+ expect(page.find('.board-header')).to have_content('20')
+ expect(page).to have_selector('.card', count: 20)
+
+ evaluate_script("document.querySelectorAll('.board .board-list')[0].scrollTop = document.querySelectorAll('.board .board-list')[0].scrollHeight")
+ wait_for_vue_resource(spinner: false)
+
+ expect(page.find('.board-header')).to have_content('40')
+ expect(page).to have_selector('.card', count: 40)
+ end
+ end
+
+ context 'backlog' do
+ it 'shows issues in backlog with no labels' do
+ page.within(find('.board', match: :first)) do
+ expect(page.find('.board-header')).to have_content('6')
+ expect(page).to have_selector('.card', count: 6)
+ end
+ end
+
+ it 'is searchable' do
+ page.within(find('.board', match: :first)) do
+ find('.form-control').set issue1.title
+
+ wait_for_vue_resource(spinner: false)
+
+ expect(page).to have_selector('.card', count: 1)
+ end
+ end
+
+ it 'clears search' do
+ page.within(find('.board', match: :first)) do
+ find('.form-control').set issue1.title
+
+ expect(page).to have_selector('.card', count: 1)
+
+ find('.board-search-clear-btn').click
+ end
+
+ wait_for_vue_resource
+
+ page.within(find('.board', match: :first)) do
+ expect(page).to have_selector('.card', count: 6)
+ end
+ end
+
+ it 'moves issue from backlog into list' do
+ drag_to(list_to_index: 1)
+
+ page.within(find('.board', match: :first)) do
+ expect(page.find('.board-header')).to have_content('5')
+ expect(page).to have_selector('.card', count: 5)
+ end
+
+ wait_for_vue_resource
+
+ page.within(find('.board:nth-child(2)')) do
+ expect(page.find('.board-header')).to have_content('3')
+ expect(page).to have_selector('.card', count: 3)
+ end
+ end
+ end
+
+ context 'done' do
+ it 'shows list of done issues' do
+ expect(find('.board:nth-child(4)')).to have_selector('.card', count: 1)
+ end
+
+ it 'moves issue to done' do
+ drag_to(list_from_index: 0, list_to_index: 3)
+
+ expect(find('.board:nth-child(4)')).to have_selector('.card', count: 2)
+ expect(find('.board:nth-child(4)')).to have_content(issue9.title)
+ expect(find('.board:nth-child(4)')).not_to have_content(planning.title)
+ end
+
+ it 'removes all of the same issue to done' do
+ drag_to(list_from_index: 1, list_to_index: 3)
+
+ expect(find('.board:nth-child(2)')).to have_selector('.card', count: 1)
+ expect(find('.board:nth-child(3)')).to have_selector('.card', count: 1)
+ expect(find('.board:nth-child(4)')).to have_content(issue6.title)
+ expect(find('.board:nth-child(4)')).not_to have_content(planning.title)
+ end
+ end
+
+ context 'lists' do
+ it 'changes position of list' do
+ drag_to(list_from_index: 1, list_to_index: 2, selector: '.board-header')
+
+ expect(find('.board:nth-child(2)')).to have_content(development.title)
+ expect(find('.board:nth-child(2)')).to have_content(planning.title)
+ end
+
+ it 'issue moves between lists' do
+ drag_to(list_from_index: 1, card_index: 1, list_to_index: 2)
+
+ expect(find('.board:nth-child(2)')).to have_selector('.card', count: 1)
+ expect(find('.board:nth-child(3)')).to have_selector('.card', count: 3)
+ expect(find('.board:nth-child(3)')).to have_content(issue6.title)
+ expect(find('.board:nth-child(3)').all('.card').last).not_to have_content(development.title)
+ end
+
+ it 'issue moves between lists' do
+ drag_to(list_from_index: 2, list_to_index: 1)
+
+ expect(find('.board:nth-child(2)')).to have_selector('.card', count: 3)
+ expect(find('.board:nth-child(3)')).to have_selector('.card', count: 1)
+ expect(find('.board:nth-child(2)')).to have_content(issue7.title)
+ expect(find('.board:nth-child(2)').all('.card').first).not_to have_content(planning.title)
+ end
+
+ it 'issue moves from done' do
+ drag_to(list_from_index: 3, list_to_index: 1)
+
+ expect(find('.board:nth-child(2)')).to have_selector('.card', count: 3)
+ expect(find('.board:nth-child(2)')).to have_content(issue8.title)
+ end
+
+ context 'issue card' do
+ it 'shows assignee' do
+ page.within(find('.board', match: :first)) do
+ expect(page).to have_selector('.avatar', count: 1)
+ end
+ end
+ end
+
+ context 'new list' do
+ it 'shows all labels in new list dropdown' do
+ click_button 'Create new list'
+ wait_for_ajax
+
+ page.within('.dropdown-menu-issues-board-new') do
+ expect(page).to have_content(planning.title)
+ expect(page).to have_content(development.title)
+ expect(page).to have_content(testing.title)
+ end
+ end
+
+ it 'creates new list for label' do
+ click_button 'Create new list'
+ wait_for_ajax
+
+ page.within('.dropdown-menu-issues-board-new') do
+ click_link testing.title
+ end
+
+ wait_for_vue_resource
+
+ expect(page).to have_selector('.board', count: 5)
+ end
+
+ it 'creates new list for Backlog label' do
+ click_button 'Create new list'
+ wait_for_ajax
+
+ page.within('.dropdown-menu-issues-board-new') do
+ click_link backlog.title
+ end
+
+ wait_for_vue_resource
+
+ expect(page).to have_selector('.board', count: 5)
+ end
+
+ it 'creates new list for Done label' do
+ click_button 'Create new list'
+ wait_for_ajax
+
+ page.within('.dropdown-menu-issues-board-new') do
+ click_link done.title
+ end
+
+ wait_for_vue_resource
+
+ expect(page).to have_selector('.board', count: 5)
+ end
+
+ it 'moves issues from backlog into new list' do
+ page.within(find('.board', match: :first)) do
+ expect(page.find('.board-header')).to have_content('6')
+ expect(page).to have_selector('.card', count: 6)
+ end
+
+ click_button 'Create new list'
+ wait_for_ajax
+
+ page.within('.dropdown-menu-issues-board-new') do
+ click_link testing.title
+ end
+
+ wait_for_vue_resource
+
+ page.within(find('.board', match: :first)) do
+ expect(page.find('.board-header')).to have_content('5')
+ expect(page).to have_selector('.card', count: 5)
+ end
+ end
+ end
+ end
+
+ context 'filtering' do
+ it 'filters by author' do
+ page.within '.issues-filters' do
+ click_button('Author')
+ wait_for_ajax
+
+ page.within '.dropdown-menu-author' do
+ click_link(user2.name)
+ end
+ wait_for_vue_resource(spinner: false)
+
+ expect(find('.js-author-search')).to have_content(user2.name)
+ end
+
+ wait_for_vue_resource
+
+ page.within(find('.board', match: :first)) do
+ expect(page.find('.board-header')).to have_content('1')
+ expect(page).to have_selector('.card', count: 1)
+ end
+
+ page.within(find('.board:nth-child(2)')) do
+ expect(page.find('.board-header')).to have_content('0')
+ expect(page).to have_selector('.card', count: 0)
+ end
+ end
+
+ it 'filters by assignee' do
+ page.within '.issues-filters' do
+ click_button('Assignee')
+ wait_for_ajax
+
+ page.within '.dropdown-menu-assignee' do
+ click_link(user.name)
+ end
+ wait_for_vue_resource(spinner: false)
+
+ expect(find('.js-assignee-search')).to have_content(user.name)
+ end
+
+ wait_for_vue_resource
+
+ page.within(find('.board', match: :first)) do
+ expect(page.find('.board-header')).to have_content('1')
+ expect(page).to have_selector('.card', count: 1)
+ end
+
+ page.within(find('.board:nth-child(2)')) do
+ expect(page.find('.board-header')).to have_content('0')
+ expect(page).to have_selector('.card', count: 0)
+ end
+ end
+
+ it 'filters by milestone' do
+ page.within '.issues-filters' do
+ click_button('Milestone')
+ wait_for_ajax
+
+ page.within '.milestone-filter' do
+ click_link(milestone.title)
+ end
+ wait_for_vue_resource(spinner: false)
+
+ expect(find('.js-milestone-select')).to have_content(milestone.title)
+ end
+
+ wait_for_vue_resource
+
+ page.within(find('.board', match: :first)) do
+ expect(page.find('.board-header')).to have_content('0')
+ expect(page).to have_selector('.card', count: 0)
+ end
+
+ page.within(find('.board:nth-child(2)')) do
+ expect(page.find('.board-header')).to have_content('1')
+ expect(page).to have_selector('.card', count: 1)
+ end
+ end
+
+ it 'filters by label' do
+ page.within '.issues-filters' do
+ click_button('Label')
+ wait_for_ajax
+
+ page.within '.dropdown-menu-labels' do
+ click_link(testing.title)
+ wait_for_vue_resource(spinner: false)
+ find('.dropdown-menu-close').click
+ end
+ end
+
+ wait_for_vue_resource
+
+ page.within(find('.board', match: :first)) do
+ expect(page.find('.board-header')).to have_content('1')
+ expect(page).to have_selector('.card', count: 1)
+ end
+
+ page.within(find('.board:nth-child(2)')) do
+ expect(page.find('.board-header')).to have_content('0')
+ expect(page).to have_selector('.card', count: 0)
+ end
+ end
+
+ it 'infinite scrolls list with label filter' do
+ 50.times do
+ create(:labeled_issue, project: project, labels: [testing])
+ end
+
+ page.within '.issues-filters' do
+ click_button('Label')
+ wait_for_ajax
+
+ page.within '.dropdown-menu-labels' do
+ click_link(testing.title)
+ wait_for_vue_resource(spinner: false)
+ find('.dropdown-menu-close').click
+ end
+ end
+
+ wait_for_vue_resource
+
+ page.within(find('.board', match: :first)) do
+ expect(page.find('.board-header')).to have_content('20')
+ expect(page).to have_selector('.card', count: 20)
+
+ evaluate_script("document.querySelectorAll('.board .board-list')[0].scrollTop = document.querySelectorAll('.board .board-list')[0].scrollHeight")
+
+ expect(page.find('.board-header')).to have_content('40')
+ expect(page).to have_selector('.card', count: 40)
+ end
+ end
+
+ it 'filters by multiple labels' do
+ page.within '.issues-filters' do
+ click_button('Label')
+ wait_for_ajax
+
+ page.within(find('.dropdown-menu-labels')) do
+ click_link(testing.title)
+ wait_for_vue_resource(spinner: false)
+ click_link(bug.title)
+ wait_for_vue_resource(spinner: false)
+ find('.dropdown-menu-close').click
+ end
+ end
+
+ wait_for_vue_resource
+
+ page.within(find('.board', match: :first)) do
+ expect(page.find('.board-header')).to have_content('1')
+ expect(page).to have_selector('.card', count: 1)
+ end
+
+ page.within(find('.board:nth-child(2)')) do
+ expect(page.find('.board-header')).to have_content('0')
+ expect(page).to have_selector('.card', count: 0)
+ end
+ end
+
+ it 'filters by no label' do
+ page.within '.issues-filters' do
+ click_button('Label')
+ wait_for_ajax
+
+ page.within '.dropdown-menu-labels' do
+ click_link("No Label")
+ wait_for_vue_resource(spinner: false)
+ find('.dropdown-menu-close').click
+ end
+ end
+
+ wait_for_vue_resource
+
+ page.within(find('.board', match: :first)) do
+ expect(page.find('.board-header')).to have_content('5')
+ expect(page).to have_selector('.card', count: 5)
+ end
+
+ page.within(find('.board:nth-child(2)')) do
+ expect(page.find('.board-header')).to have_content('0')
+ expect(page).to have_selector('.card', count: 0)
+ end
+ end
+
+ it 'filters by clicking label button on issue' do
+ page.within(find('.board', match: :first)) do
+ expect(page).to have_selector('.card', count: 6)
+ expect(find('.card', match: :first)).to have_content(bug.title)
+ click_button(bug.title)
+ wait_for_vue_resource(spinner: false)
+ end
+
+ wait_for_vue_resource
+
+ page.within(find('.board', match: :first)) do
+ expect(page.find('.board-header')).to have_content('1')
+ expect(page).to have_selector('.card', count: 1)
+ end
+
+ page.within(find('.board:nth-child(2)')) do
+ expect(page.find('.board-header')).to have_content('0')
+ expect(page).to have_selector('.card', count: 0)
+ end
+
+ page.within('.labels-filter') do
+ expect(find('.dropdown-toggle-text')).to have_content(bug.title)
+ end
+ end
+
+ it 'removes label filter by clicking label button on issue' do
+ page.within(find('.board', match: :first)) do
+ page.within(find('.card', match: :first)) do
+ click_button(bug.title)
+ end
+ wait_for_vue_resource(spinner: false)
+
+ expect(page).to have_selector('.card', count: 1)
+ end
+
+ wait_for_vue_resource
+
+ page.within('.labels-filter') do
+ expect(find('.dropdown-toggle-text')).to have_content(bug.title)
+ end
+ end
+ end
+ end
+
+ context 'keyboard shortcuts' do
+ before do
+ visit namespace_project_board_path(project.namespace, project)
+ wait_for_vue_resource
+ end
+
+ it 'allows user to use keyboard shortcuts' do
+ find('.boards-list').native.send_keys('i')
+ expect(page).to have_content('New Issue')
+ end
+ end
+
+ context 'signed out user' do
+ before do
+ logout
+ visit namespace_project_board_path(project.namespace, project)
+ wait_for_vue_resource
+ end
+
+ it 'does not show create new list' do
+ expect(page).not_to have_selector('.js-new-board-list')
+ end
+ end
+
+ context 'as guest user' do
+ let(:user_guest) { create(:user) }
+
+ before do
+ project.team << [user_guest, :guest]
+ logout
+ login_as(user_guest)
+ visit namespace_project_board_path(project.namespace, project)
+ wait_for_vue_resource
+ end
+
+ it 'does not show create new list' do
+ expect(page).not_to have_selector('.js-new-board-list')
+ end
+ end
+
+ def drag_to(list_from_index: 0, card_index: 0, to_index: 0, list_to_index: 0, selector: '.board-list')
+ evaluate_script("simulateDrag({scrollable: document.getElementById('board-app'), from: {el: $('#{selector}').eq(#{list_from_index}).get(0), index: #{card_index}}, to: {el: $('.board-list').eq(#{list_to_index}).get(0), index: #{to_index}}});")
+
+ Timeout.timeout(Capybara.default_max_wait_time) do
+ loop until page.evaluate_script('window.SIMULATE_DRAG_ACTIVE').zero?
+ end
+
+ wait_for_vue_resource
+ end
+
+ def wait_for_vue_resource(spinner: true)
+ Timeout.timeout(Capybara.default_max_wait_time) do
+ loop until page.evaluate_script('Vue.activeResources').zero?
+ end
+
+ if spinner
+ expect(find('.boards-list')).not_to have_selector('.fa-spinner')
+ end
+ end
+end
diff --git a/spec/features/builds_spec.rb b/spec/features/builds_spec.rb
index 16832c297ac..0cfeb2e57d8 100644
--- a/spec/features/builds_spec.rb
+++ b/spec/features/builds_spec.rb
@@ -13,17 +13,33 @@ describe "Builds" do
end
describe "GET /:project/builds" do
+ context "Pending scope" do
+ before do
+ visit namespace_project_builds_path(@project.namespace, @project, scope: :pending)
+ end
+
+ it "shows Pending tab builds" do
+ expect(page).to have_link 'Cancel running'
+ expect(page).to have_selector('.nav-links li.active', text: 'Pending')
+ expect(page).to have_content @build.short_sha
+ expect(page).to have_content @build.ref
+ expect(page).to have_content @build.name
+ end
+ end
+
context "Running scope" do
before do
@build.run!
visit namespace_project_builds_path(@project.namespace, @project, scope: :running)
end
- it { expect(page).to have_selector('.nav-links li.active', text: 'Running') }
- it { expect(page).to have_link 'Cancel running' }
- 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 "shows Running tab builds" do
+ expect(page).to have_selector('.nav-links li.active', text: 'Running')
+ expect(page).to have_link 'Cancel running'
+ expect(page).to have_content @build.short_sha
+ expect(page).to have_content @build.ref
+ expect(page).to have_content @build.name
+ end
end
context "Finished scope" do
@@ -32,9 +48,11 @@ describe "Builds" do
visit namespace_project_builds_path(@project.namespace, @project, scope: :finished)
end
- it { expect(page).to have_selector('.nav-links li.active', text: 'Finished') }
- it { expect(page).to have_content 'No builds to show' }
- it { expect(page).to have_link 'Cancel running' }
+ it "shows Finished tab builds" do
+ expect(page).to have_selector('.nav-links li.active', text: 'Finished')
+ expect(page).to have_content 'No builds to show'
+ expect(page).to have_link 'Cancel running'
+ end
end
context "All builds" do
@@ -43,11 +61,13 @@ describe "Builds" do
visit namespace_project_builds_path(@project.namespace, @project)
end
- it { expect(page).to have_selector('.nav-links li.active', text: 'All') }
- 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).not_to have_link 'Cancel running' }
+ it "shows All tab builds" do
+ expect(page).to have_selector('.nav-links li.active', text: 'All')
+ expect(page).to have_content @build.short_sha
+ expect(page).to have_content @build.ref
+ expect(page).to have_content @build.name
+ expect(page).not_to have_link 'Cancel running'
+ end
end
end
@@ -179,9 +199,13 @@ describe "Builds" do
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' }
+ it 'shows the right status and buttons' do
+ expect(page).to have_http_status(200)
+ expect(page).to have_content 'pending'
+ page.within('aside.right-sidebar') do
+ expect(page).to have_content 'Cancel'
+ end
+ end
end
context "Build from other project" do
@@ -192,7 +216,25 @@ describe "Builds" do
page.driver.post(retry_namespace_project_build_path(@project.namespace, @project, @build2))
end
- it { expect(page.status_code).to eq(404) }
+ it { expect(page).to have_http_status(404) }
+ end
+
+ context "Build that current user is not allowed to retry" do
+ before do
+ @build.run!
+ @build.cancel!
+ @project.update(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
+
+ logout_direct
+ login_with(create(:user))
+ visit namespace_project_build_path(@project.namespace, @project, @build)
+ end
+
+ it 'does not show the Retry button' do
+ page.within('aside.right-sidebar') do
+ expect(page).not_to have_content 'Retry'
+ end
+ end
end
end
diff --git a/spec/features/ci_lint_spec.rb b/spec/features/ci_lint_spec.rb
index 30e29d9d552..81077f4b005 100644
--- a/spec/features/ci_lint_spec.rb
+++ b/spec/features/ci_lint_spec.rb
@@ -17,7 +17,7 @@ describe 'CI Lint' do
File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml'))
end
- it 'Yaml parsing' do
+ it 'parses Yaml' do
within "table" do
expect(page).to have_content('Job - rspec')
expect(page).to have_content('Job - spinach')
diff --git a/spec/features/commits_spec.rb b/spec/features/commits_spec.rb
index 45e1a157a1f..5910803df51 100644
--- a/spec/features/commits_spec.rb
+++ b/spec/features/commits_spec.rb
@@ -52,7 +52,7 @@ describe 'Commits' do
visit namespace_project_commits_path(project.namespace, project, :master)
end
- it 'should show build status' do
+ it 'shows build status' do
page.within("//li[@id='commit-#{pipeline.short_sha}']") do
expect(page).to have_css(".ci-status-link")
end
diff --git a/spec/features/compare_spec.rb b/spec/features/compare_spec.rb
new file mode 100644
index 00000000000..ca7f73e24cc
--- /dev/null
+++ b/spec/features/compare_spec.rb
@@ -0,0 +1,42 @@
+require "spec_helper"
+
+describe "Compare", js: true do
+ let(:user) { create(:user) }
+ let(:project) { create(:project) }
+
+ before do
+ project.team << [user, :master]
+ login_as user
+ visit namespace_project_compare_index_path(project.namespace, project, from: "master", to: "master")
+ end
+
+ describe "branches" do
+ it "pre-populates fields" do
+ expect(page.find_field("from").value).to eq("master")
+ end
+
+ it "compares branches" do
+ fill_in "from", with: "fea"
+ find("#from").click
+
+ click_link "feature"
+ expect(page.find_field("from").value).to eq("feature")
+
+ click_button "Compare"
+ expect(page).to have_content "Commits"
+ end
+ end
+
+ describe "tags" do
+ it "compares tags" do
+ fill_in "from", with: "v1.0"
+ find("#from").click
+
+ click_link "v1.0.0"
+ expect(page.find_field("from").value).to eq("v1.0.0")
+
+ click_button "Compare"
+ expect(page).to have_content "Commits"
+ end
+ end
+end
diff --git a/spec/features/dashboard/label_filter_spec.rb b/spec/features/dashboard/label_filter_spec.rb
index 24e83d44010..4cff12de854 100644
--- a/spec/features/dashboard/label_filter_spec.rb
+++ b/spec/features/dashboard/label_filter_spec.rb
@@ -16,7 +16,7 @@ describe 'Dashboard > label filter', feature: true, js: true do
end
context 'duplicate labels' do
- it 'should remove duplicate labels' do
+ it 'removes duplicate labels' do
page.within('.labels-filter') do
click_button 'Label'
end
diff --git a/spec/features/dashboard_issues_spec.rb b/spec/features/dashboard_issues_spec.rb
index 39805da9d0b..3fb1cb37544 100644
--- a/spec/features/dashboard_issues_spec.rb
+++ b/spec/features/dashboard_issues_spec.rb
@@ -16,7 +16,7 @@ describe "Dashboard Issues filtering", feature: true, js: true do
visit_issues
end
- it 'should show all issues with no milestone' do
+ it 'shows all issues with no milestone' do
show_milestone_dropdown
click_link 'No Milestone'
@@ -24,7 +24,7 @@ describe "Dashboard Issues filtering", feature: true, js: true do
expect(page).to have_selector('.issue', count: 1)
end
- it 'should show all issues with any milestone' do
+ it 'shows all issues with any milestone' do
show_milestone_dropdown
click_link 'Any Milestone'
@@ -32,7 +32,7 @@ describe "Dashboard Issues filtering", feature: true, js: true do
expect(page).to have_selector('.issue', count: 2)
end
- it 'should show all issues with the selected milestone' do
+ it 'shows all issues with the selected milestone' do
show_milestone_dropdown
page.within '.dropdown-content' do
diff --git a/spec/features/environments_spec.rb b/spec/features/environments_spec.rb
index 7fb28f4174b..fcd41b38413 100644
--- a/spec/features/environments_spec.rb
+++ b/spec/features/environments_spec.rb
@@ -13,6 +13,7 @@ feature 'Environments', feature: true do
describe 'when showing environments' do
given!(:environment) { }
given!(:deployment) { }
+ given!(:manual) { }
before do
visit namespace_project_environments_path(project.namespace, project)
@@ -43,6 +44,24 @@ feature 'Environments', feature: true do
scenario 'does show deployment SHA' do
expect(page).to have_link(deployment.short_sha)
end
+
+ context 'with build and manual actions' do
+ given(:pipeline) { create(:ci_pipeline, project: project) }
+ given(:build) { create(:ci_build, pipeline: pipeline) }
+ given(:deployment) { create(:deployment, environment: environment, deployable: build) }
+ given(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'deploy to production') }
+
+ scenario 'does show a play button' do
+ expect(page).to have_link(manual.name.humanize)
+ end
+
+ scenario 'does allow to play manual action' do
+ expect(manual).to be_skipped
+ expect{ click_link(manual.name.humanize) }.not_to change { Ci::Pipeline.count }
+ expect(page).to have_content(manual.name)
+ expect(manual.reload).to be_pending
+ end
+ end
end
end
@@ -54,6 +73,7 @@ feature 'Environments', feature: true do
describe 'when showing the environment' do
given(:environment) { create(:environment, project: project) }
given!(:deployment) { }
+ given!(:manual) { }
before do
visit namespace_project_environment_path(project.namespace, project, environment)
@@ -72,20 +92,36 @@ feature 'Environments', feature: true do
expect(page).to have_link(deployment.short_sha)
end
- scenario 'does not show a retry button for deployment without build' do
- expect(page).not_to have_link('Retry')
+ scenario 'does not show a re-deploy button for deployment without build' do
+ expect(page).not_to have_link('Re-deploy')
end
context 'with build' do
- given(:build) { create(:ci_build, project: project) }
+ given(:pipeline) { create(:ci_pipeline, project: project) }
+ given(:build) { create(:ci_build, pipeline: pipeline) }
given(:deployment) { create(:deployment, environment: environment, deployable: build) }
scenario 'does show build name' do
expect(page).to have_link("#{build.name} (##{build.id})")
end
- scenario 'does show retry button' do
- expect(page).to have_link('Retry')
+ scenario 'does show re-deploy button' do
+ expect(page).to have_link('Re-deploy')
+ end
+
+ context 'with manual action' do
+ given(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'deploy to production') }
+
+ scenario 'does show a play button' do
+ expect(page).to have_link(manual.name.humanize)
+ end
+
+ scenario 'does allow to play manual action' do
+ expect(manual).to be_skipped
+ expect{ click_link(manual.name.humanize) }.not_to change { Ci::Pipeline.count }
+ expect(page).to have_content(manual.name)
+ expect(manual.reload).to be_pending
+ end
end
end
end
@@ -104,7 +140,7 @@ feature 'Environments', feature: true do
context 'for valid name' do
before do
fill_in('Name', with: 'production')
- click_on 'Create environment'
+ click_on 'Save'
end
scenario 'does create a new pipeline' do
@@ -115,7 +151,7 @@ feature 'Environments', feature: true do
context 'for invalid name' do
before do
fill_in('Name', with: 'name with spaces')
- click_on 'Create environment'
+ click_on 'Save'
end
scenario 'does show errors' do
diff --git a/spec/features/expand_collapse_diffs_spec.rb b/spec/features/expand_collapse_diffs_spec.rb
new file mode 100644
index 00000000000..688f68d3cff
--- /dev/null
+++ b/spec/features/expand_collapse_diffs_spec.rb
@@ -0,0 +1,248 @@
+require 'spec_helper'
+
+feature 'Expand and collapse diffs', js: true, feature: true do
+ include WaitForAjax
+
+ let(:branch) { 'expand-collapse-diffs' }
+
+ before do
+ login_as :admin
+ project = create(:project)
+
+ # Ensure that undiffable.md is in .gitattributes
+ project.repository.copy_gitattributes(branch)
+ visit namespace_project_commit_path(project.namespace, project, project.commit(branch))
+ execute_script('window.ajaxUris = []; $(document).ajaxSend(function(event, xhr, settings) { ajaxUris.push(settings.url) });')
+ end
+
+ def file_container(filename)
+ find("[data-blob-diff-path*='#{filename}']")
+ end
+
+ # Use define_method instead of let (which is memoized) so that this just works across a
+ # reload.
+ #
+ files = [
+ 'small_diff.md', 'large_diff.md', 'large_diff_renamed.md', 'undiffable.md',
+ 'too_large.md', 'too_large_image.jpg'
+ ]
+
+ files.each do |file|
+ define_method(file.split('.').first) { file_container(file) }
+ end
+
+ context 'visiting a commit with collapsed diffs' do
+ it 'shows small diffs immediately' do
+ expect(small_diff).to have_selector('.code')
+ expect(small_diff).not_to have_selector('.nothing-here-block')
+ end
+
+ it 'collapses large diffs by default' do
+ expect(large_diff).not_to have_selector('.code')
+ expect(large_diff).to have_selector('.nothing-here-block')
+ end
+
+ it 'collapses large diffs for renamed files by default' do
+ expect(large_diff_renamed).not_to have_selector('.code')
+ expect(large_diff_renamed).to have_selector('.nothing-here-block')
+ expect(large_diff_renamed).to have_selector('.file-title .deletion')
+ expect(large_diff_renamed).to have_selector('.file-title .addition')
+ end
+
+ it 'shows non-renderable diffs as such immediately, regardless of their size' do
+ expect(undiffable).not_to have_selector('.code')
+ expect(undiffable).to have_selector('.nothing-here-block')
+ expect(undiffable).to have_content('gitattributes')
+ end
+
+ it 'does not allow diffs that are larger than the maximum size to be expanded' do
+ expect(too_large).not_to have_selector('.code')
+ expect(too_large).to have_selector('.nothing-here-block')
+ expect(too_large).to have_content('too large')
+ end
+
+ it 'shows image diffs immediately, regardless of their size' do
+ expect(too_large_image).not_to have_selector('.nothing-here-block')
+ expect(too_large_image).to have_selector('.image')
+ end
+
+ context 'expanding a diff for a renamed file' do
+ before do
+ large_diff_renamed.find('.nothing-here-block').click
+ wait_for_ajax
+ end
+
+ it 'shows the old content' do
+ old_line = large_diff_renamed.find('.line_content.old')
+
+ expect(old_line).to have_content('two copies')
+ end
+
+ it 'shows the new content' do
+ new_line = large_diff_renamed.find('.line_content.new', match: :prefer_exact)
+
+ expect(new_line).to have_content('three copies')
+ end
+ end
+
+ context 'expanding a large diff' do
+ before do
+ click_link('large_diff.md')
+ wait_for_ajax
+ end
+
+ it 'makes a request to get the content' do
+ ajax_uris = evaluate_script('ajaxUris')
+
+ expect(ajax_uris).not_to be_empty
+ expect(ajax_uris.first).to include('large_diff.md')
+ end
+
+ it 'shows the diff content' do
+ expect(large_diff).to have_selector('.code')
+ expect(large_diff).not_to have_selector('.nothing-here-block')
+ end
+
+ context 'adding a comment to the expanded diff' do
+ let(:comment_text) { 'A comment' }
+
+ before do
+ large_diff.find('.diff-line-num', match: :prefer_exact).hover
+ large_diff.find('.add-diff-note').click
+ large_diff.find('.note-textarea').send_keys comment_text
+ large_diff.find_button('Comment').click
+ wait_for_ajax
+ end
+
+ it 'adds the comment' do
+ expect(large_diff.find('.notes')).to have_content comment_text
+ end
+
+ context 'reloading the page' do
+ before { refresh }
+
+ it 'collapses the large diff by default' do
+ expect(large_diff).not_to have_selector('.code')
+ expect(large_diff).to have_selector('.nothing-here-block')
+ end
+
+ context 'expanding the diff' do
+ before do
+ click_link('large_diff.md')
+ wait_for_ajax
+ end
+
+ it 'shows the diff content' do
+ expect(large_diff).to have_selector('.code')
+ expect(large_diff).not_to have_selector('.nothing-here-block')
+ end
+
+ it 'shows the diff comment' do
+ expect(large_diff.find('.notes')).to have_content comment_text
+ end
+ end
+ end
+ end
+ end
+
+ context 'collapsing an expanded diff' do
+ before { click_link('small_diff.md') }
+
+ it 'hides the diff content' do
+ expect(small_diff).not_to have_selector('.code')
+ expect(small_diff).to have_selector('.nothing-here-block')
+ end
+
+ context 're-expanding the same diff' do
+ before { click_link('small_diff.md') }
+
+ it 'shows the diff content' do
+ expect(small_diff).to have_selector('.code')
+ expect(small_diff).not_to have_selector('.nothing-here-block')
+ end
+
+ it 'does not make a new HTTP request' do
+ expect(evaluate_script('ajaxUris')).not_to include(a_string_matching('small_diff.md'))
+ end
+ end
+ end
+ end
+
+ context 'visiting a commit without collapsed diffs' do
+ let(:branch) { 'feature' }
+
+ it 'does not show Expand all button' do
+ expect(page).not_to have_link('Expand all')
+ end
+ end
+
+ context 'visiting a commit with more than safe files' do
+ let(:branch) { 'expand-collapse-files' }
+
+ # safe-files -> 100 | safe-lines -> 5000 | commit-files -> 105
+ it 'does collapsing from the safe number of files to the end on small files' do
+ expect(page).to have_link('Expand all')
+
+ expect(page).to have_selector('.diff-content', count: 105)
+ expect(page).to have_selector('.diff-collapsed', count: 5)
+
+ %w(file-95.txt file-96.txt file-97.txt file-98.txt file-99.txt).each do |filename|
+ expect(find("[data-blob-diff-path*='#{filename}']")).to have_selector('.diff-collapsed')
+ end
+ end
+ end
+
+ context 'visiting a commit with more than safe lines' do
+ let(:branch) { 'expand-collapse-lines' }
+
+ # safe-files -> 100 | safe-lines -> 5000 | commit_files -> 8 (each 1250 lines)
+ it 'does collapsing from the safe number of lines to the end' do
+ expect(page).to have_link('Expand all')
+
+ expect(page).to have_selector('.diff-content', count: 6)
+ expect(page).to have_selector('.diff-collapsed', count: 2)
+
+ %w(file-4.txt file-5.txt).each do |filename|
+ expect(find("[data-blob-diff-path*='#{filename}']")).to have_selector('.diff-collapsed')
+ end
+ end
+ end
+
+ context 'expanding all diffs' do
+ before do
+ click_link('Expand all')
+ wait_for_ajax
+ execute_script('window.ajaxUris = []; $(document).ajaxSend(function(event, xhr, settings) { ajaxUris.push(settings.url) });')
+ end
+
+ it 'reloads the page with all diffs expanded' do
+ expect(small_diff).to have_selector('.code')
+ expect(small_diff).not_to have_selector('.nothing-here-block')
+
+ expect(large_diff).to have_selector('.code')
+ expect(large_diff).not_to have_selector('.nothing-here-block')
+ end
+
+ context 'collapsing an expanded diff' do
+ before { click_link('small_diff.md') }
+
+ it 'hides the diff content' do
+ expect(small_diff).not_to have_selector('.code')
+ expect(small_diff).to have_selector('.nothing-here-block')
+ end
+
+ context 're-expanding the same diff' do
+ before { click_link('small_diff.md') }
+
+ it 'shows the diff content' do
+ expect(small_diff).to have_selector('.code')
+ expect(small_diff).not_to have_selector('.nothing-here-block')
+ end
+
+ it 'does not make a new HTTP request' do
+ expect(evaluate_script('ajaxUris')).not_to include(a_string_matching('small_diff.md'))
+ end
+ end
+ end
+ end
+end
diff --git a/spec/features/gitlab_flavored_markdown_spec.rb b/spec/features/gitlab_flavored_markdown_spec.rb
index a89ac09f236..84d73d693bc 100644
--- a/spec/features/gitlab_flavored_markdown_spec.rb
+++ b/spec/features/gitlab_flavored_markdown_spec.rb
@@ -23,25 +23,25 @@ describe "GitLab Flavored Markdown", feature: true do
end
describe "for commits" do
- it "should render title in commits#index" do
+ it "renders title in commits#index" do
visit namespace_project_commits_path(project.namespace, project, 'master', limit: 1)
expect(page).to have_link(issue.to_reference)
end
- it "should render title in commits#show" do
+ it "renders title in commits#show" do
visit namespace_project_commit_path(project.namespace, project, commit)
expect(page).to have_link(issue.to_reference)
end
- it "should render description in commits#show" do
+ it "renders description in commits#show" do
visit namespace_project_commit_path(project.namespace, project, commit)
expect(page).to have_link(fred.to_reference)
end
- it "should render title in repositories#branches" do
+ it "renders title in repositories#branches" do
visit namespace_project_branches_path(project.namespace, project)
expect(page).to have_link(issue.to_reference)
@@ -62,19 +62,19 @@ describe "GitLab Flavored Markdown", feature: true do
description: "ask #{fred.to_reference} for details")
end
- it "should render subject in issues#index" do
+ it "renders subject in issues#index" do
visit namespace_project_issues_path(project.namespace, project)
expect(page).to have_link(@other_issue.to_reference)
end
- it "should render subject in issues#show" do
+ it "renders subject in issues#show" do
visit namespace_project_issue_path(project.namespace, project, @issue)
expect(page).to have_link(@other_issue.to_reference)
end
- it "should render details in issues#show" do
+ it "renders details in issues#show" do
visit namespace_project_issue_path(project.namespace, project, @issue)
expect(page).to have_link(fred.to_reference)
@@ -86,13 +86,13 @@ describe "GitLab Flavored Markdown", feature: true do
@merge_request = create(:merge_request, source_project: project, target_project: project, title: "fix #{issue.to_reference}")
end
- it "should render title in merge_requests#index" do
+ it "renders title in merge_requests#index" do
visit namespace_project_merge_requests_path(project.namespace, project)
expect(page).to have_link(issue.to_reference)
end
- it "should render title in merge_requests#show" do
+ it "renders title in merge_requests#show" do
visit namespace_project_merge_request_path(project.namespace, project, @merge_request)
expect(page).to have_link(issue.to_reference)
@@ -107,19 +107,19 @@ describe "GitLab Flavored Markdown", feature: true do
description: "ask #{fred.to_reference} for details")
end
- it "should render title in milestones#index" do
+ it "renders title in milestones#index" do
visit namespace_project_milestones_path(project.namespace, project)
expect(page).to have_link(issue.to_reference)
end
- it "should render title in milestones#show" do
+ it "renders title in milestones#show" do
visit namespace_project_milestone_path(project.namespace, project, @milestone)
expect(page).to have_link(issue.to_reference)
end
- it "should render description in milestones#show" do
+ it "renders description in milestones#show" do
visit namespace_project_milestone_path(project.namespace, project, @milestone)
expect(page).to have_link(fred.to_reference)
diff --git a/spec/features/groups/members/user_requests_access_spec.rb b/spec/features/groups/members/user_requests_access_spec.rb
index d1a6a98ab72..b3baa2ab57c 100644
--- a/spec/features/groups/members/user_requests_access_spec.rb
+++ b/spec/features/groups/members/user_requests_access_spec.rb
@@ -12,6 +12,13 @@ feature 'Groups > Members > User requests access', feature: true do
visit group_path(group)
end
+ scenario 'request access feature is disabled' do
+ group.update_attributes(request_access_enabled: false)
+ visit group_path(group)
+
+ expect(page).not_to have_content 'Request Access'
+ end
+
scenario 'user can request access to a group' do
perform_enqueued_jobs { click_link 'Request Access' }
diff --git a/spec/features/groups_spec.rb b/spec/features/groups_spec.rb
index 891df65216d..2d8b59472e8 100644
--- a/spec/features/groups_spec.rb
+++ b/spec/features/groups_spec.rb
@@ -1,14 +1,26 @@
require 'spec_helper'
feature 'Group', feature: true do
+ before do
+ login_as(:admin)
+ end
+
+ describe 'creating a group with space in group path' do
+ it 'renders new group form with validation errors' do
+ visit new_group_path
+ fill_in 'Group path', with: 'space group'
+
+ click_button 'Create group'
+
+ expect(current_path).to eq(groups_path)
+ expect(page).to have_content("Path can contain only letters, digits, '_', '-' and '.'. Cannot start with '-' or end in '.'.")
+ end
+ end
+
describe 'description' do
let(:group) { create(:group) }
let(:path) { group_path(group) }
- before do
- login_as(:admin)
- end
-
it 'parses Markdown' do
group.update_attribute(:description, 'This is **my** group')
visit path
diff --git a/spec/features/help_pages_spec.rb b/spec/features/help_pages_spec.rb
index 8c6b669ce78..e2101b333e2 100644
--- a/spec/features/help_pages_spec.rb
+++ b/spec/features/help_pages_spec.rb
@@ -5,8 +5,8 @@ describe 'Help Pages', feature: true do
before do
login_as :user
end
- it 'replace the variable $your_email with the email of the user' do
- visit help_page_path('ssh', 'README')
+ it 'replaces the variable $your_email with the email of the user' do
+ visit help_page_path('ssh/README')
expect(page).to have_content("ssh-keygen -t rsa -C \"#{@user.email}\"")
end
end
diff --git a/spec/features/issuables/default_sort_order_spec.rb b/spec/features/issuables/default_sort_order_spec.rb
new file mode 100644
index 00000000000..9a2b879e789
--- /dev/null
+++ b/spec/features/issuables/default_sort_order_spec.rb
@@ -0,0 +1,195 @@
+require 'spec_helper'
+
+describe 'Projects > Issuables > Default sort order', feature: true do
+ let(:project) { create(:empty_project, :public) }
+
+ let(:first_created_issuable) { issuables.order_created_asc.first }
+ let(:last_created_issuable) { issuables.order_created_desc.first }
+
+ let(:first_updated_issuable) { issuables.order_updated_asc.first }
+ let(:last_updated_issuable) { issuables.order_updated_desc.first }
+
+ context 'for merge requests' do
+ include MergeRequestHelpers
+
+ let!(:issuables) do
+ timestamps = [{ created_at: 3.minutes.ago, updated_at: 20.seconds.ago },
+ { created_at: 2.minutes.ago, updated_at: 30.seconds.ago },
+ { created_at: 4.minutes.ago, updated_at: 10.seconds.ago }]
+
+ timestamps.each_with_index do |ts, i|
+ create issuable_type, { title: "#{issuable_type}_#{i}",
+ source_branch: "#{issuable_type}_#{i}",
+ source_project: project }.merge(ts)
+ end
+
+ MergeRequest.all
+ end
+
+ context 'in the "merge requests" tab', js: true do
+ let(:issuable_type) { :merge_request }
+
+ it 'is "last created"' do
+ visit_merge_requests project
+
+ expect(first_merge_request).to include(last_created_issuable.title)
+ expect(last_merge_request).to include(first_created_issuable.title)
+ end
+ end
+
+ context 'in the "merge requests / open" tab', js: true do
+ let(:issuable_type) { :merge_request }
+
+ it 'is "last created"' do
+ visit_merge_requests_with_state(project, 'open')
+
+ expect(selected_sort_order).to eq('last created')
+ expect(first_merge_request).to include(last_created_issuable.title)
+ expect(last_merge_request).to include(first_created_issuable.title)
+ end
+ end
+
+ context 'in the "merge requests / merged" tab', js: true do
+ let(:issuable_type) { :merged_merge_request }
+
+ it 'is "last updated"' do
+ visit_merge_requests_with_state(project, 'merged')
+
+ expect(find('.issues-other-filters')).to have_content('Last updated')
+ expect(first_merge_request).to include(last_updated_issuable.title)
+ expect(last_merge_request).to include(first_updated_issuable.title)
+ end
+ end
+
+ context 'in the "merge requests / closed" tab', js: true do
+ let(:issuable_type) { :closed_merge_request }
+
+ it 'is "last updated"' do
+ visit_merge_requests_with_state(project, 'closed')
+
+ expect(find('.issues-other-filters')).to have_content('Last updated')
+ expect(first_merge_request).to include(last_updated_issuable.title)
+ expect(last_merge_request).to include(first_updated_issuable.title)
+ end
+ end
+
+ context 'in the "merge requests / all" tab', js: true do
+ let(:issuable_type) { :merge_request }
+
+ it 'is "last created"' do
+ visit_merge_requests_with_state(project, 'all')
+
+ expect(find('.issues-other-filters')).to have_content('Last created')
+ expect(first_merge_request).to include(last_created_issuable.title)
+ expect(last_merge_request).to include(first_created_issuable.title)
+ end
+ end
+ end
+
+ context 'for issues' do
+ include IssueHelpers
+
+ let!(:issuables) do
+ timestamps = [{ created_at: 3.minutes.ago, updated_at: 20.seconds.ago },
+ { created_at: 2.minutes.ago, updated_at: 30.seconds.ago },
+ { created_at: 4.minutes.ago, updated_at: 10.seconds.ago }]
+
+ timestamps.each_with_index do |ts, i|
+ create issuable_type, { title: "#{issuable_type}_#{i}",
+ project: project }.merge(ts)
+ end
+
+ Issue.all
+ end
+
+ context 'in the "issues" tab', js: true do
+ let(:issuable_type) { :issue }
+
+ it 'is "last created"' do
+ visit_issues project
+
+ expect(find('.issues-other-filters')).to have_content('Last created')
+ expect(first_issue).to include(last_created_issuable.title)
+ expect(last_issue).to include(first_created_issuable.title)
+ end
+ end
+
+ context 'in the "issues / open" tab', js: true do
+ let(:issuable_type) { :issue }
+
+ it 'is "last created"' do
+ visit_issues_with_state(project, 'open')
+
+ expect(find('.issues-other-filters')).to have_content('Last created')
+ expect(first_issue).to include(last_created_issuable.title)
+ expect(last_issue).to include(first_created_issuable.title)
+ end
+ end
+
+ context 'in the "issues / closed" tab', js: true do
+ let(:issuable_type) { :closed_issue }
+
+ it 'is "last updated"' do
+ visit_issues_with_state(project, 'closed')
+
+ expect(find('.issues-other-filters')).to have_content('Last updated')
+ expect(first_issue).to include(last_updated_issuable.title)
+ expect(last_issue).to include(first_updated_issuable.title)
+ end
+ end
+
+ context 'in the "issues / all" tab', js: true do
+ let(:issuable_type) { :issue }
+
+ it 'is "last created"' do
+ visit_issues_with_state(project, 'all')
+
+ expect(find('.issues-other-filters')).to have_content('Last created')
+ expect(first_issue).to include(last_created_issuable.title)
+ expect(last_issue).to include(first_created_issuable.title)
+ end
+ end
+
+ context 'when the sort in the URL is id_desc' do
+ let(:issuable_type) { :issue }
+
+ before { visit_issues(project, sort: 'id_desc') }
+
+ it 'shows the sort order as last created' do
+ expect(find('.issues-other-filters')).to have_content('Last created')
+ expect(first_issue).to include(last_created_issuable.title)
+ expect(last_issue).to include(first_created_issuable.title)
+ end
+ end
+
+ context 'when the sort in the URL is id_asc' do
+ let(:issuable_type) { :issue }
+
+ before { visit_issues(project, sort: 'id_asc') }
+
+ it 'shows the sort order as oldest created' do
+ expect(find('.issues-other-filters')).to have_content('Oldest created')
+ expect(first_issue).to include(first_created_issuable.title)
+ expect(last_issue).to include(last_created_issuable.title)
+ end
+ end
+ end
+
+ def selected_sort_order
+ find('.pull-right .dropdown button').text.downcase
+ end
+
+ def visit_merge_requests_with_state(project, state)
+ visit_merge_requests project
+ visit_issuables_with_state state
+ end
+
+ def visit_issues_with_state(project, state)
+ visit_issues project
+ visit_issuables_with_state state
+ end
+
+ def visit_issuables_with_state(state)
+ within('.issues-state-filters') { find("span", text: state.titleize).click }
+ end
+end
diff --git a/spec/features/issues/award_emoji_spec.rb b/spec/features/issues/award_emoji_spec.rb
index 07a854ea014..6eb04cf74c5 100644
--- a/spec/features/issues/award_emoji_spec.rb
+++ b/spec/features/issues/award_emoji_spec.rb
@@ -21,32 +21,32 @@ describe 'Awards Emoji', feature: true do
visit namespace_project_issue_path(project.namespace, project, issue)
end
- it 'should increment the thumbsdown emoji', js: true do
+ it 'increments the thumbsdown emoji', js: true do
find('[data-emoji="thumbsdown"]').click
sleep 2
expect(thumbsdown_emoji).to have_text("1")
end
context 'click the thumbsup emoji' do
- it 'should increment the thumbsup emoji', js: true do
+ it 'increments the thumbsup emoji', js: true do
find('[data-emoji="thumbsup"]').click
sleep 2
expect(thumbsup_emoji).to have_text("1")
end
- it 'should decrement the thumbsdown emoji', js: true do
+ it 'decrements the thumbsdown emoji', js: true do
expect(thumbsdown_emoji).to have_text("0")
end
end
context 'click the thumbsdown emoji' do
- it 'should increment the thumbsdown emoji', js: true do
+ it 'increments the thumbsdown emoji', js: true do
find('[data-emoji="thumbsdown"]').click
sleep 2
expect(thumbsdown_emoji).to have_text("1")
end
- it 'should decrement the thumbsup emoji', js: true do
+ it 'decrements the thumbsup emoji', js: true do
expect(thumbsup_emoji).to have_text("0")
end
end
diff --git a/spec/features/issues/award_spec.rb b/spec/features/issues/award_spec.rb
index 63efecf8780..401e1ea2b89 100644
--- a/spec/features/issues/award_spec.rb
+++ b/spec/features/issues/award_spec.rb
@@ -11,7 +11,7 @@ feature 'Issue awards', js: true, feature: true do
visit namespace_project_issue_path(project.namespace, project, issue)
end
- it 'should add award to issue' do
+ it 'adds 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'
@@ -20,7 +20,7 @@ feature 'Issue awards', js: true, feature: true do
expect(first('.js-emoji-btn')).to have_content '1'
end
- it 'should remove award from issue' do
+ it 'removes award from issue' do
first('.js-emoji-btn').click
find('.js-emoji-btn.active').click
expect(first('.js-emoji-btn')).to have_content '0'
@@ -29,7 +29,7 @@ feature 'Issue awards', js: true, feature: true do
expect(first('.js-emoji-btn')).to have_content '0'
end
- it 'should only have one menu on the page' do
+ it 'only has one menu on the page' do
first('.js-add-award').click
expect(page).to have_selector('.emoji-menu')
@@ -42,7 +42,7 @@ feature 'Issue awards', js: true, feature: true do
visit namespace_project_issue_path(project.namespace, project, issue)
end
- it 'should not see award menu button' do
+ it 'does not see award menu button' do
expect(page).not_to have_selector('.js-award-holder')
end
end
diff --git a/spec/features/issues/bulk_assignment_labels_spec.rb b/spec/features/issues/bulk_assignment_labels_spec.rb
index afc093cc1f5..bc2c087c9b9 100644
--- a/spec/features/issues/bulk_assignment_labels_spec.rb
+++ b/spec/features/issues/bulk_assignment_labels_spec.rb
@@ -175,7 +175,7 @@ feature 'Issues > Labels bulk assignment', feature: true do
visit namespace_project_issues_path(project.namespace, project)
end
- it 'labels are kept' do
+ it 'keeps labels' do
expect(find("#issue_#{issue1.id}")).to have_content 'bug'
expect(find("#issue_#{issue2.id}")).to have_content 'feature'
@@ -197,7 +197,7 @@ feature 'Issues > Labels bulk assignment', feature: true do
visit namespace_project_issues_path(project.namespace, project)
end
- it 'existing label is kept and new label is present' do
+ it 'keeps existing label and new label is present' do
expect(find("#issue_#{issue1.id}")).to have_content 'bug'
check 'check_all_issues'
@@ -222,7 +222,7 @@ feature 'Issues > Labels bulk assignment', feature: true do
visit namespace_project_issues_path(project.namespace, project)
end
- it 'existing label is kept and new label is present' do
+ it 'keeps existing label and new label is present' do
expect(find("#issue_#{issue1.id}")).to have_content 'bug'
expect(find("#issue_#{issue1.id}")).to have_content 'bug'
expect(find("#issue_#{issue2.id}")).to have_content 'feature'
@@ -252,7 +252,7 @@ feature 'Issues > Labels bulk assignment', feature: true do
visit namespace_project_issues_path(project.namespace, project)
end
- it 'labels are kept' do
+ it 'keeps labels' do
expect(find("#issue_#{issue1.id}")).to have_content 'bug'
expect(find("#issue_#{issue1.id}")).to have_content 'First Release'
expect(find("#issue_#{issue2.id}")).to have_content 'feature'
diff --git a/spec/features/issues/filter_by_labels_spec.rb b/spec/features/issues/filter_by_labels_spec.rb
index 5ea02b8d39c..908b18e5339 100644
--- a/spec/features/issues/filter_by_labels_spec.rb
+++ b/spec/features/issues/filter_by_labels_spec.rb
@@ -37,25 +37,25 @@ feature 'Issue filtering by Labels', feature: true do
wait_for_ajax
end
- it 'should show issue "Bugfix1" and "Bugfix2" in issues list' do
+ it 'shows issue "Bugfix1" and "Bugfix2" in issues list' do
expect(page).to have_content "Bugfix1"
expect(page).to have_content "Bugfix2"
end
- it 'should not show "Feature1" in issues list' do
+ it 'does not show "Feature1" in issues list' do
expect(page).not_to have_content "Feature1"
end
- it 'should show label "bug" in filtered-labels' do
+ it 'shows label "bug" in filtered-labels' do
expect(find('.filtered-labels')).to have_content "bug"
end
- it 'should not show label "feature" and "enhancement" in filtered-labels' do
+ it 'does not show label "feature" and "enhancement" in filtered-labels' 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
+ it 'removes label "bug"' do
find('.js-label-filter-remove').click
wait_for_ajax
expect(find('.filtered-labels', visible: false)).to have_no_content "bug"
@@ -71,20 +71,20 @@ feature 'Issue filtering by Labels', feature: true do
wait_for_ajax
end
- it 'should show issue "Feature1" in issues list' do
+ it 'shows issue "Feature1" in issues list' do
expect(page).to have_content "Feature1"
end
- it 'should not show "Bugfix1" and "Bugfix2" in issues list' do
+ it 'does not show "Bugfix1" and "Bugfix2" in issues list' do
expect(page).not_to have_content "Bugfix2"
expect(page).not_to have_content "Bugfix1"
end
- it 'should show label "feature" in filtered-labels' do
+ it 'shows label "feature" in filtered-labels' do
expect(find('.filtered-labels')).to have_content "feature"
end
- it 'should not show label "bug" and "enhancement" in filtered-labels' do
+ it 'does not show label "bug" and "enhancement" in filtered-labels' do
expect(find('.filtered-labels')).not_to have_content "bug"
expect(find('.filtered-labels')).not_to have_content "enhancement"
end
@@ -99,20 +99,20 @@ feature 'Issue filtering by Labels', feature: true do
wait_for_ajax
end
- it 'should show issue "Bugfix2" in issues list' do
+ it 'shows issue "Bugfix2" in issues list' do
expect(page).to have_content "Bugfix2"
end
- it 'should not show "Feature1" and "Bugfix1" in issues list' do
+ it 'does not show "Feature1" and "Bugfix1" in issues list' do
expect(page).not_to have_content "Feature1"
expect(page).not_to have_content "Bugfix1"
end
- it 'should show label "enhancement" in filtered-labels' do
+ it 'shows label "enhancement" in filtered-labels' do
expect(find('.filtered-labels')).to have_content "enhancement"
end
- it 'should not show label "feature" and "bug" in filtered-labels' do
+ it 'does not show label "feature" and "bug" in filtered-labels' do
expect(find('.filtered-labels')).not_to have_content "bug"
expect(find('.filtered-labels')).not_to have_content "feature"
end
@@ -128,21 +128,21 @@ feature 'Issue filtering by Labels', feature: true do
wait_for_ajax
end
- it 'should not show "Bugfix1" or "Feature1" in issues list' do
+ it 'does not show "Bugfix1" or "Feature1" in issues list' do
expect(page).not_to have_content "Bugfix1"
expect(page).not_to have_content "Feature1"
end
- it 'should show label "enhancement" and "feature" in filtered-labels' do
+ it 'shows label "enhancement" and "feature" in filtered-labels' do
expect(find('.filtered-labels')).to have_content "enhancement"
expect(find('.filtered-labels')).to have_content "feature"
end
- it 'should not show label "bug" in filtered-labels' do
+ it 'does not show label "bug" in filtered-labels' do
expect(find('.filtered-labels')).not_to have_content "bug"
end
- it 'should remove label "enhancement"' do
+ it 'removes label "enhancement"' do
find('.js-label-filter-remove', match: :first).click
wait_for_ajax
expect(find('.filtered-labels')).to have_no_content "enhancement"
@@ -159,20 +159,20 @@ feature 'Issue filtering by Labels', feature: true do
wait_for_ajax
end
- it 'should show issue "Bugfix2" in issues list' do
+ it 'shows issue "Bugfix2" in issues list' do
expect(page).to have_content "Bugfix2"
end
- it 'should not show "Feature1"' do
+ it 'does not show "Feature1"' do
expect(page).not_to have_content "Feature1"
end
- it 'should show label "bug" and "enhancement" in filtered-labels' do
+ it 'shows label "bug" and "enhancement" in filtered-labels' do
expect(find('.filtered-labels')).to have_content "bug"
expect(find('.filtered-labels')).to have_content "enhancement"
end
- it 'should not show label "feature" in filtered-labels' do
+ it 'does not show label "feature" in filtered-labels' do
expect(find('.filtered-labels')).not_to have_content "feature"
end
end
@@ -191,7 +191,7 @@ feature 'Issue filtering by Labels', feature: true do
end
end
- it 'should allow user to remove filtered labels' do
+ it 'allows user to remove filtered labels' do
first('.js-label-filter-remove').click
wait_for_ajax
@@ -201,11 +201,11 @@ feature 'Issue filtering by Labels', feature: true do
end
context 'dropdown filtering', js: true do
- it 'should filter by label name' do
+ it 'filters by label name' do
page.within '.labels-filter' do
click_button 'Label'
wait_for_ajax
- fill_in 'label-name', with: 'bug'
+ find('.dropdown-input input').set 'bug'
page.within '.dropdown-content' do
expect(page).not_to have_content 'enhancement'
diff --git a/spec/features/issues/filter_by_milestone_spec.rb b/spec/features/issues/filter_by_milestone_spec.rb
index 99445185893..485dc560061 100644
--- a/spec/features/issues/filter_by_milestone_spec.rb
+++ b/spec/features/issues/filter_by_milestone_spec.rb
@@ -15,7 +15,7 @@ feature 'Issue filtering by Milestone', feature: true do
end
context 'filters by upcoming milestone', js: true do
- it 'should not show issues with no expiry' do
+ it 'does not show issues with no expiry' do
create(:issue, project: project)
create(:issue, project: project, milestone: milestone)
@@ -25,7 +25,7 @@ feature 'Issue filtering by Milestone', feature: true do
expect(page).to have_css('.issue', count: 0)
end
- it 'should show issues in future' do
+ it 'shows issues in future' do
milestone = create(:milestone, project: project, due_date: Date.tomorrow)
create(:issue, project: project)
create(:issue, project: project, milestone: milestone)
@@ -36,7 +36,7 @@ feature 'Issue filtering by Milestone', feature: true do
expect(page).to have_css('.issue', count: 1)
end
- it 'should not show issues in past' do
+ it 'does not show issues in past' do
milestone = create(:milestone, project: project, due_date: Date.yesterday)
create(:issue, project: project)
create(:issue, project: project, milestone: milestone)
diff --git a/spec/features/issues/filter_issues_spec.rb b/spec/features/issues/filter_issues_spec.rb
index 4b9b5394b61..e262f285868 100644
--- a/spec/features/issues/filter_issues_spec.rb
+++ b/spec/features/issues/filter_issues_spec.rb
@@ -26,17 +26,17 @@ describe 'Filter issues', feature: true do
end
context 'assignee', js: true do
- it 'should update to current user' do
+ it 'updates to current user' do
expect(find('.js-assignee-search .dropdown-toggle-text')).to have_content(user.name)
end
- it 'should not change when closed link is clicked' do
+ it 'does not change when closed link is clicked' do
find('.issues-state-filters a', text: "Closed").click
expect(find('.js-assignee-search .dropdown-toggle-text')).to have_content(user.name)
end
- it 'should not change when all link is clicked' do
+ it 'does not change when all link is clicked' do
find('.issues-state-filters a', text: "All").click
expect(find('.js-assignee-search .dropdown-toggle-text')).to have_content(user.name)
@@ -56,17 +56,17 @@ describe 'Filter issues', feature: true do
end
context 'milestone', js: true do
- it 'should update to current milestone' do
+ it 'updates to current milestone' do
expect(find('.js-milestone-select .dropdown-toggle-text')).to have_content(milestone.title)
end
- it 'should not change when closed link is clicked' do
+ it 'does not change when closed link is clicked' do
find('.issues-state-filters a', text: "Closed").click
expect(find('.js-milestone-select .dropdown-toggle-text')).to have_content(milestone.title)
end
- it 'should not change when all link is clicked' do
+ it 'does not change when all link is clicked' do
find('.issues-state-filters a', text: "All").click
expect(find('.js-milestone-select .dropdown-toggle-text')).to have_content(milestone.title)
@@ -81,7 +81,7 @@ describe 'Filter issues', feature: true do
wait_for_ajax
end
- it 'should filter by any label' do
+ it 'filters by any label' do
find('.dropdown-menu-labels a', text: 'Any Label').click
page.first('.labels-filter .dropdown-title .dropdown-menu-close-icon').click
wait_for_ajax
@@ -89,7 +89,7 @@ describe 'Filter issues', feature: true do
expect(find('.labels-filter')).to have_content 'Label'
end
- it 'should filter by no label' do
+ it 'filters by no label' do
find('.dropdown-menu-labels a', text: 'No Label').click
page.first('.labels-filter .dropdown-title .dropdown-menu-close-icon').click
wait_for_ajax
@@ -100,7 +100,7 @@ describe 'Filter issues', feature: true do
expect(find('.js-label-select .dropdown-toggle-text')).to have_content('No Label')
end
- it 'should filter by no label' do
+ it 'filters by no label' do
find('.dropdown-menu-labels a', text: label.title).click
page.within '.labels-filter' do
expect(page).to have_content label.title
@@ -117,7 +117,7 @@ describe 'Filter issues', feature: true do
find('.dropdown-menu-user-link', text: user.username).click
- wait_for_ajax
+ expect(page).not_to have_selector('.issues-list .issue')
find('.js-label-select').click
@@ -128,19 +128,19 @@ describe 'Filter issues', feature: true do
end
context 'assignee and label', js: true do
- it 'should update to current assignee and label' do
+ it 'updates to current assignee and label' do
expect(find('.js-assignee-search .dropdown-toggle-text')).to have_content(user.name)
expect(find('.js-label-select .dropdown-toggle-text')).to have_content(label.title)
end
- it 'should not change when closed link is clicked' do
+ it 'does not change when closed link is clicked' do
find('.issues-state-filters a', text: "Closed").click
expect(find('.js-assignee-search .dropdown-toggle-text')).to have_content(user.name)
expect(find('.js-label-select .dropdown-toggle-text')).to have_content(label.title)
end
- it 'should not change when all link is clicked' do
+ it 'does not change when all link is clicked' do
find('.issues-state-filters a', text: "All").click
expect(find('.js-assignee-search .dropdown-toggle-text')).to have_content(user.name)
@@ -168,7 +168,7 @@ describe 'Filter issues', feature: true do
end
context 'only text', js: true do
- it 'should filter issues by searched text' do
+ it 'filters issues by searched text' do
fill_in 'issue_search', with: 'Bug'
page.within '.issues-list' do
@@ -176,7 +176,7 @@ describe 'Filter issues', feature: true do
end
end
- it 'should not show any issues' do
+ it 'does not show any issues' do
fill_in 'issue_search', with: 'testing'
page.within '.issues-list' do
@@ -186,7 +186,7 @@ describe 'Filter issues', feature: true do
end
context 'text and dropdown options', js: true do
- it 'should filter by text and label' do
+ it 'filters by text and label' do
fill_in 'issue_search', with: 'Bug'
page.within '.issues-list' do
@@ -204,7 +204,7 @@ describe 'Filter issues', feature: true do
end
end
- it 'should filter by text and milestone' do
+ it 'filters by text and milestone' do
fill_in 'issue_search', with: 'Bug'
page.within '.issues-list' do
@@ -221,7 +221,7 @@ describe 'Filter issues', feature: true do
end
end
- it 'should filter by text and assignee' do
+ it 'filters by text and assignee' do
fill_in 'issue_search', with: 'Bug'
page.within '.issues-list' do
@@ -238,7 +238,7 @@ describe 'Filter issues', feature: true do
end
end
- it 'should filter by text and author' do
+ it 'filters by text and author' do
fill_in 'issue_search', with: 'Bug'
page.within '.issues-list' do
@@ -269,7 +269,7 @@ describe 'Filter issues', feature: true do
visit namespace_project_issues_path(project.namespace, project)
end
- it 'should be able to filter and sort issues' do
+ it 'is able to filter and sort issues' do
click_button 'Label'
wait_for_ajax
page.within '.labels-filter' do
diff --git a/spec/features/issues/issue_sidebar_spec.rb b/spec/features/issues/issue_sidebar_spec.rb
index 5739bc64dfb..4b1aec8bf71 100644
--- a/spec/features/issues/issue_sidebar_spec.rb
+++ b/spec/features/issues/issue_sidebar_spec.rb
@@ -17,7 +17,7 @@ feature 'Issue Sidebar', feature: true do
end
describe 'when clicking on edit labels', js: true do
- it 'dropdown has an option to create a new label' do
+ it 'shows dropdown option to create a new label' do
find('.block.labels .edit-link').click
page.within('.block.labels') do
@@ -27,7 +27,7 @@ feature 'Issue Sidebar', feature: true do
end
context 'creating a new label', js: true do
- it 'option to crate a new label is present' do
+ it 'shows option to crate a new label is present' do
page.within('.block.labels') do
find('.edit-link').click
@@ -35,7 +35,7 @@ feature 'Issue Sidebar', feature: true do
end
end
- it 'dropdown switches to "create label" section' do
+ it 'shows dropdown switches to "create label" section' do
page.within('.block.labels') do
find('.edit-link').click
click_link 'Create new'
@@ -44,7 +44,7 @@ feature 'Issue Sidebar', feature: true do
end
end
- it 'new label is added' do
+ it 'adds new label' do
page.within('.block.labels') do
find('.edit-link').click
sleep 1
diff --git a/spec/features/issues/new_branch_button_spec.rb b/spec/features/issues/new_branch_button_spec.rb
index 16e188d2a8a..fb0c4704285 100644
--- a/spec/features/issues/new_branch_button_spec.rb
+++ b/spec/features/issues/new_branch_button_spec.rb
@@ -20,7 +20,7 @@ feature 'Start new branch from an issue', feature: true do
context "when there is a referenced merge request" do
let(:note) do
create(:note, :on_issue, :system, project: project,
- note: "mentioned in !#{referenced_mr.iid}")
+ note: "Mentioned in !#{referenced_mr.iid}")
end
let(:referenced_mr) do
create(:merge_request, :simple, source_project: project, target_project: project,
@@ -41,7 +41,7 @@ feature 'Start new branch from an issue', feature: true do
end
context "for visiters" do
- it 'no button is shown', js: true do
+ it 'shows no buttons', js: true do
visit namespace_project_issue_path(project.namespace, project, issue)
expect(page).not_to have_css('#new-branch')
diff --git a/spec/features/issues/todo_spec.rb b/spec/features/issues/todo_spec.rb
index bc0f437a8ce..de8fdda388d 100644
--- a/spec/features/issues/todo_spec.rb
+++ b/spec/features/issues/todo_spec.rb
@@ -11,7 +11,7 @@ feature 'Manually create a todo item from issue', feature: true, js: true do
visit namespace_project_issue_path(project.namespace, project, issue)
end
- it 'should create todo when clicking button' do
+ it 'creates todo when clicking button' do
page.within '.issuable-sidebar' do
click_button 'Add Todo'
expect(page).to have_content 'Mark Done'
@@ -28,7 +28,7 @@ feature 'Manually create a todo item from issue', feature: true, js: true do
end
end
- it 'should mark a todo as done' do
+ it 'marks a todo as done' do
page.within '.issuable-sidebar' do
click_button 'Add Todo'
click_button 'Mark Done'
diff --git a/spec/features/issues/update_issues_spec.rb b/spec/features/issues/update_issues_spec.rb
index ddbd69b2891..ae5da3877a8 100644
--- a/spec/features/issues/update_issues_spec.rb
+++ b/spec/features/issues/update_issues_spec.rb
@@ -13,7 +13,7 @@ feature 'Multiple issue updating from issues#index', feature: true do
end
context 'status', js: true do
- it 'should be set to closed' do
+ it 'sets to closed' do
visit namespace_project_issues_path(project.namespace, project)
find('#check_all_issues').click
@@ -24,7 +24,7 @@ feature 'Multiple issue updating from issues#index', feature: true do
expect(page).to have_selector('.issue', count: 0)
end
- it 'should be set to open' do
+ it 'sets to open' do
create_closed
visit namespace_project_issues_path(project.namespace, project, state: 'closed')
@@ -38,7 +38,7 @@ feature 'Multiple issue updating from issues#index', feature: true do
end
context 'assignee', js: true do
- it 'should update to current user' do
+ it 'updates to current user' do
visit namespace_project_issues_path(project.namespace, project)
find('#check_all_issues').click
@@ -52,7 +52,7 @@ feature 'Multiple issue updating from issues#index', feature: true do
end
end
- it 'should update to unassigned' do
+ it 'updates to unassigned' do
create_assigned
visit namespace_project_issues_path(project.namespace, project)
@@ -68,7 +68,7 @@ feature 'Multiple issue updating from issues#index', feature: true do
context 'milestone', js: true do
let(:milestone) { create(:milestone, project: project) }
- it 'should update milestone' do
+ it 'updates milestone' do
visit namespace_project_issues_path(project.namespace, project)
find('#check_all_issues').click
@@ -80,7 +80,7 @@ feature 'Multiple issue updating from issues#index', feature: true do
expect(find('.issue')).to have_content milestone.title
end
- it 'should set to no milestone' do
+ it 'sets to no milestone' do
create_with_milestone
visit namespace_project_issues_path(project.namespace, project)
diff --git a/spec/features/issues/user_uses_slash_commands_spec.rb b/spec/features/issues/user_uses_slash_commands_spec.rb
new file mode 100644
index 00000000000..2883e392694
--- /dev/null
+++ b/spec/features/issues/user_uses_slash_commands_spec.rb
@@ -0,0 +1,58 @@
+require 'rails_helper'
+
+feature 'Issues > User uses slash commands', feature: true, js: true do
+ include WaitForAjax
+
+ it_behaves_like 'issuable record that supports slash commands in its description and notes', :issue do
+ let(:issuable) { create(:issue, project: project) }
+ end
+
+ describe 'issue-only commands' do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :public) }
+
+ before do
+ project.team << [user, :master]
+ login_with(user)
+ visit namespace_project_issue_path(project.namespace, project, issue)
+ end
+
+ describe 'adding a due date from note' do
+ let(:issue) { create(:issue, project: project) }
+
+ it 'does not create a note, and sets the due date accordingly' do
+ page.within('.js-main-target-form') do
+ fill_in 'note[note]', with: "/due 2016-08-28"
+ click_button 'Comment'
+ end
+
+ expect(page).not_to have_content '/due 2016-08-28'
+ expect(page).to have_content 'Your commands have been executed!'
+
+ issue.reload
+
+ expect(issue.due_date).to eq Date.new(2016, 8, 28)
+ end
+ end
+
+ describe 'removing a due date from note' do
+ let(:issue) { create(:issue, project: project, due_date: Date.new(2016, 8, 28)) }
+
+ it 'does not create a note, and removes the due date accordingly' do
+ expect(issue.due_date).to eq Date.new(2016, 8, 28)
+
+ page.within('.js-main-target-form') do
+ fill_in 'note[note]', with: "/remove_due_date"
+ click_button 'Comment'
+ end
+
+ expect(page).not_to have_content '/remove_due_date'
+ expect(page).to have_content 'Your commands have been executed!'
+
+ issue.reload
+
+ expect(issue.due_date).to be_nil
+ end
+ end
+ end
+end
diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb
index d51c9abea19..d744d0eb6af 100644
--- a/spec/features/issues_spec.rb
+++ b/spec/features/issues_spec.rb
@@ -1,6 +1,7 @@
require 'spec_helper'
describe 'Issues', feature: true do
+ include IssueHelpers
include SortingHelper
let(:project) { create(:project) }
@@ -25,7 +26,7 @@ describe 'Issues', feature: true do
find('.js-zen-enter').click
end
- it 'should open new issue popup' do
+ it 'opens new issue popup' do
expect(page).to have_content("Issue ##{issue.iid}")
end
@@ -70,7 +71,7 @@ describe 'Issues', feature: true do
visit new_namespace_project_issue_path(project.namespace, project)
end
- it 'should save with due date' do
+ it 'saves with due date' do
date = Date.today.at_beginning_of_month
fill_in 'issue_title', with: 'bug 345'
@@ -98,7 +99,7 @@ describe 'Issues', feature: true do
visit edit_namespace_project_issue_path(project.namespace, project, issue)
end
- it 'should save with due date' do
+ it 'saves with due date' do
date = Date.today.at_beginning_of_month
expect(find('#issuable-due-date').value).to eq date.to_s
@@ -121,6 +122,17 @@ describe 'Issues', feature: true do
expect(page).to have_content date.to_s(:medium)
end
end
+
+ it 'warns about version conflict' do
+ issue.update(title: "New title")
+
+ fill_in 'issue_title', with: 'bug 345'
+ fill_in 'issue_description', with: 'bug description'
+
+ click_button 'Save changes'
+
+ expect(page).to have_content 'Someone edited the issue the same time you did'
+ end
end
end
@@ -154,7 +166,7 @@ describe 'Issues', feature: true do
let(:issue) { @issue }
- it 'should allow filtering by issues with no specified assignee' do
+ it 'allows filtering by issues with no specified assignee' do
visit namespace_project_issues_path(project.namespace, project, assignee_id: IssuableFinder::NONE)
expect(page).to have_content 'foobar'
@@ -162,7 +174,7 @@ describe 'Issues', feature: true do
expect(page).not_to have_content 'gitlab'
end
- it 'should allow filtering by a specified assignee' do
+ it 'allows filtering by a specified assignee' do
visit namespace_project_issues_path(project.namespace, project, assignee_id: @user.id)
expect(page).not_to have_content 'foobar'
@@ -186,15 +198,15 @@ describe 'Issues', feature: true do
it 'sorts by newest' do
visit namespace_project_issues_path(project.namespace, project, sort: sort_value_recently_created)
- expect(first_issue).to include('baz')
- expect(last_issue).to include('foo')
+ expect(first_issue).to include('foo')
+ expect(last_issue).to include('baz')
end
it 'sorts by oldest' do
visit namespace_project_issues_path(project.namespace, project, sort: sort_value_oldest_created)
- expect(first_issue).to include('foo')
- expect(last_issue).to include('baz')
+ expect(first_issue).to include('baz')
+ expect(last_issue).to include('foo')
end
it 'sorts by most recently updated' do
@@ -350,8 +362,8 @@ describe 'Issues', feature: true do
sort: sort_value_oldest_created,
assignee_id: user2.id)
- expect(first_issue).to include('foo')
- expect(last_issue).to include('bar')
+ expect(first_issue).to include('bar')
+ expect(last_issue).to include('foo')
expect(page).not_to have_content 'baz'
end
end
@@ -513,7 +525,7 @@ describe 'Issues', feature: true do
visit new_namespace_project_issue_path(project.namespace, project)
end
- it 'should upload file when dragging into textarea' do
+ it 'uploads file when dragging into textarea' do
drop_in_dropzone test_image_file
# Wait for the file to upload
@@ -524,6 +536,35 @@ describe 'Issues', feature: true do
end
end
+ xdescribe 'new issue by email' do
+ shared_examples 'show the email in the modal' do
+ before do
+ stub_incoming_email_setting(enabled: true, address: "p+%{key}@gl.ab")
+
+ visit namespace_project_issues_path(project.namespace, project)
+ click_button('Email a new issue')
+ end
+
+ it 'click the button to show modal for the new email' do
+ page.within '#issue-email-modal' do
+ email = project.new_issue_address(@user)
+
+ expect(page).to have_selector("input[value='#{email}']")
+ end
+ end
+ end
+
+ context 'with existing issues' do
+ let!(:issue) { create(:issue, project: project, author: @user) }
+
+ it_behaves_like 'show the email in the modal'
+ end
+
+ context 'without existing issues' do
+ it_behaves_like 'show the email in the modal'
+ 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) }
@@ -532,7 +573,7 @@ describe 'Issues', feature: true do
visit namespace_project_issue_path(project.namespace, project, issue)
end
- it 'should add due date to issue' do
+ it 'adds due date to issue' do
page.within '.due_date' do
click_link 'Edit'
@@ -544,7 +585,7 @@ describe 'Issues', feature: true do
end
end
- it 'should remove due date from issue' do
+ it 'removes due date from issue' do
page.within '.due_date' do
click_link 'Edit'
@@ -561,14 +602,6 @@ describe 'Issues', feature: true do
end
end
- def first_issue
- page.all('ul.issues-list > li').first.text
- end
-
- def last_issue
- page.all('ul.issues-list > li').last.text
- end
-
def drop_in_dropzone(file_path)
# Generate a fake input selector
page.execute_script <<-JS
diff --git a/spec/features/login_spec.rb b/spec/features/login_spec.rb
index 72b5ff231f7..2523b4b7898 100644
--- a/spec/features/login_spec.rb
+++ b/spec/features/login_spec.rb
@@ -28,6 +28,11 @@ feature 'Login', feature: true do
end
describe 'with two-factor authentication' do
+ def enter_code(code)
+ fill_in 'Two-Factor Authentication code', with: code
+ click_button 'Verify code'
+ end
+
context 'with valid username/password' do
let(:user) { create(:user, :two_factor) }
@@ -36,11 +41,6 @@ feature 'Login', feature: true do
expect(page).to have_content('Two-Factor Authentication')
end
- def enter_code(code)
- fill_in 'Two-Factor Authentication code', with: code
- click_button 'Verify code'
- end
-
it 'does not show a "You are already signed in." error message' do
enter_code(user.current_otp)
expect(page).not_to have_content('You are already signed in.')
@@ -108,6 +108,39 @@ feature 'Login', feature: true do
end
end
end
+
+ context 'logging in via OAuth' do
+ def saml_config
+ OpenStruct.new(name: 'saml', label: 'saml', args: {
+ assertion_consumer_service_url: 'https://localhost:3443/users/auth/saml/callback',
+ idp_cert_fingerprint: '26:43:2C:47:AF:F0:6B:D0:07:9C:AD:A3:74:FE:5D:94:5F:4E:9E:52',
+ idp_sso_target_url: 'https://idp.example.com/sso/saml',
+ issuer: 'https://localhost:3443/',
+ name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient'
+ })
+ end
+
+ def stub_omniauth_config(messages)
+ Rails.application.env_config['devise.mapping'] = Devise.mappings[:user]
+ Rails.application.routes.disable_clear_and_finalize = true
+ Rails.application.routes.draw do
+ post '/users/auth/saml' => 'omniauth_callbacks#saml'
+ end
+ allow(Gitlab::OAuth::Provider).to receive_messages(providers: [:saml], config_for: saml_config)
+ allow(Gitlab.config.omniauth).to receive_messages(messages)
+ expect_any_instance_of(Object).to receive(:omniauth_authorize_path).with(:user, "saml").and_return('/users/auth/saml')
+ end
+
+ it 'shows 2FA prompt after OAuth login' do
+ stub_omniauth_config(enabled: true, auto_link_saml_user: true, allow_single_sign_on: ['saml'], providers: [saml_config])
+ user = create(:omniauth_user, :two_factor, extern_uid: 'my-uid', provider: 'saml')
+ login_via('saml', user, 'my-uid')
+
+ expect(page).to have_content('Two-Factor Authentication')
+ enter_code(user.current_otp)
+ expect(current_path).to eq root_path
+ end
+ end
end
describe 'without two-factor authentication' do
diff --git a/spec/features/markdown_spec.rb b/spec/features/markdown_spec.rb
index 09ccc77c101..32159559c37 100644
--- a/spec/features/markdown_spec.rb
+++ b/spec/features/markdown_spec.rb
@@ -236,6 +236,14 @@ describe 'GitLab Markdown', feature: true do
it 'includes TaskListFilter' do
expect(doc).to parse_task_lists
end
+
+ it 'includes InlineDiffFilter' do
+ expect(doc).to parse_inline_diffs
+ end
+
+ it 'includes VideoLinkFilter' do
+ expect(doc).to parse_video_links
+ end
end
context 'wiki pipeline' do
@@ -293,6 +301,10 @@ describe 'GitLab Markdown', feature: true do
it 'includes InlineDiffFilter' do
expect(doc).to parse_inline_diffs
end
+
+ it 'includes VideoLinkFilter' do
+ expect(doc).to parse_video_links
+ 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
index 007f67d6080..ac260e118d0 100644
--- a/spec/features/merge_requests/award_spec.rb
+++ b/spec/features/merge_requests/award_spec.rb
@@ -11,7 +11,7 @@ feature 'Merge request awards', js: true, feature: true do
visit namespace_project_merge_request_path(project.namespace, project, merge_request)
end
- it 'should add award to merge request' do
+ it 'adds 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'
@@ -20,7 +20,7 @@ feature 'Merge request awards', js: true, feature: true do
expect(first('.js-emoji-btn')).to have_content '1'
end
- it 'should remove award from merge request' do
+ it 'removes 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'
@@ -29,7 +29,7 @@ feature 'Merge request awards', js: true, feature: true do
expect(first('.js-emoji-btn')).to have_content '0'
end
- it 'should only have one menu on the page' do
+ it 'has only one menu on the page' do
first('.js-add-award').click
expect(page).to have_selector('.emoji-menu')
@@ -42,7 +42,7 @@ feature 'Merge request awards', js: true, feature: true do
visit namespace_project_merge_request_path(project.namespace, project, merge_request)
end
- it 'should not see award menu button' do
+ it 'does not see award menu button' do
expect(page).not_to have_selector('.js-award-holder')
end
end
diff --git a/spec/features/merge_requests/conflicts_spec.rb b/spec/features/merge_requests/conflicts_spec.rb
new file mode 100644
index 00000000000..759edf8ec80
--- /dev/null
+++ b/spec/features/merge_requests/conflicts_spec.rb
@@ -0,0 +1,73 @@
+require 'spec_helper'
+
+feature 'Merge request conflict resolution', js: true, feature: true do
+ include WaitForAjax
+
+ let(:user) { create(:user) }
+ let(:project) { create(:project) }
+
+ def create_merge_request(source_branch)
+ create(:merge_request, source_branch: source_branch, target_branch: 'conflict-start', source_project: project) do |mr|
+ mr.mark_as_unmergeable
+ end
+ end
+
+ context 'when a merge request can be resolved in the UI' do
+ let(:merge_request) { create_merge_request('conflict-resolvable') }
+
+ before do
+ project.team << [user, :developer]
+ login_as(user)
+
+ visit namespace_project_merge_request_path(project.namespace, project, merge_request)
+ end
+
+ it 'shows a link to the conflict resolution page' do
+ expect(page).to have_link('conflicts', href: /\/conflicts\Z/)
+ end
+
+ context 'visiting the conflicts resolution page' do
+ before { click_link('conflicts', href: /\/conflicts\Z/) }
+
+ it 'shows the conflicts' do
+ begin
+ expect(find('#conflicts')).to have_content('popen.rb')
+ rescue Capybara::Poltergeist::JavascriptError
+ retry
+ end
+ end
+ end
+ end
+
+ UNRESOLVABLE_CONFLICTS = {
+ 'conflict-too-large' => 'when the conflicts contain a large file',
+ 'conflict-binary-file' => 'when the conflicts contain a binary file',
+ 'conflict-contains-conflict-markers' => 'when the conflicts contain a file with ambiguous conflict markers',
+ 'conflict-missing-side' => 'when the conflicts contain a file edited in one branch and deleted in another',
+ 'conflict-non-utf8' => 'when the conflicts contain a non-UTF-8 file',
+ }
+
+ UNRESOLVABLE_CONFLICTS.each do |source_branch, description|
+ context description do
+ let(:merge_request) { create_merge_request(source_branch) }
+
+ before do
+ project.team << [user, :developer]
+ login_as(user)
+
+ visit namespace_project_merge_request_path(project.namespace, project, merge_request)
+ end
+
+ it 'does not show a link to the conflict resolution page' do
+ expect(page).not_to have_link('conflicts', href: /\/conflicts\Z/)
+ end
+
+ it 'shows an error if the conflicts page is visited directly' do
+ visit current_url + '/conflicts'
+ wait_for_ajax
+
+ expect(find('#conflicts')).to have_content('Please try to resolve them locally.')
+ end
+ end
+ end
+end
diff --git a/spec/features/merge_requests/create_new_mr_spec.rb b/spec/features/merge_requests/create_new_mr_spec.rb
index e296078bad8..b963d1305b5 100644
--- a/spec/features/merge_requests/create_new_mr_spec.rb
+++ b/spec/features/merge_requests/create_new_mr_spec.rb
@@ -8,11 +8,14 @@ feature 'Create New Merge Request', feature: true, js: true do
project.team << [user, :master]
login_as user
- visit namespace_project_merge_requests_path(project.namespace, project)
end
it 'generates a diff for an orphaned branch' do
+ visit namespace_project_merge_requests_path(project.namespace, project)
+
click_link 'New Merge Request'
+ expect(page).to have_content('Source branch')
+ expect(page).to have_content('Target branch')
first('.js-source-branch').click
first('.dropdown-source-branch .dropdown-content a', text: 'orphaned-branch').click
@@ -40,4 +43,20 @@ feature 'Create New Merge Request', feature: true, js: true do
expect(page).not_to have_content private_project.to_reference
end
end
+
+ it 'allows to change the diff view' do
+ visit new_namespace_project_merge_request_path(project.namespace, project, merge_request: { target_branch: 'master', source_branch: 'fix' })
+
+ click_link 'Changes'
+
+ expect(page).to have_css('a.btn.active', text: 'Inline')
+ expect(page).not_to have_css('a.btn.active', text: 'Side-by-side')
+
+ click_link 'Side-by-side'
+
+ within '.merge-request' do
+ expect(page).not_to have_css('a.btn.active', text: 'Inline')
+ expect(page).to have_css('a.btn.active', text: 'Side-by-side')
+ 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
index f676200ecf3..4d5d4aa121a 100644
--- a/spec/features/merge_requests/created_from_fork_spec.rb
+++ b/spec/features/merge_requests/created_from_fork_spec.rb
@@ -29,12 +29,16 @@ feature 'Merge request created from fork' do
include WaitForAjax
given(:pipeline) do
- create(:ci_pipeline_with_two_job, project: fork_project,
- sha: merge_request.diff_head_sha,
- ref: merge_request.source_branch)
+ create(:ci_pipeline,
+ project: fork_project,
+ sha: merge_request.diff_head_sha,
+ ref: merge_request.source_branch)
end
- background { pipeline.create_builds(user) }
+ background do
+ create(:ci_build, pipeline: pipeline, name: 'rspec')
+ create(:ci_build, pipeline: pipeline, name: 'spinach')
+ end
scenario 'user visits a pipelines page', js: true do
visit_merge_request(merge_request)
diff --git a/spec/features/merge_requests/diff_notes_resolve_spec.rb b/spec/features/merge_requests/diff_notes_resolve_spec.rb
new file mode 100644
index 00000000000..c6adf7e4c56
--- /dev/null
+++ b/spec/features/merge_requests/diff_notes_resolve_spec.rb
@@ -0,0 +1,497 @@
+require 'spec_helper'
+
+feature 'Diff notes resolve', feature: true, js: true do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :public) }
+ let(:merge_request) { create(:merge_request_with_diffs, source_project: project, author: user, title: "Bug NS-04") }
+ let!(:note) { create(:diff_note_on_merge_request, project: project, noteable: merge_request) }
+ let(:path) { "files/ruby/popen.rb" }
+ let(:position) do
+ Gitlab::Diff::Position.new(
+ old_path: path,
+ new_path: path,
+ old_line: nil,
+ new_line: 9,
+ diff_refs: merge_request.diff_refs
+ )
+ end
+
+ context 'no discussions' do
+ before do
+ project.team << [user, :master]
+ login_as user
+ note.destroy
+ visit_merge_request
+ end
+
+ it 'displays no discussion resolved data' do
+ expect(page).not_to have_content('discussion resolved')
+ expect(page).not_to have_selector('.discussion-next-btn')
+ end
+ end
+
+ context 'as authorized user' do
+ before do
+ project.team << [user, :master]
+ login_as user
+ visit_merge_request
+ end
+
+ context 'single discussion' do
+ it 'shows text with how many discussions' do
+ page.within '.line-resolve-all-container' do
+ expect(page).to have_content('0/1 discussion resolved')
+ end
+ end
+
+ it 'allows user to mark a note as resolved' do
+ page.within '.diff-content .note' do
+ find('.line-resolve-btn').click
+
+ expect(page).to have_selector('.line-resolve-btn.is-active')
+ expect(find('.line-resolve-btn')['data-original-title']).to eq("Resolved by #{user.name}")
+ end
+
+ page.within '.diff-content' do
+ expect(page).to have_selector('.btn', text: 'Unresolve discussion')
+ end
+
+ page.within '.line-resolve-all-container' do
+ expect(page).to have_content('1/1 discussion resolved')
+ expect(page).to have_selector('.line-resolve-btn.is-active')
+ end
+ end
+
+ it 'allows user to mark discussion as resolved' do
+ page.within '.diff-content' do
+ click_button 'Resolve discussion'
+ end
+
+ page.within '.diff-content .note' do
+ expect(page).to have_selector('.line-resolve-btn.is-active')
+
+ expect(find('.line-resolve-btn')['data-original-title']).to eq("Resolved by #{user.name}")
+ end
+
+ page.within '.line-resolve-all-container' do
+ expect(page).to have_content('1/1 discussion resolved')
+ expect(page).to have_selector('.line-resolve-btn.is-active')
+ end
+ end
+
+ it 'allows user to unresolve discussion' do
+ page.within '.diff-content' do
+ click_button 'Resolve discussion'
+ click_button 'Unresolve discussion'
+ end
+
+ page.within '.line-resolve-all-container' do
+ expect(page).to have_content('0/1 discussion resolved')
+ end
+ end
+
+ it 'hides resolved discussion' do
+ page.within '.diff-content' do
+ click_button 'Resolve discussion'
+ end
+
+ visit_merge_request
+
+ expect(page).to have_selector('.discussion-body', visible: false)
+ end
+
+ it 'allows user to resolve from reply form without a comment' do
+ page.within '.diff-content' do
+ click_button 'Reply...'
+
+ click_button 'Resolve discussion'
+ end
+
+ page.within '.line-resolve-all-container' do
+ expect(page).to have_content('1/1 discussion resolved')
+ expect(page).to have_selector('.line-resolve-btn.is-active')
+ end
+ end
+
+ it 'allows user to unresolve from reply form without a comment' do
+ page.within '.diff-content' do
+ click_button 'Resolve discussion'
+ sleep 1
+
+ click_button 'Reply...'
+
+ click_button 'Unresolve discussion'
+ end
+
+ page.within '.line-resolve-all-container' do
+ expect(page).to have_content('0/1 discussion resolved')
+ expect(page).not_to have_selector('.line-resolve-btn.is-active')
+ end
+ end
+
+ it 'allows user to comment & resolve discussion' do
+ page.within '.diff-content' do
+ click_button 'Reply...'
+
+ find('.js-note-text').set 'testing'
+
+ click_button 'Comment & resolve discussion'
+ end
+
+ page.within '.line-resolve-all-container' do
+ expect(page).to have_content('1/1 discussion resolved')
+ expect(page).to have_selector('.line-resolve-btn.is-active')
+ end
+ end
+
+ it 'allows user to comment & unresolve discussion' do
+ page.within '.diff-content' do
+ click_button 'Resolve discussion'
+
+ click_button 'Reply...'
+
+ find('.js-note-text').set 'testing'
+
+ click_button 'Comment & unresolve discussion'
+ end
+
+ page.within '.line-resolve-all-container' do
+ expect(page).to have_content('0/1 discussion resolved')
+ end
+ end
+
+ it 'allows user to quickly scroll to next unresolved discussion' do
+ page.within '.line-resolve-all-container' do
+ page.find('.discussion-next-btn').click
+ end
+
+ expect(page.evaluate_script("$('body').scrollTop()")).to be > 0
+ end
+
+ it 'hides jump to next button when all resolved' do
+ page.within '.diff-content' do
+ click_button 'Resolve discussion'
+ end
+
+ expect(page).to have_selector('.discussion-next-btn', visible: false)
+ end
+
+ it 'updates updated text after resolving note' do
+ page.within '.diff-content .note' do
+ find('.line-resolve-btn').click
+ end
+
+ expect(page).to have_content("Resolved by #{user.name}")
+ end
+
+ it 'hides jump to next discussion button' do
+ page.within '.discussion-reply-holder' do
+ expect(page).not_to have_selector('.discussion-next-btn')
+ end
+ end
+ end
+
+ context 'multiple notes' do
+ before do
+ create(:diff_note_on_merge_request, project: project, noteable: merge_request)
+ end
+
+ it 'does not mark discussion as resolved when resolving single note' do
+ page.within '.diff-content .note' do
+ first('.line-resolve-btn').click
+ sleep 1
+ expect(first('.line-resolve-btn')['data-original-title']).to eq("Resolved by #{user.name}")
+ end
+
+ expect(page).to have_content('Last updated')
+
+ page.within '.line-resolve-all-container' do
+ expect(page).to have_content('0/1 discussion resolved')
+ end
+ end
+
+ it 'resolves discussion' do
+ page.all('.note').each do |note|
+ note.find('.line-resolve-btn').click
+ end
+
+ expect(page).to have_content('Resolved by')
+
+ page.within '.line-resolve-all-container' do
+ expect(page).to have_content('1/1 discussion resolved')
+ end
+ end
+ end
+
+ context 'muliple discussions' do
+ before do
+ create(:diff_note_on_merge_request, project: project, position: position, noteable: merge_request)
+ visit_merge_request
+ end
+
+ it 'shows text with how many discussions' do
+ page.within '.line-resolve-all-container' do
+ expect(page).to have_content('0/2 discussions resolved')
+ end
+ end
+
+ it 'allows user to mark a single note as resolved' do
+ click_button('Resolve discussion', match: :first)
+
+ page.within '.line-resolve-all-container' do
+ expect(page).to have_content('1/2 discussions resolved')
+ end
+ end
+
+ it 'allows user to mark all notes as resolved' do
+ page.all('.line-resolve-btn').each do |btn|
+ btn.click
+ end
+
+ page.within '.line-resolve-all-container' do
+ expect(page).to have_content('2/2 discussions resolved')
+ expect(page).to have_selector('.line-resolve-btn.is-active')
+ end
+ end
+
+ it 'allows user user to mark all discussions as resolved' do
+ page.all('.discussion-reply-holder').each do |reply_holder|
+ page.within reply_holder do
+ click_button 'Resolve discussion'
+ end
+ end
+
+ page.within '.line-resolve-all-container' do
+ expect(page).to have_content('2/2 discussions resolved')
+ expect(page).to have_selector('.line-resolve-btn.is-active')
+ end
+ end
+
+ it 'allows user to quickly scroll to next unresolved discussion' do
+ page.within first('.discussion-reply-holder') do
+ click_button 'Resolve discussion'
+ end
+
+ page.within '.line-resolve-all-container' do
+ page.find('.discussion-next-btn').click
+ end
+
+ expect(page.evaluate_script("$('body').scrollTop()")).to be > 0
+ end
+
+ it 'updates updated text after resolving note' do
+ page.within first('.diff-content .note') do
+ find('.line-resolve-btn').click
+ end
+
+ expect(page).to have_content("Resolved by #{user.name}")
+ end
+
+ it 'shows jump to next discussion button' do
+ page.all('.discussion-reply-holder').each do |holder|
+ expect(holder).to have_selector('.discussion-next-btn')
+ end
+ end
+
+ it 'displays next discussion even if hidden' do
+ page.all('.note-discussion').each do |discussion|
+ page.within discussion do
+ click_link 'Toggle discussion'
+ end
+ end
+
+ page.within('.issuable-discussion #notes') do
+ expect(page).not_to have_selector('.btn', text: 'Resolve discussion')
+ end
+
+ page.within '.line-resolve-all-container' do
+ page.find('.discussion-next-btn').click
+ end
+
+ expect(find('.discussion-with-resolve-btn')).to have_selector('.btn', text: 'Resolve discussion')
+ end
+ end
+
+ context 'changes tab' do
+ it 'shows text with how many discussions' do
+ page.within '.line-resolve-all-container' do
+ expect(page).to have_content('0/1 discussion resolved')
+ end
+ end
+
+ it 'allows user to mark a note as resolved' do
+ page.within '.diff-content .note' do
+ find('.line-resolve-btn').click
+
+ expect(page).to have_selector('.line-resolve-btn.is-active')
+ end
+
+ page.within '.diff-content' do
+ expect(page).to have_selector('.btn', text: 'Unresolve discussion')
+ end
+
+ page.within '.line-resolve-all-container' do
+ expect(page).to have_content('1/1 discussion resolved')
+ expect(page).to have_selector('.line-resolve-btn.is-active')
+ end
+ end
+
+ it 'allows user to mark discussion as resolved' do
+ page.within '.diff-content' do
+ click_button 'Resolve discussion'
+ end
+
+ page.within '.diff-content .note' do
+ expect(page).to have_selector('.line-resolve-btn.is-active')
+ end
+
+ page.within '.line-resolve-all-container' do
+ expect(page).to have_content('1/1 discussion resolved')
+ expect(page).to have_selector('.line-resolve-btn.is-active')
+ end
+ end
+
+ it 'allows user to unresolve discussion' do
+ page.within '.diff-content' do
+ click_button 'Resolve discussion'
+ click_button 'Unresolve discussion'
+ end
+
+ page.within '.line-resolve-all-container' do
+ expect(page).to have_content('0/1 discussion resolved')
+ end
+ end
+
+ it 'allows user to comment & resolve discussion' do
+ page.within '.diff-content' do
+ click_button 'Reply...'
+
+ find('.js-note-text').set 'testing'
+
+ click_button 'Comment & resolve discussion'
+ end
+
+ page.within '.line-resolve-all-container' do
+ expect(page).to have_content('1/1 discussion resolved')
+ expect(page).to have_selector('.line-resolve-btn.is-active')
+ end
+ end
+
+ it 'allows user to comment & unresolve discussion' do
+ page.within '.diff-content' do
+ click_button 'Resolve discussion'
+
+ click_button 'Reply...'
+
+ find('.js-note-text').set 'testing'
+
+ click_button 'Comment & unresolve discussion'
+ end
+
+ page.within '.line-resolve-all-container' do
+ expect(page).to have_content('0/1 discussion resolved')
+ end
+ end
+ end
+ end
+
+ context 'as a guest' do
+ let(:guest) { create(:user) }
+
+ before do
+ project.team << [guest, :guest]
+ login_as guest
+ end
+
+ context 'someone elses merge request' do
+ before do
+ visit_merge_request
+ end
+
+ it 'does not allow user to mark note as resolved' do
+ page.within '.diff-content .note' do
+ expect(page).not_to have_selector('.line-resolve-btn')
+ end
+
+ page.within '.line-resolve-all-container' do
+ expect(page).to have_content('0/1 discussion resolved')
+ end
+ end
+
+ it 'does not allow user to mark discussion as resolved' do
+ page.within '.diff-content .note' do
+ expect(page).not_to have_selector('.btn', text: 'Resolve discussion')
+ end
+ end
+ end
+
+ context 'guest users merge request' do
+ before do
+ mr = create(:merge_request_with_diffs, source_project: project, source_branch: 'markdown', author: guest, title: "Bug")
+ create(:diff_note_on_merge_request, project: project, noteable: mr)
+ visit_merge_request(mr)
+ end
+
+ it 'allows user to mark a note as resolved' do
+ page.within '.diff-content .note' do
+ find('.line-resolve-btn').click
+
+ expect(page).to have_selector('.line-resolve-btn.is-active')
+ end
+
+ page.within '.diff-content' do
+ expect(page).to have_selector('.btn', text: 'Unresolve discussion')
+ end
+
+ page.within '.line-resolve-all-container' do
+ expect(page).to have_content('1/1 discussion resolved')
+ expect(page).to have_selector('.line-resolve-btn.is-active')
+ end
+ end
+ end
+ end
+
+ context 'unauthorized user' do
+ context 'no resolved comments' do
+ before do
+ visit_merge_request
+ end
+
+ it 'does not allow user to mark note as resolved' do
+ page.within '.diff-content .note' do
+ expect(page).not_to have_selector('.line-resolve-btn')
+ end
+
+ page.within '.line-resolve-all-container' do
+ expect(page).to have_content('0/1 discussion resolved')
+ end
+ end
+ end
+
+ context 'resolved comment' do
+ before do
+ note.resolve!(user)
+ visit_merge_request
+ end
+
+ it 'shows resolved icon' do
+ expect(page).to have_content '1/1 discussion resolved'
+
+ click_link 'Toggle discussion'
+ expect(page).to have_selector('.line-resolve-btn.is-active')
+ end
+
+ it 'does not allow user to click resolve button' do
+ expect(page).to have_selector('.line-resolve-btn.is-disabled')
+ click_link 'Toggle discussion'
+
+ expect(page).to have_selector('.line-resolve-btn.is-disabled')
+ end
+ end
+ end
+
+ def visit_merge_request(mr = nil)
+ mr = mr || merge_request
+ visit namespace_project_merge_request_path(mr.project.namespace, mr.project, mr)
+ end
+end
diff --git a/spec/features/merge_requests/diff_notes_spec.rb b/spec/features/merge_requests/diff_notes_spec.rb
new file mode 100644
index 00000000000..a818679a874
--- /dev/null
+++ b/spec/features/merge_requests/diff_notes_spec.rb
@@ -0,0 +1,207 @@
+require 'spec_helper'
+
+feature 'Diff notes', js: true, feature: true do
+ include WaitForAjax
+
+ before do
+ login_as :admin
+ @merge_request = create(:merge_request)
+ @project = @merge_request.source_project
+ end
+
+ context 'merge request diffs' do
+ let(:comment_button_class) { '.add-diff-note' }
+ let(:notes_holder_input_class) { 'js-temp-notes-holder' }
+ let(:notes_holder_input_xpath) { './following-sibling::*[contains(concat(" ", @class, " "), " notes_holder ")]' }
+ let(:test_note_comment) { 'this is a test note!' }
+
+ context 'when hovering over a parallel view diff file' do
+ before(:each) do
+ visit diffs_namespace_project_merge_request_path(@project.namespace, @project, @merge_request, view: 'parallel')
+ end
+
+ context 'with an old line on the left and no line on the right' do
+ it 'should allow commenting on the left side' do
+ should_allow_commenting(find('[id="6eb14e00385d2fb284765eb1cd8d420d33d63fc9_23_22"]').find(:xpath, '..'), 'left')
+ end
+
+ it 'should not allow commenting on the right side' do
+ should_not_allow_commenting(find('[id="6eb14e00385d2fb284765eb1cd8d420d33d63fc9_23_22"]').find(:xpath, '..'), 'right')
+ end
+ end
+
+ context 'with no line on the left and a new line on the right' do
+ it 'should not allow commenting on the left side' do
+ should_not_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_15"]').find(:xpath, '..'), 'left')
+ end
+
+ it 'should allow commenting on the right side' do
+ should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_15"]').find(:xpath, '..'), 'right')
+ end
+ end
+
+ context 'with an old line on the left and a new line on the right' do
+ it 'should allow commenting on the left side' do
+ should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_9_9"]').find(:xpath, '..'), 'left')
+ end
+
+ it 'should allow commenting on the right side' do
+ should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_9_9"]').find(:xpath, '..'), 'right')
+ end
+ end
+
+ context 'with an unchanged line on the left and an unchanged line on the right' do
+ it 'should allow commenting on the left side' do
+ should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_7_7"]', match: :first).find(:xpath, '..'), 'left')
+ end
+
+ it 'should allow commenting on the right side' do
+ should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_7_7"]', match: :first).find(:xpath, '..'), 'right')
+ end
+ end
+
+ context 'with a match line' do
+ it 'should not allow commenting on the left side' do
+ should_not_allow_commenting(find('.match', match: :first).find(:xpath, '..'), 'left')
+ end
+
+ it 'should not allow commenting on the right side' do
+ should_not_allow_commenting(find('.match', match: :first).find(:xpath, '..'), 'right')
+ end
+ end
+
+ context 'with an unfolded line' do
+ before(:each) do
+ find('.js-unfold', match: :first).click
+ wait_for_ajax
+ end
+
+ # The first `.js-unfold` unfolds upwards, therefore the first
+ # `.line_holder` will be an unfolded line.
+ let(:line_holder) { first('.line_holder[id="1"]') }
+
+ it 'should not allow commenting on the left side' do
+ should_not_allow_commenting(line_holder, 'left')
+ end
+
+ it 'should not allow commenting on the right side' do
+ should_not_allow_commenting(line_holder, 'right')
+ end
+ end
+ end
+
+ context 'when hovering over an inline view diff file' do
+ before do
+ visit diffs_namespace_project_merge_request_path(@project.namespace, @project, @merge_request, view: 'inline')
+ end
+
+ context 'with a new line' do
+ it 'should allow commenting' do
+ should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"]'))
+ end
+ end
+
+ context 'with an old line' do
+ it 'should allow commenting' do
+ should_allow_commenting(find('[id="6eb14e00385d2fb284765eb1cd8d420d33d63fc9_22_22"]'))
+ end
+ end
+
+ context 'with an unchanged line' do
+ it 'should allow commenting' do
+ should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_7_7"]'))
+ end
+ end
+
+ context 'with a match line' do
+ it 'should not allow commenting' do
+ should_not_allow_commenting(find('.match', match: :first))
+ end
+ end
+
+ context 'with an unfolded line' do
+ before(:each) do
+ find('.js-unfold', match: :first).click
+ wait_for_ajax
+ end
+
+ # The first `.js-unfold` unfolds upwards, therefore the first
+ # `.line_holder` will be an unfolded line.
+ let(:line_holder) { first('.line_holder[id="1"]') }
+
+ it 'should not allow commenting' do
+ should_not_allow_commenting line_holder
+ end
+ end
+
+ context 'when hovering over a diff discussion' do
+ before do
+ visit diffs_namespace_project_merge_request_path(@project.namespace, @project, @merge_request, view: 'inline')
+ should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_7_7"]'))
+ visit namespace_project_merge_request_path(@project.namespace, @project, @merge_request)
+ end
+
+ it 'should not allow commenting' do
+ should_not_allow_commenting(find('.line_holder', match: :first))
+ end
+ end
+ end
+
+ def should_allow_commenting(line_holder, diff_side = nil)
+ line = get_line_components(line_holder, diff_side)
+ line[:content].hover
+ expect(line[:num]).to have_css comment_button_class
+
+ comment_on_line(line_holder, line)
+
+ assert_comment_persistence(line_holder)
+ end
+
+ def should_not_allow_commenting(line_holder, diff_side = nil)
+ line = get_line_components(line_holder, diff_side)
+ line[:content].hover
+ expect(line[:num]).not_to have_css comment_button_class
+ end
+
+ def get_line_components(line_holder, diff_side = nil)
+ if diff_side.nil?
+ get_inline_line_components(line_holder)
+ else
+ get_parallel_line_components(line_holder, diff_side)
+ end
+ end
+
+ def get_inline_line_components(line_holder)
+ { content: line_holder.find('.line_content', match: :first), num: line_holder.find('.diff-line-num', match: :first) }
+ end
+
+ def get_parallel_line_components(line_holder, diff_side = nil)
+ side_index = diff_side == 'left' ? 0 : 1
+ # Wait for `.line_content`
+ line_holder.find('.line_content', match: :first)
+ # Wait for `.diff-line-num`
+ line_holder.find('.diff-line-num', match: :first)
+ { content: line_holder.all('.line_content')[side_index], num: line_holder.all('.diff-line-num')[side_index] }
+ end
+
+ def comment_on_line(line_holder, line)
+ line[:num].find(comment_button_class).trigger 'click'
+ line_holder.find(:xpath, notes_holder_input_xpath)
+
+ notes_holder_input = line_holder.find(:xpath, notes_holder_input_xpath)
+ expect(notes_holder_input[:class]).to include(notes_holder_input_class)
+
+ notes_holder_input.fill_in 'note[note]', with: test_note_comment
+ click_button 'Comment'
+ wait_for_ajax
+ end
+
+ def assert_comment_persistence(line_holder)
+ expect(line_holder).to have_xpath notes_holder_input_xpath
+
+ notes_holder_saved = line_holder.find(:xpath, notes_holder_input_xpath)
+ expect(notes_holder_saved[:class]).not_to include(notes_holder_input_class)
+ expect(notes_holder_saved).to have_content test_note_comment
+ end
+ end
+end
diff --git a/spec/features/merge_requests/diffs_spec.rb b/spec/features/merge_requests/diffs_spec.rb
new file mode 100644
index 00000000000..c9a0059645d
--- /dev/null
+++ b/spec/features/merge_requests/diffs_spec.rb
@@ -0,0 +1,25 @@
+require 'spec_helper'
+
+feature 'Diffs URL', js: true, feature: true do
+ before do
+ login_as :admin
+ @merge_request = create(:merge_request)
+ @project = @merge_request.source_project
+ end
+
+ context 'when visit with */* as accept header' do
+ before(:each) do
+ page.driver.add_header('Accept', '*/*')
+ end
+
+ it 'renders the notes' do
+ create :note_on_merge_request, project: @project, noteable: @merge_request, note: 'Rebasing with master'
+
+ visit diffs_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)
+
+ # Load notes and diff through AJAX
+ expect(page).to have_css('.note-text', visible: false, text: 'Rebasing with master')
+ expect(page).to have_css('.diffs.tab-pane.active')
+ end
+ end
+end
diff --git a/spec/features/merge_requests/edit_mr_spec.rb b/spec/features/merge_requests/edit_mr_spec.rb
index 9e007ab7635..c77e719c5df 100644
--- a/spec/features/merge_requests/edit_mr_spec.rb
+++ b/spec/features/merge_requests/edit_mr_spec.rb
@@ -14,8 +14,19 @@ feature 'Edit Merge Request', feature: true do
end
context 'editing a MR' do
- it 'form should have class js-quick-submit' do
+ it 'has class js-quick-submit in form' do
expect(page).to have_selector('.js-quick-submit')
end
+
+ it 'warns about version conflict' do
+ merge_request.update(title: "New title")
+
+ fill_in 'merge_request_title', with: 'bug 345'
+ fill_in 'merge_request_description', with: 'bug description'
+
+ click_button 'Save changes'
+
+ expect(page).to have_content 'Someone edited the merge request the same time you did'
+ end
end
end
diff --git a/spec/features/merge_requests/filter_by_milestone_spec.rb b/spec/features/merge_requests/filter_by_milestone_spec.rb
index e3ecd60a5f3..bb0bb590a46 100644
--- a/spec/features/merge_requests/filter_by_milestone_spec.rb
+++ b/spec/features/merge_requests/filter_by_milestone_spec.rb
@@ -21,7 +21,7 @@ feature 'Merge Request filtering by Milestone', feature: true do
end
context 'filters by upcoming milestone', js: true do
- it 'should not show issues with no expiry' do
+ it 'does not show issues with no expiry' do
create(:merge_request, :with_diffs, source_project: project)
create(:merge_request, :simple, source_project: project, milestone: milestone)
@@ -31,7 +31,7 @@ feature 'Merge Request filtering by Milestone', feature: true do
expect(page).to have_css('.merge-request', count: 0)
end
- it 'should show issues in future' do
+ it 'shows issues in future' do
milestone = create(:milestone, project: project, due_date: Date.tomorrow)
create(:merge_request, :with_diffs, source_project: project)
create(:merge_request, :simple, source_project: project, milestone: milestone)
@@ -42,7 +42,7 @@ feature 'Merge Request filtering by Milestone', feature: true do
expect(page).to have_css('.merge-request', count: 1)
end
- it 'should not show issues in past' do
+ it 'does not show issues in past' do
milestone = create(:milestone, project: project, due_date: Date.yesterday)
create(:merge_request, :with_diffs, source_project: project)
create(:merge_request, :simple, source_project: project, milestone: milestone)
diff --git a/spec/features/merge_requests/merge_request_versions_spec.rb b/spec/features/merge_requests/merge_request_versions_spec.rb
new file mode 100644
index 00000000000..577c910f11b
--- /dev/null
+++ b/spec/features/merge_requests/merge_request_versions_spec.rb
@@ -0,0 +1,37 @@
+require 'spec_helper'
+
+feature 'Merge Request versions', js: true, feature: true do
+ before do
+ login_as :admin
+ merge_request = create(:merge_request, importing: true)
+ merge_request.merge_request_diffs.create(head_commit_sha: '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9')
+ merge_request.merge_request_diffs.create(head_commit_sha: '5937ac0a7beb003549fc5fd26fc247adbce4a52e')
+ project = merge_request.source_project
+ visit diffs_namespace_project_merge_request_path(project.namespace, project, merge_request)
+ end
+
+ it 'show the latest version of the diff' do
+ page.within '.mr-version-switch' do
+ expect(page).to have_content 'Version: latest'
+ end
+
+ expect(page).to have_content '8 changed files'
+ end
+
+ describe 'switch between versions' do
+ before do
+ page.within '.mr-version-switch' do
+ find('.btn-link').click
+ click_link '6f6d7e7e'
+ end
+ end
+
+ it 'should show older version' do
+ page.within '.mr-version-switch' do
+ expect(page).to have_content 'Version: 6f6d7e7e'
+ end
+
+ expect(page).to have_content '5 changed files'
+ end
+ 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 96f7b8c9932..60bc07bd1a0 100644
--- a/spec/features/merge_requests/merge_when_build_succeeds_spec.rb
+++ b/spec/features/merge_requests/merge_when_build_succeeds_spec.rb
@@ -73,7 +73,7 @@ feature 'Merge When Build Succeeds', feature: true, js: true do
end
context 'Build is not active' do
- it "should not allow for enabling" do
+ it "does not allow for enabling" do
visit_merge_request(merge_request)
expect(page).not_to have_link "Merge When Build Succeeds"
end
diff --git a/spec/features/merge_requests/pipelines_spec.rb b/spec/features/merge_requests/pipelines_spec.rb
new file mode 100644
index 00000000000..9c4c0525267
--- /dev/null
+++ b/spec/features/merge_requests/pipelines_spec.rb
@@ -0,0 +1,48 @@
+require 'spec_helper'
+
+feature 'Pipelines for Merge Requests', feature: true, js: true do
+ include WaitForAjax
+
+ given(:user) { create(:user) }
+ given(:merge_request) { create(:merge_request) }
+ given(:project) { merge_request.target_project }
+
+ before do
+ project.team << [user, :master]
+ login_as user
+ end
+
+ context 'with pipelines' do
+ let!(:pipeline) do
+ create(:ci_empty_pipeline,
+ project: merge_request.source_project,
+ ref: merge_request.source_branch,
+ sha: merge_request.diff_head_sha)
+ end
+
+ before do
+ visit namespace_project_merge_request_path(project.namespace, project, merge_request)
+ end
+
+ scenario 'user visits merge request pipelines tab' do
+ page.within('.merge-request-tabs') do
+ click_link('Pipelines')
+ end
+ wait_for_ajax
+
+ expect(page).to have_selector('.pipeline-actions')
+ end
+ end
+
+ context 'without pipelines' do
+ before do
+ visit namespace_project_merge_request_path(project.namespace, project, merge_request)
+ end
+
+ scenario 'user visits merge request page' do
+ page.within('.merge-request-tabs') do
+ expect(page).to have_no_link('Pipelines')
+ end
+ end
+ 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 1c130057c56..cabb8e455f9 100644
--- a/spec/features/merge_requests/user_lists_merge_requests_spec.rb
+++ b/spec/features/merge_requests/user_lists_merge_requests_spec.rb
@@ -1,6 +1,7 @@
require 'spec_helper'
describe 'Projects > Merge requests > User lists merge requests', feature: true do
+ include MergeRequestHelpers
include SortingHelper
let(:project) { create(:project, :public) }
@@ -23,10 +24,12 @@ describe 'Projects > Merge requests > User lists merge requests', feature: true
milestone: create(:milestone, due_date: '2013-12-12'),
created_at: 2.minutes.ago,
updated_at: 2.minutes.ago)
+ # lfs in itself is not a great choice for the title if one wants to match the whole body content later on
+ # just think about the scenario when faker generates 'Chester Runolfsson' as the user's name
create(:merge_request,
- title: 'lfs',
+ title: 'merge_lfs',
source_project: project,
- source_branch: 'lfs',
+ source_branch: 'merge_lfs',
created_at: 3.minutes.ago,
updated_at: 10.seconds.ago)
end
@@ -35,7 +38,7 @@ describe 'Projects > Merge requests > User lists merge requests', feature: true
visit_merge_requests(project, assignee_id: IssuableFinder::NONE)
expect(current_path).to eq(namespace_project_merge_requests_path(project.namespace, project))
- expect(page).to have_content 'lfs'
+ expect(page).to have_content 'merge_lfs'
expect(page).not_to have_content 'fix'
expect(page).not_to have_content 'markdown'
expect(count_merge_requests).to eq(1)
@@ -44,7 +47,7 @@ describe 'Projects > Merge requests > User lists merge requests', feature: true
it 'filters on a specific assignee' do
visit_merge_requests(project, assignee_id: user.id)
- expect(page).not_to have_content 'lfs'
+ expect(page).not_to have_content 'merge_lfs'
expect(page).to have_content 'fix'
expect(page).to have_content 'markdown'
expect(count_merge_requests).to eq(2)
@@ -53,23 +56,23 @@ describe 'Projects > Merge requests > User lists merge requests', feature: true
it 'sorts by newest' do
visit_merge_requests(project, sort: sort_value_recently_created)
- expect(first_merge_request).to include('lfs')
- expect(last_merge_request).to include('fix')
+ expect(first_merge_request).to include('fix')
+ expect(last_merge_request).to include('merge_lfs')
expect(count_merge_requests).to eq(3)
end
it 'sorts by oldest' do
visit_merge_requests(project, sort: sort_value_oldest_created)
- expect(first_merge_request).to include('fix')
- expect(last_merge_request).to include('lfs')
+ expect(first_merge_request).to include('merge_lfs')
+ expect(last_merge_request).to include('fix')
expect(count_merge_requests).to eq(3)
end
it 'sorts by last updated' do
visit_merge_requests(project, sort: sort_value_recently_updated)
- expect(first_merge_request).to include('lfs')
+ expect(first_merge_request).to include('merge_lfs')
expect(count_merge_requests).to eq(3)
end
@@ -143,18 +146,6 @@ describe 'Projects > Merge requests > User lists merge requests', feature: true
end
end
- def visit_merge_requests(project, opts = {})
- visit namespace_project_merge_requests_path(project.namespace, project, opts)
- end
-
- def first_merge_request
- page.all('ul.mr-list > li').first.text
- end
-
- def last_merge_request
- page.all('ul.mr-list > li').last.text
- end
-
def count_merge_requests
page.all('ul.mr-list > li').count
end
diff --git a/spec/features/merge_requests/user_uses_slash_commands_spec.rb b/spec/features/merge_requests/user_uses_slash_commands_spec.rb
new file mode 100644
index 00000000000..d9ef0d18074
--- /dev/null
+++ b/spec/features/merge_requests/user_uses_slash_commands_spec.rb
@@ -0,0 +1,32 @@
+require 'rails_helper'
+
+feature 'Merge Requests > User uses slash commands', feature: true, js: true do
+ include WaitForAjax
+
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :public) }
+ let(:merge_request) { create(:merge_request, source_project: project) }
+ let!(:milestone) { create(:milestone, project: project, title: 'ASAP') }
+
+ it_behaves_like 'issuable record that supports slash commands in its description and notes', :merge_request do
+ let(:issuable) { create(:merge_request, source_project: project) }
+ let(:new_url_opts) { { merge_request: { source_branch: 'feature' } } }
+ end
+
+ describe 'adding a due date from note' do
+ before do
+ project.team << [user, :master]
+ login_with(user)
+ visit namespace_project_merge_request_path(project.namespace, project, merge_request)
+ end
+
+ it 'does not recognize the command nor create a note' do
+ page.within('.js-main-target-form') do
+ fill_in 'note[note]', with: "/due 2016-08-28"
+ click_button 'Comment'
+ end
+
+ expect(page).not_to have_content '/due 2016-08-28'
+ end
+ end
+end
diff --git a/spec/features/milestone_spec.rb b/spec/features/milestone_spec.rb
index c2c7acff3e8..c43661e5681 100644
--- a/spec/features/milestone_spec.rb
+++ b/spec/features/milestone_spec.rb
@@ -13,7 +13,7 @@ feature 'Milestone', feature: true do
end
feature 'Create a milestone' do
- scenario 'should show an informative message for a new issue' do
+ scenario 'shows an informative message for a new issue' do
visit new_namespace_project_milestone_path(project.namespace, project)
page.within '.milestone-form' do
fill_in "milestone_title", with: '8.7'
@@ -25,7 +25,7 @@ feature 'Milestone', feature: true do
end
feature 'Open a milestone with closed issues' do
- scenario 'should show an informative message' do
+ scenario 'shows an informative message' do
create(:issue, title: "Bugfix1", project: project, milestone: milestone, state: "closed")
visit namespace_project_milestone_path(project.namespace, project, milestone)
diff --git a/spec/features/notes_on_merge_requests_spec.rb b/spec/features/notes_on_merge_requests_spec.rb
index 5174168713c..f1c522155d3 100644
--- a/spec/features/notes_on_merge_requests_spec.rb
+++ b/spec/features/notes_on_merge_requests_spec.rb
@@ -23,7 +23,7 @@ describe 'Comments', feature: true do
subject { page }
describe 'the note form' do
- it 'should be valid' do
+ it 'is valid' do
is_expected.to have_css('.js-main-target-form', visible: true, count: 1)
expect(find('.js-main-target-form input[type=submit]').value).
to eq('Comment')
@@ -39,7 +39,7 @@ describe 'Comments', feature: true do
end
end
- it 'should have enable submit button and preview button' do
+ it 'has enable submit button and preview button' do
page.within('.js-main-target-form') do
expect(page).not_to have_css('.js-comment-button[disabled]')
expect(page).to have_css('.js-md-preview-button', visible: true)
@@ -57,7 +57,7 @@ describe 'Comments', feature: true do
end
end
- it 'should be added and form reset' do
+ it 'is added and form reset' do
is_expected.to have_content('This is awsome!')
page.within('.js-main-target-form') do
expect(page).to have_no_field('note[note]', with: 'This is awesome!')
@@ -70,7 +70,7 @@ describe 'Comments', feature: true do
end
describe 'when editing a note', js: true do
- it 'should contain the hidden edit form' do
+ it 'contains the hidden edit form' do
page.within("#note_#{note.id}") do
is_expected.to have_css('.note-edit-form', visible: false)
end
@@ -82,7 +82,7 @@ describe 'Comments', feature: true do
find(".js-note-edit").click
end
- it 'should show the note edit form and hide the note body' do
+ it 'shows the note edit form and hide the note body' do
page.within("#note_#{note.id}") do
expect(find('.current-note-edit-form', visible: true)).to be_visible
expect(find('.note-edit-form', visible: true)).to be_visible
@@ -135,6 +135,28 @@ describe 'Comments', feature: true do
end
end
+ describe 'Handles cross-project system notes', js: true, feature: true do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :public) }
+ let(:project2) { create(:project, :private) }
+ let(:issue) { create(:issue, project: project2) }
+ let(:merge_request) { create(:merge_request, source_project: project, source_branch: 'markdown') }
+ let!(:note) { create(:note_on_merge_request, :system, noteable: merge_request, project: project, note: "Mentioned in #{issue.to_reference(project)}") }
+
+ it 'shows the system note' do
+ login_as :admin
+ visit namespace_project_merge_request_path(project.namespace, project, merge_request)
+
+ expect(page).to have_css('.system-note')
+ end
+
+ it 'hides redacted system note' do
+ visit namespace_project_merge_request_path(project.namespace, project, merge_request)
+
+ expect(page).not_to have_css('.system-note')
+ end
+ end
+
describe 'On a merge request diff', js: true, feature: true do
let(:merge_request) { create(:merge_request) }
let(:project) { merge_request.source_project }
@@ -212,7 +234,7 @@ describe 'Comments', feature: true do
end
end
- it 'should be added as discussion' do
+ it 'adds as discussion' do
is_expected.to have_content('Another comment on line 10')
is_expected.to have_css('.notes_holder')
is_expected.to have_css('.notes_holder .note', count: 1)
@@ -231,6 +253,7 @@ describe 'Comments', feature: true do
end
def click_diff_line(data = line_code)
- execute_script("$('button[data-line-code=\"#{data}\"]').click()")
+ find(".line_holder[id='#{data}'] td.line_content").hover
+ find(".line_holder[id='#{data}'] button").trigger('click')
end
end
diff --git a/spec/features/participants_autocomplete_spec.rb b/spec/features/participants_autocomplete_spec.rb
index c7c00a3266a..a78a1c9c890 100644
--- a/spec/features/participants_autocomplete_spec.rb
+++ b/spec/features/participants_autocomplete_spec.rb
@@ -12,17 +12,17 @@ feature 'Member autocomplete', feature: true do
end
shared_examples "open suggestions" do
- it 'suggestions are displayed' do
+ it 'displays suggestions' do
expect(page).to have_selector('.atwho-view', visible: true)
end
- it 'author is suggested' do
+ it 'suggests author' do
page.within('.atwho-view', visible: true) do
expect(page).to have_content(author.username)
end
end
- it 'participant is suggested' do
+ it 'suggests participant' do
page.within('.atwho-view', visible: true) do
expect(page).to have_content(participant.username)
end
diff --git a/spec/features/pipelines_settings_spec.rb b/spec/features/pipelines_settings_spec.rb
new file mode 100644
index 00000000000..dcc364a3d01
--- /dev/null
+++ b/spec/features/pipelines_settings_spec.rb
@@ -0,0 +1,35 @@
+require 'spec_helper'
+
+feature "Pipelines settings", feature: true do
+ include GitlabRoutingHelper
+
+ let(:project) { create(:empty_project) }
+ let(:user) { create(:user) }
+ let(:role) { :developer }
+
+ background do
+ login_as(user)
+ project.team << [user, role]
+ visit namespace_project_pipelines_settings_path(project.namespace, project)
+ end
+
+ context 'for developer' do
+ given(:role) { :developer }
+
+ scenario 'to be disallowed to view' do
+ expect(page.status_code).to eq(404)
+ end
+ end
+
+ context 'for master' do
+ given(:role) { :master }
+
+ scenario 'be allowed to change' do
+ fill_in('Test coverage parsing', with: 'coverage_regex')
+ click_on 'Save changes'
+
+ expect(page.status_code).to eq(200)
+ expect(page).to have_field('Test coverage parsing', with: 'coverage_regex')
+ end
+ end
+end
diff --git a/spec/features/profile_spec.rb b/spec/features/profile_spec.rb
index c80253fead8..c3d8c349ca4 100644
--- a/spec/features/profile_spec.rb
+++ b/spec/features/profile_spec.rb
@@ -15,7 +15,7 @@ describe 'Profile account page', feature: true do
it { expect(page).to have_content('Remove account') }
- it 'should delete the account' do
+ it 'deletes the account' do
expect { click_link 'Delete account' }.to change { User.count }.by(-1)
expect(current_path).to eq(new_user_session_path)
end
@@ -27,7 +27,7 @@ describe 'Profile account page', feature: true do
visit profile_account_path
end
- it 'should not have option to remove account' do
+ it 'does not have option to remove account' do
expect(page).not_to have_content('Remove account')
expect(current_path).to eq(profile_account_path)
end
diff --git a/spec/features/profiles/password_spec.rb b/spec/features/profiles/password_spec.rb
new file mode 100644
index 00000000000..4cbdd89d46f
--- /dev/null
+++ b/spec/features/profiles/password_spec.rb
@@ -0,0 +1,45 @@
+require 'spec_helper'
+
+describe 'Profile > Password', feature: true do
+ let(:user) { create(:user, password_automatically_set: true) }
+
+ before do
+ login_as(user)
+ visit edit_profile_password_path
+ end
+
+ def fill_passwords(password, confirmation)
+ fill_in 'New password', with: password
+ fill_in 'Password confirmation', with: confirmation
+
+ click_button 'Save password'
+ end
+
+ context 'User with password automatically set' do
+ describe 'User puts different passwords in the field and in the confirmation' do
+ it 'shows an error message' do
+ fill_passwords('mypassword', 'mypassword2')
+
+ page.within('.alert-danger') do
+ expect(page).to have_content("Password confirmation doesn't match Password")
+ end
+ end
+
+ it 'does not contains the current password field after an error' do
+ fill_passwords('mypassword', 'mypassword2')
+
+ expect(page).to have_no_field('user[current_password]')
+ end
+ end
+
+ describe 'User puts the same passwords in the field and in the confirmation' do
+ it 'shows a success message' do
+ fill_passwords('mypassword', 'mypassword')
+
+ page.within('.flash-notice') do
+ expect(page).to have_content('Password was successfully updated. Please login with it')
+ end
+ end
+ end
+ end
+end
diff --git a/spec/features/profiles/preferences_spec.rb b/spec/features/profiles/preferences_spec.rb
index 787bf42d048..d14a1158b67 100644
--- a/spec/features/profiles/preferences_spec.rb
+++ b/spec/features/profiles/preferences_spec.rb
@@ -68,10 +68,14 @@ describe 'Profile > Preferences', feature: true do
allowing_for_delay do
find('#logo').click
+
+ expect(page).to have_content("You don't have starred projects yet")
expect(page.current_path).to eq starred_dashboard_projects_path
end
click_link 'Your Projects'
+
+ expect(page).not_to have_content("You don't have starred projects yet")
expect(page.current_path).to eq dashboard_projects_path
end
end
diff --git a/spec/features/projects/badges/coverage_spec.rb b/spec/features/projects/badges/coverage_spec.rb
new file mode 100644
index 00000000000..5972e7f31c2
--- /dev/null
+++ b/spec/features/projects/badges/coverage_spec.rb
@@ -0,0 +1,82 @@
+require 'spec_helper'
+
+feature 'test coverage badge' do
+ given!(:user) { create(:user) }
+ given!(:project) { create(:project, :private) }
+
+ context 'when user has access to view badge' do
+ background do
+ project.team << [user, :developer]
+ login_as(user)
+ end
+
+ scenario 'user requests coverage badge image for pipeline' do
+ create_pipeline do |pipeline|
+ create_build(pipeline, coverage: 100, name: 'test:1')
+ create_build(pipeline, coverage: 90, name: 'test:2')
+ end
+
+ show_test_coverage_badge
+
+ expect_coverage_badge('95%')
+ end
+
+ scenario 'user requests coverage badge for specific job' do
+ create_pipeline do |pipeline|
+ create_build(pipeline, coverage: 50, name: 'test:1')
+ create_build(pipeline, coverage: 50, name: 'test:2')
+ create_build(pipeline, coverage: 85, name: 'coverage')
+ end
+
+ show_test_coverage_badge(job: 'coverage')
+
+ expect_coverage_badge('85%')
+ end
+
+ scenario 'user requests coverage badge for pipeline without coverage' do
+ create_pipeline do |pipeline|
+ create_build(pipeline, coverage: nil, name: 'test')
+ end
+
+ show_test_coverage_badge
+
+ expect_coverage_badge('unknown')
+ end
+ end
+
+ context 'when user does not have access to view badge' do
+ background { login_as(user) }
+
+ scenario 'user requests test coverage badge image' do
+ show_test_coverage_badge
+
+ expect(page).to have_http_status(404)
+ end
+ end
+
+ def create_pipeline
+ opts = { project: project, ref: 'master', sha: project.commit.id }
+
+ create(:ci_pipeline, opts).tap do |pipeline|
+ yield pipeline
+ pipeline.build_updated
+ end
+ end
+
+ def create_build(pipeline, coverage:, name:)
+ opts = { pipeline: pipeline, coverage: coverage, name: name }
+
+ create(:ci_build, :success, opts)
+ end
+
+ def show_test_coverage_badge(job: nil)
+ visit coverage_namespace_project_badges_path(
+ project.namespace, project, ref: :master, job: job, format: :svg)
+ end
+
+ def expect_coverage_badge(coverage)
+ svg = Nokogiri::XML.parse(page.body)
+ expect(page.response_headers['Content-Type']).to include('image/svg+xml')
+ expect(svg.at(%Q{text:contains("#{coverage}")})).to be_truthy
+ end
+end
diff --git a/spec/features/projects/badges/list_spec.rb b/spec/features/projects/badges/list_spec.rb
index 01e90618a98..67a4a5d1ab1 100644
--- a/spec/features/projects/badges/list_spec.rb
+++ b/spec/features/projects/badges/list_spec.rb
@@ -6,28 +6,46 @@ feature 'list of badges' do
project = create(:project)
project.team << [user, :master]
login_as(user)
- visit namespace_project_badges_path(project.namespace, project)
+ visit namespace_project_pipelines_settings_path(project.namespace, project)
end
- scenario 'user displays list of badges' do
- expect(page).to have_content 'build status'
- expect(page).to have_content 'Markdown'
- expect(page).to have_content 'HTML'
- expect(page).to have_css('.highlight', count: 2)
- expect(page).to have_xpath("//img[@alt='build status']")
+ scenario 'user wants to see build status badge' do
+ page.within('.build-status') do
+ expect(page).to have_content 'build status'
+ expect(page).to have_content 'Markdown'
+ expect(page).to have_content 'HTML'
+ expect(page).to have_css('.highlight', count: 2)
+ expect(page).to have_xpath("//img[@alt='build status']")
- page.within('.highlight', match: :first) do
- expect(page).to have_content 'badges/master/build.svg'
+ page.within('.highlight', match: :first) do
+ expect(page).to have_content 'badges/master/build.svg'
+ end
end
end
- scenario 'user changes current ref on badges list page', js: true do
- first('.js-project-refs-dropdown').click
+ scenario 'user wants to see coverage report badge' do
+ page.within('.coverage-report') do
+ expect(page).to have_content 'coverage report'
+ expect(page).to have_content 'Markdown'
+ expect(page).to have_content 'HTML'
+ expect(page).to have_css('.highlight', count: 2)
+ expect(page).to have_xpath("//img[@alt='coverage report']")
- page.within '.project-refs-form' do
- click_link 'improve/awesome'
+ page.within('.highlight', match: :first) do
+ expect(page).to have_content 'badges/master/coverage.svg'
+ end
end
+ end
+
+ scenario 'user changes current ref of build status badge', js: true do
+ page.within('.build-status') do
+ first('.js-project-refs-dropdown').click
- expect(page).to have_content 'badges/improve/awesome/build.svg'
+ page.within '.project-refs-form' do
+ click_link 'improve/awesome'
+ end
+
+ expect(page).to have_content 'badges/improve/awesome/build.svg'
+ end
end
end
diff --git a/spec/features/projects/branches/delete_spec.rb b/spec/features/projects/branches/delete_spec.rb
new file mode 100644
index 00000000000..63878c55421
--- /dev/null
+++ b/spec/features/projects/branches/delete_spec.rb
@@ -0,0 +1,24 @@
+require 'spec_helper'
+
+feature 'Delete branch', feature: true, js: true do
+ include WaitForAjax
+
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+
+ before do
+ project.team << [user, :master]
+ login_as user
+ visit namespace_project_branches_path(project.namespace, project)
+ end
+
+ it 'destroys tooltip' do
+ first('.remove-row').hover
+ expect(page).to have_selector('.tooltip')
+
+ first('.remove-row').click
+ wait_for_ajax
+
+ expect(page).not_to have_selector('.tooltip')
+ end
+end
diff --git a/spec/features/projects/branches_spec.rb b/spec/features/projects/branches_spec.rb
new file mode 100644
index 00000000000..1b14945bf0a
--- /dev/null
+++ b/spec/features/projects/branches_spec.rb
@@ -0,0 +1,32 @@
+require 'spec_helper'
+
+describe 'Branches', feature: true do
+ let(:project) { create(:project) }
+ let(:repository) { project.repository }
+
+ before do
+ login_as :user
+ project.team << [@user, :developer]
+ end
+
+ describe 'Initial branches page' do
+ it 'shows all the branches' do
+ visit namespace_project_branches_path(project.namespace, project)
+
+ repository.branches { |branch| expect(page).to have_content("#{branch.name}") }
+ expect(page).to have_content("Protected branches can be managed in project settings")
+ end
+ end
+
+ describe 'Find branches' do
+ it 'shows filtered branches', js: true do
+ visit namespace_project_branches_path(project.namespace, project)
+
+ fill_in 'branch-search', with: 'fix'
+ find('#branch-search').native.send_keys(:enter)
+
+ expect(page).to have_content('fix')
+ expect(find('.all-branches')).to have_selector('li', count: 1)
+ end
+ end
+end
diff --git a/spec/features/projects/commits/cherry_pick_spec.rb b/spec/features/projects/commits/cherry_pick_spec.rb
index 1b4ff6b6f1b..e45e3a36d01 100644
--- a/spec/features/projects/commits/cherry_pick_spec.rb
+++ b/spec/features/projects/commits/cherry_pick_spec.rb
@@ -1,4 +1,5 @@
require 'spec_helper'
+include WaitForAjax
describe 'Cherry-pick Commits' do
let(:project) { create(:project) }
@@ -8,12 +9,11 @@ describe 'Cherry-pick Commits' do
before do
login_as :user
project.team << [@user, :master]
- visit namespace_project_commits_path(project.namespace, project, project.repository.root_ref, { limit: 5 })
+ visit namespace_project_commit_path(project.namespace, project, master_pickable_commit.id)
end
context "I cherry-pick a commit" 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
@@ -26,7 +26,6 @@ describe 'Cherry-pick Commits' do
context "I cherry-pick a merge commit" do
it do
- visit namespace_project_commit_path(project.namespace, project, master_pickable_merge.id)
find("a[href='#modal-cherry-pick-commit']").click
page.within('#modal-cherry-pick-commit') do
uncheck 'create_merge_request'
@@ -38,7 +37,6 @@ describe 'Cherry-pick Commits' do
context "I cherry-pick a commit that was previously cherry-picked" do
it do
- visit namespace_project_commit_path(project.namespace, project, master_pickable_commit.id)
find("a[href='#modal-cherry-pick-commit']").click
page.within('#modal-cherry-pick-commit') do
uncheck 'create_merge_request'
@@ -56,7 +54,6 @@ describe 'Cherry-pick Commits' do
context "I cherry-pick a commit in a new merge request" do
it do
- visit namespace_project_commit_path(project.namespace, project, master_pickable_commit.id)
find("a[href='#modal-cherry-pick-commit']").click
page.within('#modal-cherry-pick-commit') do
click_button 'Cherry-pick'
@@ -64,4 +61,28 @@ describe 'Cherry-pick Commits' do
expect(page).to have_content('The commit has been successfully cherry-picked. You can now submit a merge request to get this change into the original branch.')
end
end
+
+ context "I cherry-pick a commit from a different branch", js: true do
+ it do
+ find('.commit-action-buttons a.dropdown-toggle').click
+ find(:css, "a[href='#modal-cherry-pick-commit']").click
+
+ page.within('#modal-cherry-pick-commit') do
+ click_button 'master'
+ end
+
+ wait_for_ajax
+
+ page.within('#modal-cherry-pick-commit .dropdown-menu .dropdown-content') do
+ click_link 'feature'
+ end
+
+ page.within('#modal-cherry-pick-commit') do
+ uncheck 'create_merge_request'
+ click_button 'Cherry-pick'
+ end
+
+ expect(page).to have_content('The commit has been successfully cherry-picked.')
+ end
+ end
end
diff --git a/spec/features/projects/files/editing_a_file_spec.rb b/spec/features/projects/files/editing_a_file_spec.rb
new file mode 100644
index 00000000000..fe047e00409
--- /dev/null
+++ b/spec/features/projects/files/editing_a_file_spec.rb
@@ -0,0 +1,34 @@
+require 'spec_helper'
+
+feature 'User wants to edit a file', feature: true do
+ include WaitForAjax
+
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+ let(:commit_params) do
+ {
+ source_branch: project.default_branch,
+ target_branch: project.default_branch,
+ commit_message: "Committing First Update",
+ file_path: ".gitignore",
+ file_content: "First Update",
+ last_commit_sha: Gitlab::Git::Commit.last_for_path(project.repository, project.default_branch,
+ ".gitignore").sha
+ }
+ end
+
+ background do
+ project.team << [user, :master]
+ login_as user
+ visit namespace_project_edit_blob_path(project.namespace, project,
+ File.join(project.default_branch, '.gitignore'))
+ end
+
+ scenario 'file has been updated since the user opened the edit page' do
+ Files::UpdateService.new(project, user, commit_params).execute
+
+ click_button 'Commit Changes'
+
+ expect(page).to have_content 'Someone edited the file the same time you did.'
+ end
+end
diff --git a/spec/features/projects/files/files_sort_submodules_with_folders_spec.rb b/spec/features/projects/files/files_sort_submodules_with_folders_spec.rb
new file mode 100644
index 00000000000..10b91d8990b
--- /dev/null
+++ b/spec/features/projects/files/files_sort_submodules_with_folders_spec.rb
@@ -0,0 +1,29 @@
+require 'spec_helper'
+
+feature 'User views files page', feature: true do
+ include WaitForAjax
+
+ let(:user) { create(:user) }
+ let(:project) { create(:forked_project_with_submodules) }
+
+ before do
+ project.team << [user, :master]
+ login_as user
+ visit namespace_project_tree_path(project.namespace, project, project.repository.root_ref)
+ end
+
+ scenario 'user sees folders and submodules sorted together, followed by files' do
+ rows = all('td.tree-item-file-name').map(&:text)
+ tree = project.repository.tree
+
+ folders = tree.trees.map(&:name)
+ files = tree.blobs.map(&:name)
+ submodules = tree.submodules.map do |submodule|
+ submodule.name + " @ " + submodule.id[0..7]
+ end
+
+ sorted_titles = (folders + submodules).sort + files
+
+ expect(rows).to eq(sorted_titles)
+ 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 e1e105e6bbe..a521ce50f35 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
@@ -23,7 +23,7 @@ feature 'project owner creates a license file', feature: true, js: true do
select_template('MIT License')
- file_content = find('.file-content')
+ file_content = first('.file-editor')
expect(file_content).to have_content('The MIT License (MIT)')
expect(file_content).to have_content("Copyright (c) #{Time.now.year} #{project.namespace.human_name}")
@@ -39,6 +39,7 @@ feature 'project owner creates a license file', feature: true, js: true do
scenario 'project master creates a license file from the "Add license" link' do
click_link 'Add License'
+ expect(page).to have_content('New File')
expect(current_path).to eq(
namespace_project_new_blob_path(project.namespace, project, 'master'))
expect(find('#file_name').value).to eq('LICENSE')
@@ -46,7 +47,7 @@ feature 'project owner creates a license file', feature: true, js: true do
select_template('MIT License')
- file_content = find('.file-content')
+ file_content = first('.file-editor')
expect(file_content).to have_content('The MIT License (MIT)')
expect(file_content).to have_content("Copyright (c) #{Time.now.year} #{project.namespace.human_name}")
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 67aac25e427..4453b6d485f 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
@@ -14,6 +14,7 @@ feature 'project owner sees a link to create a license file in empty project', f
visit namespace_project_path(project.namespace, project)
click_link 'Create empty bare repository'
click_on 'LICENSE'
+ expect(page).to have_content('New File')
expect(current_path).to eq(
namespace_project_new_blob_path(project.namespace, project, 'master'))
@@ -22,7 +23,7 @@ feature 'project owner sees a link to create a license file in empty project', f
select_template('MIT License')
- file_content = find('.file-content')
+ file_content = first('.file-editor')
expect(file_content).to have_content('The MIT License (MIT)')
expect(file_content).to have_content("Copyright (c) #{Time.now.year} #{project.namespace.human_name}")
diff --git a/spec/features/projects/group_links_spec.rb b/spec/features/projects/group_links_spec.rb
new file mode 100644
index 00000000000..1a71a03fbd9
--- /dev/null
+++ b/spec/features/projects/group_links_spec.rb
@@ -0,0 +1,32 @@
+require 'spec_helper'
+
+feature 'Project group links', feature: true, js: true do
+ include Select2Helper
+
+ let(:master) { create(:user) }
+ let(:project) { create(:project) }
+ let!(:group) { create(:group) }
+
+ background do
+ project.team << [master, :master]
+ login_as(master)
+ end
+
+ context 'setting an expiration date for a group link' do
+ before do
+ visit namespace_project_group_links_path(project.namespace, project)
+
+ select2 group.id, from: '#link_group_id'
+ fill_in 'expires_at', with: (Time.current + 4.5.days).strftime('%Y-%m-%d')
+ page.find('body').click
+ click_on 'Share'
+ end
+
+ it 'shows the expiration time with a warning class' do
+ page.within('.enabled-groups') do
+ expect(page).to have_content('expires in 4 days')
+ expect(page).to have_selector('.text-warning')
+ end
+ end
+ end
+end
diff --git a/spec/features/projects/import_export/import_file_spec.rb b/spec/features/projects/import_export/import_file_spec.rb
index bc3bf53fe9d..f707ccf4e93 100644
--- a/spec/features/projects/import_export/import_file_spec.rb
+++ b/spec/features/projects/import_export/import_file_spec.rb
@@ -3,59 +3,97 @@ require 'spec_helper'
feature 'project import', feature: true, js: true do
include Select2Helper
- let(:user) { create(:admin) }
- let!(:namespace) { create(:namespace, name: "asd", owner: user) }
+ let(:admin) { create(:admin) }
+ let(:normal_user) { create(:user) }
+ let!(:namespace) { create(:namespace, name: "asd", owner: admin) }
let(:file) { File.join(Rails.root, 'spec', 'features', 'projects', 'import_export', 'test_project_export.tar.gz') }
let(:export_path) { "#{Dir::tmpdir}/import_file_spec" }
let(:project) { Project.last }
+ let(:project_hook) { Gitlab::Git::Hook.new('post-receive', project.repository.path) }
background do
allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path)
- login_as(user)
end
after(:each) do
FileUtils.rm_rf(export_path, secure: true)
end
- scenario 'user imports an exported project successfully' do
- expect(Project.all.count).to be_zero
+ context 'admin user' do
+ before do
+ login_as(admin)
+ end
+
+ scenario 'user imports an exported project successfully' do
+ expect(Project.all.count).to be_zero
+
+ visit new_project_path
+
+ select2('2', from: '#project_namespace_id')
+ fill_in :project_path, with: 'test-project-path', visible: true
+ click_link 'GitLab export'
+
+ expect(page).to have_content('GitLab project export')
+ expect(URI.parse(current_url).query).to eq('namespace_id=2&path=test-project-path')
+
+ attach_file('file', file)
+
+ click_on 'Import project' # import starts
+
+ expect(project).not_to be_nil
+ expect(project.issues).not_to be_empty
+ expect(project.merge_requests).not_to be_empty
+ expect(project_hook).to exist
+ expect(wiki_exists?).to be true
+ expect(project.import_status).to eq('finished')
+ end
+
+ scenario 'invalid project' do
+ project = create(:project, namespace_id: 2)
+
+ visit new_project_path
+
+ select2('2', from: '#project_namespace_id')
+ fill_in :project_path, with: project.name, visible: true
+ click_link 'GitLab export'
- visit new_project_path
+ attach_file('file', file)
+ click_on 'Import project'
- select2('2', from: '#project_namespace_id')
- fill_in :project_path, with: 'test-project-path', visible: true
- click_link 'GitLab export'
+ page.within('.flash-container') do
+ expect(page).to have_content('Project could not be imported')
+ end
+ end
+
+ scenario 'project with no name' do
+ create(:project, namespace_id: 2)
- expect(page).to have_content('GitLab project export')
- expect(URI.parse(current_url).query).to eq('namespace_id=2&path=test-project-path')
+ visit new_project_path
- attach_file('file', file)
+ select2('2', from: '#project_namespace_id')
- click_on 'Import project' # import starts
+ # click on disabled element
+ find(:link, 'GitLab export').trigger('click')
- expect(project).not_to be_nil
- expect(project.issues).not_to be_empty
- expect(project.merge_requests).not_to be_empty
- expect(project.repo_exists?).to be true
- expect(wiki_exists?).to be true
- expect(project.import_status).to eq('finished')
+ page.within('.flash-container') do
+ expect(page).to have_content('Please enter path and name')
+ end
+ end
end
- scenario 'invalid project' do
- project = create(:project, namespace_id: 2)
+ context 'normal user' do
+ before do
+ login_as(normal_user)
+ end
- visit new_project_path
+ scenario 'non-admin user is not allowed to import a project' do
+ expect(Project.all.count).to be_zero
- select2('2', from: '#project_namespace_id')
- fill_in :project_path, with: project.name, visible: true
- click_link 'GitLab export'
+ visit new_project_path
- attach_file('file', file)
- click_on 'Import project'
+ fill_in :project_path, with: 'test-project-path', visible: true
- page.within('.flash-container') do
- expect(page).to have_content('Project could not be imported')
+ expect(page).not_to have_content('GitLab export')
end
end
diff --git a/spec/features/projects/issuable_templates_spec.rb b/spec/features/projects/issuable_templates_spec.rb
new file mode 100644
index 00000000000..4a83740621a
--- /dev/null
+++ b/spec/features/projects/issuable_templates_spec.rb
@@ -0,0 +1,89 @@
+require 'spec_helper'
+
+feature 'issuable templates', feature: true, js: true do
+ include WaitForAjax
+
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :public) }
+
+ before do
+ project.team << [user, :master]
+ login_as user
+ end
+
+ context 'user creates an issue using templates' do
+ let(:template_content) { 'this is a test "bug" template' }
+ let(:issue) { create(:issue, author: user, assignee: user, project: project) }
+
+ background do
+ project.repository.commit_file(user, '.gitlab/issue_templates/bug.md', template_content, 'added issue template', 'master', false)
+ visit edit_namespace_project_issue_path project.namespace, project, issue
+ fill_in :'issue[title]', with: 'test issue title'
+ end
+
+ scenario 'user selects "bug" template' do
+ select_template 'bug'
+ wait_for_ajax
+ preview_template
+ save_changes
+ end
+ end
+
+ context 'user creates a merge request using templates' do
+ let(:template_content) { 'this is a test "feature-proposal" template' }
+ let(:merge_request) { create(:merge_request, :with_diffs, source_project: project) }
+
+ background do
+ project.repository.commit_file(user, '.gitlab/merge_request_templates/feature-proposal.md', template_content, 'added merge request template', 'master', false)
+ visit edit_namespace_project_merge_request_path project.namespace, project, merge_request
+ fill_in :'merge_request[title]', with: 'test merge request title'
+ end
+
+ scenario 'user selects "feature-proposal" template' do
+ select_template 'feature-proposal'
+ wait_for_ajax
+ preview_template
+ save_changes
+ end
+ end
+
+ context 'user creates a merge request from a forked project using templates' do
+ let(:template_content) { 'this is a test "feature-proposal" template' }
+ let(:fork_user) { create(:user) }
+ let(:fork_project) { create(:project, :public) }
+ let(:merge_request) { create(:merge_request, :with_diffs, source_project: fork_project) }
+
+ background do
+ logout
+ project.team << [fork_user, :developer]
+ fork_project.team << [fork_user, :master]
+ create(:forked_project_link, forked_to_project: fork_project, forked_from_project: project)
+ login_as fork_user
+ fork_project.repository.commit_file(fork_user, '.gitlab/merge_request_templates/feature-proposal.md', template_content, 'added merge request template', 'master', false)
+ visit edit_namespace_project_merge_request_path fork_project.namespace, fork_project, merge_request
+ fill_in :'merge_request[title]', with: 'test merge request title'
+ end
+
+ scenario 'user selects "feature-proposal" template' do
+ select_template 'feature-proposal'
+ wait_for_ajax
+ preview_template
+ save_changes
+ end
+ end
+
+ def preview_template
+ click_link 'Preview'
+ expect(page).to have_content template_content
+ end
+
+ def save_changes
+ click_button "Save changes"
+ expect(page).to have_content template_content
+ end
+
+ def select_template(name)
+ first('.js-issuable-selector').click
+ first('.js-issuable-selector-wrap .dropdown-content a', text: name).click
+ end
+end
diff --git a/spec/features/projects/issues/list_spec.rb b/spec/features/projects/issues/list_spec.rb
new file mode 100644
index 00000000000..3137af074ca
--- /dev/null
+++ b/spec/features/projects/issues/list_spec.rb
@@ -0,0 +1,20 @@
+require 'spec_helper'
+
+feature 'Issues List' do
+ let(:user) { create(:user) }
+ let(:project) { create(:empty_project) }
+
+ background do
+ project.team << [user, :developer]
+
+ login_as(user)
+ end
+
+ scenario 'user does not see create new list button' do
+ create(:issue, project: project)
+
+ visit namespace_project_issues_path(project.namespace, project)
+
+ expect(page).not_to have_selector('.js-new-board-list')
+ end
+end
diff --git a/spec/features/projects/labels/update_prioritization_spec.rb b/spec/features/projects/labels/update_prioritization_spec.rb
index 6a39c302f55..cb7495da8eb 100644
--- a/spec/features/projects/labels/update_prioritization_spec.rb
+++ b/spec/features/projects/labels/update_prioritization_spec.rb
@@ -76,7 +76,7 @@ feature 'Prioritize labels', feature: true do
expect(page.all('li').last).to have_content('bug')
end
- visit current_url
+ refresh
wait_for_ajax
page.within('.prioritized-labels') do
@@ -87,7 +87,7 @@ feature 'Prioritize labels', feature: true do
end
context 'as a guest' do
- it 'can not prioritize labels' do
+ it 'does not prioritize labels' do
user = create(:user)
guest = create(:user)
project = create(:project, name: 'test', namespace: user.namespace)
@@ -102,7 +102,7 @@ feature 'Prioritize labels', feature: true do
end
context 'as a non signed in user' do
- it 'can not prioritize labels' do
+ it 'does not prioritize labels' do
user = create(:user)
project = create(:project, name: 'test', namespace: user.namespace)
diff --git a/spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb b/spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb
new file mode 100644
index 00000000000..430c384ac2e
--- /dev/null
+++ b/spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb
@@ -0,0 +1,45 @@
+require 'spec_helper'
+
+feature 'Projects > Members > Master adds member with expiration date', feature: true, js: true do
+ include Select2Helper
+ include ActiveSupport::Testing::TimeHelpers
+
+ let(:master) { create(:user) }
+ let(:project) { create(:project) }
+ let!(:new_member) { create(:user) }
+
+ background do
+ project.team << [master, :master]
+ login_as(master)
+ end
+
+ scenario 'expiration date is displayed in the members list' do
+ travel_to Time.zone.parse('2016-08-06 08:00') do
+ visit namespace_project_project_members_path(project.namespace, project)
+
+ page.within '.users-project-form' do
+ select2(new_member.id, from: '#user_ids', multiple: true)
+ fill_in 'expires_at', with: '2016-08-10'
+ click_on 'Add users to project'
+ end
+
+ page.within '.project_member:first-child' do
+ expect(page).to have_content('Expires in 4 days')
+ end
+ end
+ end
+
+ scenario 'change expiration date' do
+ travel_to Time.zone.parse('2016-08-06 08:00') do
+ project.team.add_users([new_member.id], :developer, expires_at: '2016-09-06')
+ visit namespace_project_project_members_path(project.namespace, project)
+
+ page.within '.project_member:first-child' do
+ click_on 'Edit'
+ fill_in 'Access expiration date', with: '2016-08-09'
+ click_on 'Save'
+ expect(page).to have_content('Expires in 3 days')
+ end
+ end
+ end
+end
diff --git a/spec/features/projects/members/user_requests_access_spec.rb b/spec/features/projects/members/user_requests_access_spec.rb
index f2fe3ef364d..56ede8eb5be 100644
--- a/spec/features/projects/members/user_requests_access_spec.rb
+++ b/spec/features/projects/members/user_requests_access_spec.rb
@@ -11,6 +11,13 @@ feature 'Projects > Members > User requests access', feature: true do
visit namespace_project_path(project.namespace, project)
end
+ scenario 'request access feature is disabled' do
+ project.update_attributes(request_access_enabled: false)
+ visit namespace_project_path(project.namespace, project)
+
+ expect(page).not_to have_content 'Request Access'
+ end
+
scenario 'user can request access to a project' do
perform_enqueued_jobs { click_link 'Request Access' }
diff --git a/spec/features/projects/merge_requests/list_spec.rb b/spec/features/projects/merge_requests/list_spec.rb
new file mode 100644
index 00000000000..5dd58ad66a7
--- /dev/null
+++ b/spec/features/projects/merge_requests/list_spec.rb
@@ -0,0 +1,20 @@
+require 'spec_helper'
+
+feature 'Merge Requests List' do
+ let(:user) { create(:user) }
+ let(:project) { create(:project) }
+
+ background do
+ project.team << [user, :developer]
+
+ login_as(user)
+ end
+
+ scenario 'user does not see create new list button' do
+ create(:merge_request, source_project: project)
+
+ visit namespace_project_merge_requests_path(project.namespace, project)
+
+ expect(page).not_to have_selector('.js-new-board-list')
+ end
+end
diff --git a/spec/features/pipelines_spec.rb b/spec/features/projects/pipelines_spec.rb
index e7ee0aaea3c..47482bc3cc9 100644
--- a/spec/features/pipelines_spec.rb
+++ b/spec/features/projects/pipelines_spec.rb
@@ -12,7 +12,7 @@ describe "Pipelines" do
end
describe 'GET /:project/pipelines' do
- let!(:pipeline) { create(:ci_pipeline, project: project, ref: 'master', status: 'running') }
+ let!(:pipeline) { create(:ci_empty_pipeline, project: project, ref: 'master', status: 'running') }
[:all, :running, :branches].each do |scope|
context "displaying #{scope}" do
@@ -31,9 +31,12 @@ describe "Pipelines" do
end
context 'cancelable pipeline' do
- let!(:running) { create(:ci_build, :running, pipeline: pipeline, stage: 'test', commands: 'test') }
+ let!(:build) { create(:ci_build, pipeline: pipeline, stage: 'test', commands: 'test') }
- before { visit namespace_project_pipelines_path(project.namespace, project) }
+ before do
+ build.run
+ visit namespace_project_pipelines_path(project.namespace, project)
+ end
it { expect(page).to have_link('Cancel') }
it { expect(page).to have_selector('.ci-running') }
@@ -47,9 +50,12 @@ describe "Pipelines" do
end
context 'retryable pipelines' do
- let!(:failed) { create(:ci_build, :failed, pipeline: pipeline, stage: 'test', commands: 'test') }
+ let!(:build) { create(:ci_build, pipeline: pipeline, stage: 'test', commands: 'test') }
- before { visit namespace_project_pipelines_path(project.namespace, project) }
+ before do
+ build.drop
+ visit namespace_project_pipelines_path(project.namespace, project)
+ end
it { expect(page).to have_link('Retry') }
it { expect(page).to have_selector('.ci-failed') }
@@ -58,7 +64,21 @@ describe "Pipelines" do
before { click_link('Retry') }
it { expect(page).not_to have_link('Retry') }
- it { expect(page).to have_selector('.ci-pending') }
+ it { expect(page).to have_selector('.ci-running') }
+ end
+ end
+
+ context 'with manual actions' do
+ let!(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'manual build', stage: 'test', commands: 'test') }
+
+ before { visit namespace_project_pipelines_path(project.namespace, project) }
+
+ it { expect(page).to have_link('Manual build') }
+
+ context 'when playing' do
+ before { click_link('Manual build') }
+
+ it { expect(manual.reload).to be_pending }
end
end
@@ -66,27 +86,32 @@ describe "Pipelines" 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) }
+ before do
+ visit namespace_project_pipelines_path(project.namespace, project)
+ end
- it 'not be cancelable' do
+ it 'is not cancelable' do
expect(page).not_to have_link('Cancel')
end
- it 'pipeline is running' do
+ it 'has pipeline 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') }
+ let!(:status) { create(:generic_commit_status, :pending, pipeline: pipeline, stage: 'test') }
- before { visit namespace_project_pipelines_path(project.namespace, project) }
+ before do
+ status.drop
+ visit namespace_project_pipelines_path(project.namespace, project)
+ end
- it 'not be retryable' do
+ it 'is not retryable' do
expect(page).not_to have_link('Retry')
end
- it 'pipeline is failed' do
+ it 'has failed pipeline' do
expect(page).to have_selector('.ci-failed')
end
end
@@ -102,9 +127,19 @@ describe "Pipelines" do
it { expect(page).to have_link(with_artifacts.name) }
end
+ context 'with artifacts expired' do
+ let!(:with_artifacts_expired) { create(:ci_build, :artifacts_expired, :success, pipeline: pipeline, name: 'rspec', stage: 'test') }
+
+ before { visit namespace_project_pipelines_path(project.namespace, project) }
+
+ it { expect(page).not_to have_selector('.build-artifacts') }
+ end
+
context 'without artifacts' do
let!(:without_artifacts) { create(:ci_build, :success, pipeline: pipeline, name: 'rspec', stage: 'test') }
+ before { visit namespace_project_pipelines_path(project.namespace, project) }
+
it { expect(page).not_to have_selector('.build-artifacts') }
end
end
@@ -117,12 +152,13 @@ describe "Pipelines" 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')
+ @manual = create(:ci_build, :manual, pipeline: pipeline, stage: 'deploy', name: 'manual build')
@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
+ it 'shows a list of builds' do
expect(page).to have_content('Test')
expect(page).to have_content(@success.id)
expect(page).to have_content('Deploy')
@@ -131,6 +167,7 @@ describe "Pipelines" do
expect(page).to have_content(@external.id)
expect(page).to have_content('Retry failed')
expect(page).to have_content('Cancel running')
+ expect(page).to have_link('Play')
end
context 'retrying builds' do
@@ -154,6 +191,16 @@ describe "Pipelines" do
it { expect(page).to have_selector('.ci-canceled') }
end
end
+
+ context 'playing manual build' do
+ before do
+ within '.pipeline-holder' do
+ click_link('Play')
+ end
+ end
+
+ it { expect(@manual.reload).to be_pending }
+ end
end
describe 'POST /:project/pipelines' do
@@ -162,7 +209,7 @@ describe "Pipelines" do
before { visit new_namespace_project_pipeline_path(project.namespace, project) }
context 'for valid commit' do
- before { fill_in('Create for', with: 'master') }
+ before { fill_in('pipeline[ref]', with: 'master') }
context 'with gitlab-ci.yml' do
before { stub_ci_pipeline_to_return_yaml_file }
@@ -179,11 +226,37 @@ describe "Pipelines" do
context 'for invalid commit' do
before do
- fill_in('Create for', with: 'invalid reference')
+ fill_in('pipeline[ref]', with: 'invalid-reference')
click_on 'Create pipeline'
end
it { expect(page).to have_content('Reference not found') }
end
end
+
+ describe 'Create pipelines', feature: true do
+ let(:project) { create(:project) }
+
+ before do
+ visit new_namespace_project_pipeline_path(project.namespace, project)
+ end
+
+ describe 'new pipeline page' do
+ it 'has field to add a new pipeline' do
+ expect(page).to have_field('pipeline[ref]')
+ expect(page).to have_content('Create for')
+ end
+ end
+
+ describe 'find pipelines' do
+ it 'shows filtered pipelines', js: true do
+ fill_in('pipeline[ref]', with: 'fix')
+ find('input#ref').native.send_keys(:keydown)
+
+ within('.ui-autocomplete') do
+ expect(page).to have_selector('li', text: 'fix')
+ end
+ end
+ end
+ end
end
diff --git a/spec/features/projects/project_settings_spec.rb b/spec/features/projects/project_settings_spec.rb
new file mode 100644
index 00000000000..3de25d7af7d
--- /dev/null
+++ b/spec/features/projects/project_settings_spec.rb
@@ -0,0 +1,41 @@
+require 'spec_helper'
+
+describe 'Edit Project Settings', feature: true do
+ let(:user) { create(:user) }
+ let(:project) { create(:empty_project, path: 'gitlab', name: 'sample') }
+
+ before do
+ login_as(user)
+ project.team << [user, :master]
+ end
+
+ describe 'Project settings', js: true do
+ it 'shows errors for invalid project name' do
+ visit edit_namespace_project_path(project.namespace, project)
+
+ fill_in 'project_name_edit', with: 'foo&bar'
+
+ click_button 'Save changes'
+
+ expect(page).to have_field 'project_name_edit', with: 'foo&bar'
+ expect(page).to have_content "Name can contain only letters, digits, '_', '.', dash and space. It must start with letter, digit or '_'."
+ expect(page).to have_button 'Save changes'
+ end
+ end
+
+ describe 'Rename repository' do
+ it 'shows errors for invalid project path/name' do
+ visit edit_namespace_project_path(project.namespace, project)
+
+ fill_in 'Project name', with: 'foo&bar'
+ fill_in 'Path', with: 'foo&bar'
+
+ click_button 'Rename project'
+
+ expect(page).to have_field 'Project name', with: 'foo&bar'
+ expect(page).to have_field 'Path', with: 'foo&bar'
+ expect(page).to have_content "Name can contain only letters, digits, '_', '.', dash and space. It must start with letter, digit or '_'."
+ expect(page).to have_content "Path can contain only letters, digits, '_', '-' and '.'. Cannot start with '-', end in '.git' or end in '.atom'"
+ end
+ end
+end
diff --git a/spec/features/projects/ref_switcher_spec.rb b/spec/features/projects/ref_switcher_spec.rb
new file mode 100644
index 00000000000..b3ba40b35af
--- /dev/null
+++ b/spec/features/projects/ref_switcher_spec.rb
@@ -0,0 +1,29 @@
+require 'rails_helper'
+
+feature 'Ref switcher', feature: true, js: true do
+ include WaitForAjax
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :public) }
+
+ before do
+ project.team << [user, :master]
+ login_as(user)
+ visit namespace_project_tree_path(project.namespace, project, 'master')
+ end
+
+ it 'allow user to change ref by enter key' do
+ click_button 'master'
+ wait_for_ajax
+
+ page.within '.project-refs-form' do
+ input = find('input[type="search"]')
+ input.set 'expand'
+
+ input.native.send_keys :down
+ input.native.send_keys :down
+ input.native.send_keys :enter
+
+ expect(page).to have_content 'expand-collapse-files'
+ end
+ end
+end
diff --git a/spec/features/projects/slack_service/slack_service_spec.rb b/spec/features/projects/slack_service/slack_service_spec.rb
new file mode 100644
index 00000000000..16541f51d98
--- /dev/null
+++ b/spec/features/projects/slack_service/slack_service_spec.rb
@@ -0,0 +1,26 @@
+require 'spec_helper'
+
+feature 'Projects > Slack service > Setup events', feature: true do
+ let(:user) { create(:user) }
+ let(:service) { SlackService.new }
+ let(:project) { create(:project, slack_service: service) }
+
+ background do
+ service.fields
+ service.update_attributes(push_channel: 1, issue_channel: 2, merge_request_channel: 3, note_channel: 4, tag_push_channel: 5, build_channel: 6, wiki_page_channel: 7)
+ project.team << [user, :master]
+ login_as(user)
+ end
+
+ scenario 'user can filter events by channel' do
+ visit edit_namespace_project_service_path(project.namespace, project, service)
+
+ expect(page.find_field("service_push_channel").value).to have_content '1'
+ expect(page.find_field("service_issue_channel").value).to have_content '2'
+ expect(page.find_field("service_merge_request_channel").value).to have_content '3'
+ expect(page.find_field("service_note_channel").value).to have_content '4'
+ expect(page.find_field("service_tag_push_channel").value).to have_content '5'
+ expect(page.find_field("service_build_channel").value).to have_content '6'
+ expect(page.find_field("service_wiki_page_channel").value).to have_content '7'
+ end
+end
diff --git a/spec/features/projects/wiki/markdown_preview_spec.rb b/spec/features/projects/wiki/markdown_preview_spec.rb
new file mode 100644
index 00000000000..a1c386ddc18
--- /dev/null
+++ b/spec/features/projects/wiki/markdown_preview_spec.rb
@@ -0,0 +1,140 @@
+require 'spec_helper'
+
+feature 'Projects > Wiki > User previews markdown changes', feature: true, js: true do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, namespace: user.namespace) }
+ let(:wiki_content) do
+ <<-HEREDOC
+[regular link](regular)
+[relative link 1](../relative)
+[relative link 2](./relative)
+[relative link 3](./e/f/relative)
+ HEREDOC
+ end
+
+ background do
+ project.team << [user, :master]
+ login_as(user)
+
+ visit namespace_project_path(project.namespace, project)
+ click_link 'Wiki'
+ WikiPages::CreateService.new(project, user, title: 'home', content: 'Home page').execute
+ end
+
+ context "while creating a new wiki page" do
+ context "when there are no spaces or hyphens in the page name" do
+ it "rewrites relative links as expected" do
+ click_link 'New Page'
+ fill_in :new_wiki_path, with: 'a/b/c/d'
+ click_button 'Create Page'
+
+ fill_in :wiki_content, with: wiki_content
+ click_on "Preview"
+
+ expect(page).to have_content("regular link")
+
+ expect(page.html).to include("<a href=\"/#{project.path_with_namespace}/wikis/regular\">regular link</a>")
+ expect(page.html).to include("<a href=\"/#{project.path_with_namespace}/wikis/a/b/relative\">relative link 1</a>")
+ expect(page.html).to include("<a href=\"/#{project.path_with_namespace}/wikis/a/b/c/relative\">relative link 2</a>")
+ expect(page.html).to include("<a href=\"/#{project.path_with_namespace}/wikis/a/b/c/e/f/relative\">relative link 3</a>")
+ end
+ end
+
+ context "when there are spaces in the page name" do
+ it "rewrites relative links as expected" do
+ click_link 'New Page'
+ fill_in :new_wiki_path, with: 'a page/b page/c page/d page'
+ click_button 'Create Page'
+
+ fill_in :wiki_content, with: wiki_content
+ click_on "Preview"
+
+ expect(page).to have_content("regular link")
+
+ expect(page.html).to include("<a href=\"/#{project.path_with_namespace}/wikis/regular\">regular link</a>")
+ expect(page.html).to include("<a href=\"/#{project.path_with_namespace}/wikis/a-page/b-page/relative\">relative link 1</a>")
+ expect(page.html).to include("<a href=\"/#{project.path_with_namespace}/wikis/a-page/b-page/c-page/relative\">relative link 2</a>")
+ expect(page.html).to include("<a href=\"/#{project.path_with_namespace}/wikis/a-page/b-page/c-page/e/f/relative\">relative link 3</a>")
+ end
+ end
+
+ context "when there are hyphens in the page name" do
+ it "rewrites relative links as expected" do
+ click_link 'New Page'
+ fill_in :new_wiki_path, with: 'a-page/b-page/c-page/d-page'
+ click_button 'Create Page'
+
+ fill_in :wiki_content, with: wiki_content
+ click_on "Preview"
+
+ expect(page).to have_content("regular link")
+
+ expect(page.html).to include("<a href=\"/#{project.path_with_namespace}/wikis/regular\">regular link</a>")
+ expect(page.html).to include("<a href=\"/#{project.path_with_namespace}/wikis/a-page/b-page/relative\">relative link 1</a>")
+ expect(page.html).to include("<a href=\"/#{project.path_with_namespace}/wikis/a-page/b-page/c-page/relative\">relative link 2</a>")
+ expect(page.html).to include("<a href=\"/#{project.path_with_namespace}/wikis/a-page/b-page/c-page/e/f/relative\">relative link 3</a>")
+ end
+ end
+ end
+
+ context "while editing a wiki page" do
+ def create_wiki_page(path)
+ click_link 'New Page'
+ fill_in :new_wiki_path, with: path
+ click_button 'Create Page'
+ fill_in :wiki_content, with: 'content'
+ click_on "Create page"
+ end
+
+ context "when there are no spaces or hyphens in the page name" do
+ it "rewrites relative links as expected" do
+ create_wiki_page 'a/b/c/d'
+ click_link 'Edit'
+
+ fill_in :wiki_content, with: wiki_content
+ click_on "Preview"
+
+ expect(page).to have_content("regular link")
+
+ expect(page.html).to include("<a href=\"/#{project.path_with_namespace}/wikis/regular\">regular link</a>")
+ expect(page.html).to include("<a href=\"/#{project.path_with_namespace}/wikis/a/b/relative\">relative link 1</a>")
+ expect(page.html).to include("<a href=\"/#{project.path_with_namespace}/wikis/a/b/c/relative\">relative link 2</a>")
+ expect(page.html).to include("<a href=\"/#{project.path_with_namespace}/wikis/a/b/c/e/f/relative\">relative link 3</a>")
+ end
+ end
+
+ context "when there are spaces in the page name" do
+ it "rewrites relative links as expected" do
+ create_wiki_page 'a page/b page/c page/d page'
+ click_link 'Edit'
+
+ fill_in :wiki_content, with: wiki_content
+ click_on "Preview"
+
+ expect(page).to have_content("regular link")
+
+ expect(page.html).to include("<a href=\"/#{project.path_with_namespace}/wikis/regular\">regular link</a>")
+ expect(page.html).to include("<a href=\"/#{project.path_with_namespace}/wikis/a-page/b-page/relative\">relative link 1</a>")
+ expect(page.html).to include("<a href=\"/#{project.path_with_namespace}/wikis/a-page/b-page/c-page/relative\">relative link 2</a>")
+ expect(page.html).to include("<a href=\"/#{project.path_with_namespace}/wikis/a-page/b-page/c-page/e/f/relative\">relative link 3</a>")
+ end
+ end
+
+ context "when there are hyphens in the page name" do
+ it "rewrites relative links as expected" do
+ create_wiki_page 'a-page/b-page/c-page/d-page'
+ click_link 'Edit'
+
+ fill_in :wiki_content, with: wiki_content
+ click_on "Preview"
+
+ expect(page).to have_content("regular link")
+
+ expect(page.html).to include("<a href=\"/#{project.path_with_namespace}/wikis/regular\">regular link</a>")
+ expect(page.html).to include("<a href=\"/#{project.path_with_namespace}/wikis/a-page/b-page/relative\">relative link 1</a>")
+ expect(page.html).to include("<a href=\"/#{project.path_with_namespace}/wikis/a-page/b-page/c-page/relative\">relative link 2</a>")
+ expect(page.html).to include("<a href=\"/#{project.path_with_namespace}/wikis/a-page/b-page/c-page/e/f/relative\">relative link 3</a>")
+ end
+ end
+ end
+end
diff --git a/spec/features/projects/wiki/user_creates_wiki_page_spec.rb b/spec/features/projects/wiki/user_creates_wiki_page_spec.rb
index 7e6eef65873..7afd83b7250 100644
--- a/spec/features/projects/wiki/user_creates_wiki_page_spec.rb
+++ b/spec/features/projects/wiki/user_creates_wiki_page_spec.rb
@@ -30,18 +30,48 @@ feature 'Projects > Wiki > User creates wiki page', feature: true do
WikiPages::CreateService.new(project, user, title: 'home', content: 'Home page').execute
end
- scenario 'via the "new wiki page" page', js: true do
- click_link 'New Page'
+ context 'via the "new wiki page" page' do
+ scenario 'when the wiki page has a single word name', js: true do
+ click_link 'New Page'
- fill_in :new_wiki_path, with: 'foo'
- click_button 'Create Page'
+ fill_in :new_wiki_path, with: 'foo'
+ click_button 'Create Page'
- fill_in :wiki_content, with: 'My awesome wiki!'
- click_button 'Create page'
+ fill_in :wiki_content, with: 'My awesome wiki!'
+ click_button 'Create page'
- expect(page).to have_content('Foo')
- expect(page).to have_content("last edited by #{user.name}")
- expect(page).to have_content('My awesome wiki!')
+ expect(page).to have_content('Foo')
+ expect(page).to have_content("last edited by #{user.name}")
+ expect(page).to have_content('My awesome wiki!')
+ end
+
+ scenario 'when the wiki page has spaces in the name', js: true do
+ click_link 'New Page'
+
+ fill_in :new_wiki_path, with: 'Spaces in the name'
+ click_button 'Create Page'
+
+ fill_in :wiki_content, with: 'My awesome wiki!'
+ click_button 'Create page'
+
+ expect(page).to have_content('Spaces in the name')
+ expect(page).to have_content("last edited by #{user.name}")
+ expect(page).to have_content('My awesome wiki!')
+ end
+
+ scenario 'when the wiki page has hyphens in the name', js: true do
+ click_link 'New Page'
+
+ fill_in :new_wiki_path, with: 'hyphens-in-the-name'
+ click_button 'Create Page'
+
+ fill_in :wiki_content, with: 'My awesome wiki!'
+ click_button 'Create page'
+
+ expect(page).to have_content('Hyphens in the name')
+ expect(page).to have_content("last edited by #{user.name}")
+ expect(page).to have_content('My awesome wiki!')
+ end
end
end
end
diff --git a/spec/features/projects_spec.rb b/spec/features/projects_spec.rb
index 6fa8298d489..e00d85904d5 100644
--- a/spec/features/projects_spec.rb
+++ b/spec/features/projects_spec.rb
@@ -44,7 +44,7 @@ feature 'Project', feature: true do
visit edit_namespace_project_path(project.namespace, project)
end
- it 'should remove fork' do
+ it 'removes fork' do
expect(page).to have_content 'Remove fork relationship'
remove_with_confirm('Remove fork relationship', project.path)
@@ -65,7 +65,7 @@ feature 'Project', feature: true do
visit edit_namespace_project_path(project.namespace, project)
end
- it 'should remove project' do
+ it 'removes project' do
expect { remove_with_confirm('Remove project', project.path) }.to change {Project.count}.by(-1)
end
end
@@ -82,7 +82,7 @@ feature 'Project', feature: true do
visit namespace_project_path(project.namespace, project)
end
- it 'click toggle and show dropdown', js: true do
+ it 'clicks toggle and shows dropdown', js: true do
find('.js-projects-dropdown-toggle').click
expect(page).to have_css('.dropdown-menu-projects .dropdown-content li', count: 1)
end
@@ -102,7 +102,7 @@ feature 'Project', feature: true do
visit namespace_project_issue_path(project.namespace, project, issue)
end
- it 'click toggle and show dropdown' do
+ it 'clicks toggle and shows dropdown' do
find('.js-projects-dropdown-toggle').click
expect(page).to have_css('.dropdown-menu-projects .dropdown-content li', count: 2)
@@ -115,6 +115,35 @@ feature 'Project', feature: true do
end
end
+ describe 'tree view (default view is set to Files)' do
+ let(:user) { create(:user, project_view: 'files') }
+ let(:project) { create(:forked_project_with_submodules) }
+
+ before do
+ project.team << [user, :master]
+ login_as user
+ visit namespace_project_path(project.namespace, project)
+ end
+
+ it 'has working links to files' do
+ click_link('PROCESS.md')
+
+ expect(page.status_code).to eq(200)
+ end
+
+ it 'has working links to directories' do
+ click_link('encoding')
+
+ expect(page.status_code).to eq(200)
+ end
+
+ it 'has working links to submodules' do
+ click_link('645f6c4c')
+
+ expect(page.status_code).to eq(200)
+ end
+ end
+
def remove_with_confirm(button_text, confirm_with)
click_button button_text
fill_in 'confirm_name_input', with: confirm_with
diff --git a/spec/features/protected_branches/access_control_ce_spec.rb b/spec/features/protected_branches/access_control_ce_spec.rb
new file mode 100644
index 00000000000..395c61a4743
--- /dev/null
+++ b/spec/features/protected_branches/access_control_ce_spec.rb
@@ -0,0 +1,71 @@
+RSpec.shared_examples "protected branches > access control > CE" do
+ ProtectedBranch::PushAccessLevel.human_access_levels.each do |(access_type_id, access_type_name)|
+ it "allows creating protected branches that #{access_type_name} can push to" do
+ visit namespace_project_protected_branches_path(project.namespace, project)
+ set_protected_branch_name('master')
+ within('.new_protected_branch') do
+ allowed_to_push_button = find(".js-allowed-to-push")
+
+ unless allowed_to_push_button.text == access_type_name
+ allowed_to_push_button.click
+ within(".dropdown.open .dropdown-menu") { click_on access_type_name }
+ end
+ end
+ click_on "Protect"
+
+ expect(ProtectedBranch.count).to eq(1)
+ expect(ProtectedBranch.last.push_access_levels.map(&:access_level)).to eq([access_type_id])
+ end
+
+ it "allows updating protected branches so that #{access_type_name} can push to them" do
+ visit namespace_project_protected_branches_path(project.namespace, project)
+ set_protected_branch_name('master')
+ click_on "Protect"
+
+ expect(ProtectedBranch.count).to eq(1)
+
+ within(".protected-branches-list") do
+ find(".js-allowed-to-push").click
+ within('.js-allowed-to-push-container') { click_on access_type_name }
+ end
+
+ wait_for_ajax
+ expect(ProtectedBranch.last.push_access_levels.map(&:access_level)).to include(access_type_id)
+ end
+ end
+
+ ProtectedBranch::MergeAccessLevel.human_access_levels.each do |(access_type_id, access_type_name)|
+ it "allows creating protected branches that #{access_type_name} can merge to" do
+ visit namespace_project_protected_branches_path(project.namespace, project)
+ set_protected_branch_name('master')
+ within('.new_protected_branch') do
+ allowed_to_merge_button = find(".js-allowed-to-merge")
+
+ unless allowed_to_merge_button.text == access_type_name
+ allowed_to_merge_button.click
+ within(".dropdown.open .dropdown-menu") { click_on access_type_name }
+ end
+ end
+ click_on "Protect"
+
+ expect(ProtectedBranch.count).to eq(1)
+ expect(ProtectedBranch.last.merge_access_levels.map(&:access_level)).to eq([access_type_id])
+ end
+
+ it "allows updating protected branches so that #{access_type_name} can merge to them" do
+ visit namespace_project_protected_branches_path(project.namespace, project)
+ set_protected_branch_name('master')
+ click_on "Protect"
+
+ expect(ProtectedBranch.count).to eq(1)
+
+ within(".protected-branches-list") do
+ find(".js-allowed-to-merge").click
+ within('.js-allowed-to-merge-container') { click_on access_type_name }
+ end
+
+ wait_for_ajax
+ expect(ProtectedBranch.last.merge_access_levels.map(&:access_level)).to include(access_type_id)
+ end
+ end
+end
diff --git a/spec/features/protected_branches_spec.rb b/spec/features/protected_branches_spec.rb
index d94dee0c797..1a3f7b970f6 100644
--- a/spec/features/protected_branches_spec.rb
+++ b/spec/features/protected_branches_spec.rb
@@ -1,6 +1,9 @@
require 'spec_helper'
+Dir["./spec/features/protected_branches/*.rb"].sort.each { |f| require f }
feature 'Projected Branches', feature: true, js: true do
+ include WaitForAjax
+
let(:user) { create(:user, :admin) }
let(:project) { create(:project) }
@@ -9,7 +12,7 @@ feature 'Projected Branches', feature: true, js: true do
def set_protected_branch_name(branch_name)
find(".js-protected-branch-select").click
find(".dropdown-input-field").set(branch_name)
- click_on "Create Protected Branch: #{branch_name}"
+ click_on("Create wildcard #{branch_name}")
end
describe "explicit protected branches" do
@@ -69,7 +72,10 @@ feature 'Projected Branches', feature: true, js: true do
project.repository.add_branch(user, 'production-stable', 'master')
project.repository.add_branch(user, 'staging-stable', 'master')
project.repository.add_branch(user, 'development', 'master')
- create(:protected_branch, project: project, name: "*-stable")
+
+ visit namespace_project_protected_branches_path(project.namespace, project)
+ set_protected_branch_name('*-stable')
+ click_on "Protect"
visit namespace_project_protected_branches_path(project.namespace, project)
click_on "2 matching branches"
@@ -81,4 +87,8 @@ feature 'Projected Branches', feature: true, js: true do
end
end
end
+
+ describe "access control" do
+ include_examples "protected branches > access control > CE"
+ end
end
diff --git a/spec/features/search_spec.rb b/spec/features/search_spec.rb
index d0a301038c4..dcd3a2f17b0 100644
--- a/spec/features/search_spec.rb
+++ b/spec/features/search_spec.rb
@@ -12,7 +12,7 @@ describe "Search", feature: true do
visit search_path
end
- it 'top right search form is not present' do
+ it 'does not show top right search form' do
expect(page).not_to have_selector('.search')
end
@@ -28,6 +28,26 @@ describe "Search", feature: true do
end
context 'search for comments' do
+ context 'when comment belongs to a invalid commit' do
+ let(:note) { create(:note_on_commit, author: user, project: project, commit_id: project.repository.commit.id, note: 'Bug here') }
+
+ before { note.update_attributes(commit_id: 12345678) }
+
+ it 'finds comment' do
+ visit namespace_project_path(project.namespace, project)
+
+ page.within '.search' do
+ fill_in 'search', with: note.note
+ click_button 'Go'
+ end
+
+ click_link 'Comments'
+
+ expect(page).to have_text("Commit deleted")
+ expect(page).to have_text("12345678")
+ end
+ end
+
it 'finds a snippet' do
snippet = create(:project_snippet, :private, project: project, author: user, title: 'Some title')
note = create(:note,
@@ -51,21 +71,31 @@ describe "Search", feature: true do
end
describe 'Right header search field', feature: true do
+ it 'allows enter key to search', js: true do
+ visit namespace_project_path(project.namespace, project)
+ fill_in 'search', with: 'gitlab'
+ find('#search').native.send_keys(:enter)
+
+ page.within '.title' do
+ expect(page).to have_content 'Search'
+ end
+ end
+
describe 'Search in project page' do
before do
visit namespace_project_path(project.namespace, project)
end
- it 'top right search form is present' do
+ it 'shows top right search form' do
expect(page).to have_selector('#search')
end
- it 'top right search form contains location badge' do
+ it 'contains location badge in top right search form' do
expect(page).to have_selector('.has-location-badge')
end
context 'clicking the search field', js: true do
- it 'should show category search dropdown' do
+ it 'shows category search dropdown' do
page.find('#search').click
expect(page).to have_selector('.dropdown-header', text: /#{project.name}/i)
@@ -77,7 +107,7 @@ describe "Search", feature: true do
page.find('#search').click
end
- it 'should take user to her issues page when issues assigned is clicked' do
+ it 'takes user to her issues page when issues assigned is clicked' do
find('.dropdown-menu').click_link 'Issues assigned to me'
sleep 2
@@ -85,7 +115,7 @@ describe "Search", feature: true do
expect(find('.js-assignee-search .dropdown-toggle-text')).to have_content(user.name)
end
- it 'should take user to her issues page when issues authored is clicked' do
+ it 'takes user to her issues page when issues authored is clicked' do
find('.dropdown-menu').click_link "Issues I've created"
sleep 2
@@ -93,7 +123,7 @@ describe "Search", feature: true do
expect(find('.js-author-search .dropdown-toggle-text')).to have_content(user.name)
end
- it 'should take user to her MR page when MR assigned is clicked' do
+ it 'takes user to her MR page when MR assigned is clicked' do
find('.dropdown-menu').click_link 'Merge requests assigned to me'
sleep 2
@@ -101,7 +131,7 @@ describe "Search", feature: true do
expect(find('.js-assignee-search .dropdown-toggle-text')).to have_content(user.name)
end
- it 'should take user to her MR page when MR authored is clicked' do
+ it 'takes user to her MR page when MR authored is clicked' do
find('.dropdown-menu').click_link "Merge requests I've created"
sleep 2
@@ -117,7 +147,7 @@ describe "Search", feature: true do
end
end
- it 'should not display the category search dropdown' do
+ it 'does not display the category search dropdown' do
expect(page).not_to have_selector('.dropdown-header', text: /#{project.name}/i)
end
end
diff --git a/spec/features/security/dashboard_access_spec.rb b/spec/features/security/dashboard_access_spec.rb
index 788581a26cb..40f773956d1 100644
--- a/spec/features/security/dashboard_access_spec.rb
+++ b/spec/features/security/dashboard_access_spec.rb
@@ -43,6 +43,20 @@ describe "Dashboard access", feature: true do
it { is_expected.to be_allowed_for :visitor }
end
+ describe "GET /koding" do
+ subject { koding_path }
+
+ context 'with Koding enabled' do
+ before do
+ stub_application_setting(koding_enabled?: true)
+ end
+
+ it { is_expected.to be_allowed_for :admin }
+ it { is_expected.to be_allowed_for :user }
+ it { is_expected.to be_denied_for :visitor }
+ end
+ end
+
describe "GET /projects/new" do
it { expect(new_project_path).to be_allowed_for :admin }
it { expect(new_project_path).to be_allowed_for :user }
diff --git a/spec/features/security/project/internal_access_spec.rb b/spec/features/security/project/internal_access_spec.rb
index 13d980a326f..b6acc509342 100644
--- a/spec/features/security/project/internal_access_spec.rb
+++ b/spec/features/security/project/internal_access_spec.rb
@@ -426,4 +426,23 @@ describe "Internal Project Access", feature: true do
it { is_expected.to be_denied_for :external }
it { is_expected.to be_denied_for :visitor }
end
+
+ describe "GET /:project_path/container_registry" do
+ before do
+ stub_container_registry_tags('latest')
+ stub_container_registry_config(enabled: true)
+ end
+
+ subject { namespace_project_container_registry_index_path(project.namespace, project) }
+
+ it { is_expected.to be_allowed_for :admin }
+ it { is_expected.to be_allowed_for owner }
+ it { is_expected.to be_allowed_for master }
+ it { is_expected.to be_allowed_for developer }
+ it { is_expected.to be_allowed_for reporter }
+ it { is_expected.to be_allowed_for guest }
+ it { is_expected.to be_allowed_for :user }
+ it { is_expected.to be_denied_for :external }
+ it { is_expected.to be_denied_for :visitor }
+ end
end
diff --git a/spec/features/security/project/private_access_spec.rb b/spec/features/security/project/private_access_spec.rb
index ac9690cc127..ccb5c06dab0 100644
--- a/spec/features/security/project/private_access_spec.rb
+++ b/spec/features/security/project/private_access_spec.rb
@@ -362,4 +362,23 @@ describe "Private Project Access", feature: true do
it { is_expected.to be_denied_for :external }
it { is_expected.to be_denied_for :visitor }
end
+
+ describe "GET /:project_path/container_registry" do
+ before do
+ stub_container_registry_tags('latest')
+ stub_container_registry_config(enabled: true)
+ end
+
+ subject { namespace_project_container_registry_index_path(project.namespace, project) }
+
+ it { is_expected.to be_allowed_for :admin }
+ it { is_expected.to be_allowed_for owner }
+ it { is_expected.to be_allowed_for master }
+ it { is_expected.to be_allowed_for developer }
+ it { is_expected.to be_allowed_for reporter }
+ it { is_expected.to be_denied_for guest }
+ it { is_expected.to be_denied_for :user }
+ it { is_expected.to be_denied_for :external }
+ it { is_expected.to be_denied_for :visitor }
+ end
end
diff --git a/spec/features/security/project/public_access_spec.rb b/spec/features/security/project/public_access_spec.rb
index 737897de52b..985663e7c98 100644
--- a/spec/features/security/project/public_access_spec.rb
+++ b/spec/features/security/project/public_access_spec.rb
@@ -426,4 +426,23 @@ describe "Public Project Access", feature: true do
it { is_expected.to be_denied_for :external }
it { is_expected.to be_denied_for :visitor }
end
+
+ describe "GET /:project_path/container_registry" do
+ before do
+ stub_container_registry_tags('latest')
+ stub_container_registry_config(enabled: true)
+ end
+
+ subject { namespace_project_container_registry_index_path(project.namespace, project) }
+
+ it { is_expected.to be_allowed_for :admin }
+ it { is_expected.to be_allowed_for owner }
+ it { is_expected.to be_allowed_for master }
+ it { is_expected.to be_allowed_for developer }
+ it { is_expected.to be_allowed_for reporter }
+ it { is_expected.to be_allowed_for guest }
+ it { is_expected.to be_allowed_for :user }
+ it { is_expected.to be_allowed_for :external }
+ it { is_expected.to be_allowed_for :visitor }
+ end
end
diff --git a/spec/features/todos/todos_sorting_spec.rb b/spec/features/todos/todos_sorting_spec.rb
new file mode 100644
index 00000000000..e74a51acede
--- /dev/null
+++ b/spec/features/todos/todos_sorting_spec.rb
@@ -0,0 +1,67 @@
+require 'spec_helper'
+
+describe "Dashboard > User sorts todos", feature: true do
+ let(:user) { create(:user) }
+ let(:project) { create(:empty_project) }
+
+ 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(:issue_1) { create(:issue, title: 'issue_1', project: project) }
+ let(:issue_2) { create(:issue, title: 'issue_2', project: project) }
+ let(:issue_3) { create(:issue, title: 'issue_3', project: project) }
+ let(:issue_4) { create(:issue, title: 'issue_4', project: project) }
+
+ let!(:merge_request_1) { create(:merge_request, source_project: project, title: "merge_request_1") }
+
+ before do
+ create(:todo, user: user, project: project, target: issue_4, created_at: 5.hours.ago)
+ create(:todo, user: user, project: project, target: issue_2, created_at: 4.hours.ago)
+ create(:todo, user: user, project: project, target: issue_3, created_at: 3.hours.ago)
+ create(:todo, user: user, project: project, target: issue_1, created_at: 2.hours.ago)
+ create(:todo, user: user, project: project, target: merge_request_1, created_at: 1.hour.ago)
+
+ merge_request_1.labels << label_1
+ issue_3.labels << label_1
+ issue_2.labels << label_3
+ issue_1.labels << label_2
+
+ project.team << [user, :developer]
+ login_as(user)
+ visit dashboard_todos_path
+ end
+
+ it "sorts with oldest created todos first" do
+ click_link "Last created"
+
+ results_list = page.find('.todos-list')
+ expect(results_list.all('p')[0]).to have_content("merge_request_1")
+ expect(results_list.all('p')[1]).to have_content("issue_1")
+ expect(results_list.all('p')[2]).to have_content("issue_3")
+ expect(results_list.all('p')[3]).to have_content("issue_2")
+ expect(results_list.all('p')[4]).to have_content("issue_4")
+ end
+
+ it "sorts with newest created todos first" do
+ click_link "Oldest created"
+
+ results_list = page.find('.todos-list')
+ expect(results_list.all('p')[0]).to have_content("issue_4")
+ expect(results_list.all('p')[1]).to have_content("issue_2")
+ expect(results_list.all('p')[2]).to have_content("issue_3")
+ expect(results_list.all('p')[3]).to have_content("issue_1")
+ expect(results_list.all('p')[4]).to have_content("merge_request_1")
+ end
+
+ it "sorts by priority" do
+ click_link "Priority"
+
+ results_list = page.find('.todos-list')
+ expect(results_list.all('p')[0]).to have_content("issue_3")
+ expect(results_list.all('p')[1]).to have_content("merge_request_1")
+ expect(results_list.all('p')[2]).to have_content("issue_1")
+ expect(results_list.all('p')[3]).to have_content("issue_2")
+ expect(results_list.all('p')[4]).to have_content("issue_4")
+ end
+end
diff --git a/spec/features/todos/todos_spec.rb b/spec/features/todos/todos_spec.rb
index 0bdb1628c74..32544f3f538 100644
--- a/spec/features/todos/todos_spec.rb
+++ b/spec/features/todos/todos_spec.rb
@@ -24,7 +24,7 @@ describe 'Dashboard Todos', feature: true do
visit dashboard_todos_path
end
- it 'todo is present' do
+ it 'has todo present' do
expect(page).to have_selector('.todos-list .todo', count: 1)
end
@@ -41,6 +41,27 @@ describe 'Dashboard Todos', feature: true do
expect(page).to have_content("You're all done!")
end
end
+
+ context 'todo is stale on the page' do
+ before do
+ todos = TodosFinder.new(user, state: :pending).execute
+ TodoService.new.mark_todos_as_done(todos, user)
+ end
+
+ describe 'deleting the todo' do
+ before do
+ first('.done-todo').click
+ end
+
+ it 'is removed from the list' do
+ expect(page).not_to have_selector('.todos-list .todo')
+ end
+
+ it 'shows "All done" message' do
+ expect(page).to have_content("You're all done!")
+ end
+ end
+ end
end
context 'User has Todos with labels spanning multiple projects' do
diff --git a/spec/features/u2f_spec.rb b/spec/features/u2f_spec.rb
index 14613754f74..a46e48c76ed 100644
--- a/spec/features/u2f_spec.rb
+++ b/spec/features/u2f_spec.rb
@@ -1,11 +1,23 @@
require 'spec_helper'
feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature: true, js: true do
+ include WaitForAjax
+
+ before { allow_any_instance_of(U2fHelper).to receive(:inject_u2f_api?).and_return(true) }
+
+ def manage_two_factor_authentication
+ click_on 'Manage Two-Factor Authentication'
+ expect(page).to have_content("Setup New U2F Device")
+ wait_for_ajax
+ end
+
def register_u2f_device(u2f_device = nil)
- u2f_device ||= FakeU2fDevice.new(page)
+ name = FFaker::Name.first_name
+ u2f_device ||= FakeU2fDevice.new(page, name)
u2f_device.respond_to_u2f_registration
click_on 'Setup New U2F Device'
expect(page).to have_content('Your device was successfully set up')
+ fill_in "Pick a name", with: name
click_on 'Register U2F Device'
u2f_device
end
@@ -30,13 +42,14 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature:
end
describe 'when 2FA via OTP is enabled' do
- it 'allows registering a new device' do
+ it 'allows registering a new device with a name' do
visit profile_account_path
- click_on 'Manage Two-Factor Authentication'
+ manage_two_factor_authentication
expect(page.body).to match("You've already enabled two-factor authentication using mobile")
- register_u2f_device
+ u2f_device = register_u2f_device
+ expect(page.body).to match(u2f_device.name)
expect(page.body).to match('Your U2F device was registered')
end
@@ -44,23 +57,39 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature:
visit profile_account_path
# First device
- click_on 'Manage Two-Factor Authentication'
- register_u2f_device
+ manage_two_factor_authentication
+ first_device = register_u2f_device
expect(page.body).to match('Your U2F device was registered')
# Second device
- click_on 'Manage Two-Factor Authentication'
- register_u2f_device
+ second_device = 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')
+
+ expect(page.body).to match(first_device.name)
+ expect(page.body).to match(second_device.name)
+ expect(U2fRegistration.count).to eq(2)
+ end
+
+ it 'allows deleting a device' do
+ visit profile_account_path
+ manage_two_factor_authentication
+ expect(page.body).to match("You've already enabled two-factor authentication using mobile")
+
+ first_u2f_device = register_u2f_device
+ second_u2f_device = register_u2f_device
+
+ click_on "Delete", match: :first
+
+ expect(page.body).to match('Successfully deleted')
+ expect(page.body).not_to match(first_u2f_device.name)
+ expect(page.body).to match(second_u2f_device.name)
end
end
it 'allows the same device to be registered for multiple users' do
# First user
visit profile_account_path
- click_on 'Manage Two-Factor Authentication'
+ manage_two_factor_authentication
u2f_device = register_u2f_device
expect(page.body).to match('Your U2F device was registered')
logout
@@ -69,7 +98,7 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature:
user = login_as(:user)
user.update_attribute(:otp_required_for_login, true)
visit profile_account_path
- click_on 'Manage Two-Factor Authentication'
+ manage_two_factor_authentication
register_u2f_device(u2f_device)
expect(page.body).to match('Your U2F device was registered')
@@ -79,7 +108,7 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature:
context "when there are form errors" do
it "doesn't register the device if there are errors" do
visit profile_account_path
- click_on 'Manage Two-Factor Authentication'
+ manage_two_factor_authentication
# Have the "u2f device" respond with bad data
page.execute_script("u2f.register = function(_,_,_,callback) { callback('bad response'); };")
@@ -94,7 +123,7 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature:
it "allows retrying registration" do
visit profile_account_path
- click_on 'Manage Two-Factor Authentication'
+ manage_two_factor_authentication
# Failed registration
page.execute_script("u2f.register = function(_,_,_,callback) { callback('bad response'); };")
@@ -120,7 +149,7 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature:
login_as(user)
user.update_attribute(:otp_required_for_login, true)
visit profile_account_path
- click_on 'Manage Two-Factor Authentication'
+ manage_two_factor_authentication
@u2f_device = register_u2f_device
logout
end
@@ -159,7 +188,7 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature:
current_user = login_as(:user)
current_user.update_attribute(:otp_required_for_login, true)
visit profile_account_path
- click_on 'Manage Two-Factor Authentication'
+ manage_two_factor_authentication
register_u2f_device
logout
@@ -180,7 +209,7 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature:
current_user = login_as(:user)
current_user.update_attribute(:otp_required_for_login, true)
visit profile_account_path
- click_on 'Manage Two-Factor Authentication'
+ manage_two_factor_authentication
register_u2f_device(@u2f_device)
logout
@@ -198,7 +227,7 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature:
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)
+ unregistered_device = FakeU2fDevice.new(page, FFaker::Name.first_name)
login_as(user)
unregistered_device.respond_to_u2f_authentication
click_on "Login Via U2F Device"
@@ -208,21 +237,53 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature:
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) }
+ describe "when more than one device has been registered by the same user" do
+ it "allows logging in with either device" do
+ # Register first device
+ user = login_as(:user)
+ user.update_attribute(:otp_required_for_login, true)
+ visit profile_two_factor_auth_path
+ expect(page).to have_content("Your U2F device needs to be set up.")
+ first_device = register_u2f_device
+
+ # Register second device
+ visit profile_two_factor_auth_path
+ expect(page).to have_content("Your U2F device needs to be set up.")
+ second_device = register_u2f_device
+ logout
+
+ # Authenticate as both devices
+ [first_device, second_device].each do |device|
+ login_as(user)
+ 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"
- before do
- login_as(user)
- user.update_attribute(:otp_required_for_login, true)
- visit profile_account_path
- click_on 'Manage Two-Factor Authentication'
- register_u2f_device
+ expect(page.body).to match('Signed in successfully')
+
+ logout
+ end
+ end
end
- it "deletes u2f registrations" do
- expect { click_on "Disable" }.to change { U2fRegistration.count }.from(1).to(0)
+ describe "when two-factor authentication is disabled" do
+ let(:user) { create(:user) }
+
+ before do
+ user = login_as(:user)
+ user.update_attribute(:otp_required_for_login, true)
+ visit profile_account_path
+ manage_two_factor_authentication
+ expect(page).to have_content("Your U2F device needs to be set up.")
+ register_u2f_device
+ end
+
+ it "deletes u2f registrations" do
+ visit profile_account_path
+ expect { click_on "Disable" }.to change { U2fRegistration.count }.by(-1)
+ end
end
end
end
diff --git a/spec/features/variables_spec.rb b/spec/features/variables_spec.rb
index a2b8f7b6931..d7880d5778f 100644
--- a/spec/features/variables_spec.rb
+++ b/spec/features/variables_spec.rb
@@ -13,13 +13,13 @@ describe 'Project variables', js: true do
visit namespace_project_variables_path(project.namespace, project)
end
- it 'should show list of variables' do
+ it 'shows list of variables' do
page.within('.variables-table') do
expect(page).to have_content(variable.key)
end
end
- it 'should add new variable' do
+ it 'adds new variable' do
fill_in('variable_key', with: 'key')
fill_in('variable_value', with: 'key value')
click_button('Add new variable')
@@ -29,7 +29,7 @@ describe 'Project variables', js: true do
end
end
- it 'should delete variable' do
+ it 'deletes variable' do
page.within('.variables-table') do
find('.btn-variable-delete').click
end
@@ -37,11 +37,12 @@ describe 'Project variables', js: true do
expect(page).not_to have_selector('variables-table')
end
- it 'should edit variable' do
+ it 'edits variable' do
page.within('.variables-table') do
find('.btn-variable-edit').click
end
+ expect(page).to have_content('Update variable')
fill_in('variable_key', with: 'key')
fill_in('variable_value', with: 'key value')
click_button('Save variable')
diff --git a/spec/finders/branches_finder_spec.rb b/spec/finders/branches_finder_spec.rb
new file mode 100644
index 00000000000..6fce11de30f
--- /dev/null
+++ b/spec/finders/branches_finder_spec.rb
@@ -0,0 +1,80 @@
+require 'spec_helper'
+
+describe BranchesFinder do
+ let(:user) { create(:user) }
+ let(:project) { create(:project) }
+ let(:repository) { project.repository }
+
+ describe '#execute' do
+ context 'sort only' do
+ it 'sorts by name' do
+ branches_finder = described_class.new(repository, {})
+
+ result = branches_finder.execute
+
+ expect(result.first.name).to eq("'test'")
+ end
+
+ it 'sorts by recently_updated' do
+ branches_finder = described_class.new(repository, { sort: 'updated_desc' })
+
+ result = branches_finder.execute
+
+ recently_updated_branch = repository.branches.max do |a, b|
+ repository.commit(a.target).committed_date <=> repository.commit(b.target).committed_date
+ end
+
+ expect(result.first.name).to eq(recently_updated_branch.name)
+ end
+
+ it 'sorts by last_updated' do
+ branches_finder = described_class.new(repository, { sort: 'updated_asc' })
+
+ result = branches_finder.execute
+
+ expect(result.first.name).to eq('feature')
+ end
+ end
+
+ context 'filter only' do
+ it 'filters branches by name' do
+ branches_finder = described_class.new(repository, { search: 'fix' })
+
+ result = branches_finder.execute
+
+ expect(result.first.name).to eq('fix')
+ expect(result.count).to eq(1)
+ end
+
+ it 'does not find any branch with that name' do
+ branches_finder = described_class.new(repository, { search: 'random' })
+
+ result = branches_finder.execute
+
+ expect(result.count).to eq(0)
+ end
+ end
+
+ context 'filter and sort' do
+ it 'filters branches by name and sorts by recently_updated' do
+ params = { sort: 'updated_desc', search: 'feature' }
+ branches_finder = described_class.new(repository, params)
+
+ result = branches_finder.execute
+
+ expect(result.first.name).to eq('feature_conflict')
+ expect(result.count).to eq(2)
+ end
+
+ it 'filters branches by name and sorts by last_updated' do
+ params = { sort: 'updated_asc', search: 'feature' }
+ branches_finder = described_class.new(repository, params)
+
+ result = branches_finder.execute
+
+ expect(result.first.name).to eq('feature')
+ expect(result.count).to eq(2)
+ end
+ end
+ end
+end
diff --git a/spec/finders/merge_requests_finder_spec.rb b/spec/finders/merge_requests_finder_spec.rb
index bc385fd0d69..535aabfc18d 100644
--- a/spec/finders/merge_requests_finder_spec.rb
+++ b/spec/finders/merge_requests_finder_spec.rb
@@ -18,13 +18,13 @@ describe MergeRequestsFinder do
end
describe "#execute" do
- it 'should filter by scope' do
+ it 'filters by scope' do
params = { scope: 'authored', state: 'opened' }
merge_requests = MergeRequestsFinder.new(user, params).execute
expect(merge_requests.size).to eq(2)
end
- it 'should filter by project' do
+ it 'filters by project' do
params = { project_id: project1.id, scope: 'authored', state: 'opened' }
merge_requests = MergeRequestsFinder.new(user, params).execute
expect(merge_requests.size).to eq(1)
diff --git a/spec/finders/move_to_project_finder_spec.rb b/spec/finders/move_to_project_finder_spec.rb
new file mode 100644
index 00000000000..fdce4e714ff
--- /dev/null
+++ b/spec/finders/move_to_project_finder_spec.rb
@@ -0,0 +1,97 @@
+require 'spec_helper'
+
+describe MoveToProjectFinder do
+ let(:user) { create(:user) }
+ let(:project) { create(:project) }
+
+ let(:no_access_project) { create(:project) }
+ let(:guest_project) { create(:project) }
+ let(:reporter_project) { create(:project) }
+ let(:developer_project) { create(:project) }
+ let(:master_project) { create(:project) }
+
+ subject { described_class.new(user) }
+
+ describe '#execute' do
+ context 'filter' do
+ it 'does not return projects under Gitlab::Access::REPORTER' do
+ guest_project.team << [user, :guest]
+
+ expect(subject.execute(project)).to be_empty
+ end
+
+ it 'returns projects equal or above Gitlab::Access::REPORTER ordered by id in descending order' do
+ reporter_project.team << [user, :reporter]
+ developer_project.team << [user, :developer]
+ master_project.team << [user, :master]
+
+ expect(subject.execute(project).to_a).to eq([master_project, developer_project, reporter_project])
+ end
+
+ it 'does not include the source project' do
+ project.team << [user, :reporter]
+
+ expect(subject.execute(project).to_a).to be_empty
+ end
+
+ it 'does not return archived projects' do
+ reporter_project.team << [user, :reporter]
+ reporter_project.update_attributes(archived: true)
+ other_reporter_project = create(:project)
+ other_reporter_project.team << [user, :reporter]
+
+ expect(subject.execute(project).to_a).to eq([other_reporter_project])
+ end
+
+ it 'does not return projects for which issues are disabled' do
+ reporter_project.team << [user, :reporter]
+ reporter_project.update_attributes(issues_enabled: false)
+ other_reporter_project = create(:project)
+ other_reporter_project.team << [user, :reporter]
+
+ expect(subject.execute(project).to_a).to eq([other_reporter_project])
+ end
+
+ it 'returns a page of projects ordered by id in descending order' do
+ stub_const 'MoveToProjectFinder::PAGE_SIZE', 2
+
+ reporter_project.team << [user, :reporter]
+ developer_project.team << [user, :developer]
+ master_project.team << [user, :master]
+
+ expect(subject.execute(project).to_a).to eq([master_project, developer_project])
+ end
+
+ it 'returns projects after the given offset id' do
+ stub_const 'MoveToProjectFinder::PAGE_SIZE', 2
+
+ reporter_project.team << [user, :reporter]
+ developer_project.team << [user, :developer]
+ master_project.team << [user, :master]
+
+ expect(subject.execute(project, search: nil, offset_id: master_project.id).to_a).to eq([developer_project, reporter_project])
+ expect(subject.execute(project, search: nil, offset_id: developer_project.id).to_a).to eq([reporter_project])
+ expect(subject.execute(project, search: nil, offset_id: reporter_project.id).to_a).to be_empty
+ end
+ end
+
+ context 'search' do
+ it 'uses Project#search' do
+ expect(user).to receive_message_chain(:projects_where_can_admin_issues, :search) { Project.all }
+
+ subject.execute(project, search: 'wadus')
+ end
+
+ it 'returns projects matching a search query' do
+ foo_project = create(:project)
+ foo_project.team << [user, :master]
+
+ wadus_project = create(:project, name: 'wadus')
+ wadus_project.team << [user, :master]
+
+ expect(subject.execute(project).to_a).to eq([wadus_project, foo_project])
+ expect(subject.execute(project, search: 'wadus').to_a).to eq([wadus_project])
+ end
+ end
+ end
+end
diff --git a/spec/finders/notes_finder_spec.rb b/spec/finders/notes_finder_spec.rb
index 1bd354815e4..7c6860372cc 100644
--- a/spec/finders/notes_finder_spec.rb
+++ b/spec/finders/notes_finder_spec.rb
@@ -11,7 +11,7 @@ describe NotesFinder do
project.team << [user, :master]
end
- describe :execute do
+ describe '#execute' do
let(:params) { { target_id: commit.id, target_type: 'commit', last_fetched_at: 1.hour.ago.to_i } }
before do
@@ -19,12 +19,12 @@ describe NotesFinder do
note2
end
- it 'should find all notes' do
+ it 'finds all notes' do
notes = NotesFinder.new.execute(project, user, params)
expect(notes.size).to eq(2)
end
- it 'should raise an exception for an invalid target_type' do
+ it 'raises an exception for an invalid target_type' do
params.merge!(target_type: 'invalid')
expect { NotesFinder.new.execute(project, user, params) }.to raise_error('invalid target_type')
end
diff --git a/spec/finders/projects_finder_spec.rb b/spec/finders/projects_finder_spec.rb
index 0a1cc3b3df7..7a3a74335e8 100644
--- a/spec/finders/projects_finder_spec.rb
+++ b/spec/finders/projects_finder_spec.rb
@@ -23,73 +23,36 @@ describe ProjectsFinder do
let(:finder) { described_class.new }
- describe 'without a group' do
- describe 'without a user' do
- subject { finder.execute }
+ describe 'without a user' do
+ subject { finder.execute }
- it { is_expected.to eq([public_project]) }
- end
-
- describe 'with a user' do
- subject { finder.execute(user) }
-
- describe 'without private projects' do
- it { is_expected.to eq([public_project, internal_project]) }
- end
-
- describe 'with private projects' do
- before do
- private_project.team.add_user(user, Gitlab::Access::MASTER)
- end
-
- it do
- is_expected.to eq([public_project, internal_project,
- private_project])
- end
- end
- end
+ it { is_expected.to eq([public_project]) }
end
- describe 'with a group' do
- describe 'without a user' do
- subject { finder.execute(nil, group: group) }
+ describe 'with a user' do
+ subject { finder.execute(user) }
- it { is_expected.to eq([public_project]) }
+ describe 'without private projects' do
+ it { is_expected.to eq([public_project, internal_project]) }
end
- describe 'with a user' do
- subject { finder.execute(user, group: group) }
-
- describe 'without shared projects' do
- it { is_expected.to eq([public_project, internal_project]) }
+ describe 'with private projects' do
+ before do
+ private_project.team.add_user(user, Gitlab::Access::MASTER)
end
- describe 'with shared projects and group membership' do
- before do
- group.add_user(user, Gitlab::Access::DEVELOPER)
-
- shared_project.project_group_links.
- create(group_access: Gitlab::Access::MASTER, group: group)
- end
-
- it do
- is_expected.to eq([shared_project, public_project, internal_project])
- end
+ it do
+ is_expected.to eq([public_project, internal_project, private_project])
end
+ end
+ end
- describe 'with shared projects and project membership' do
- before do
- shared_project.team.add_user(user, Gitlab::Access::DEVELOPER)
+ describe 'with project_ids_relation' do
+ let(:project_ids_relation) { Project.where(id: internal_project.id) }
- shared_project.project_group_links.
- create(group_access: Gitlab::Access::MASTER, group: group)
- end
+ subject { finder.execute(user, project_ids_relation) }
- it do
- is_expected.to eq([shared_project, public_project, internal_project])
- end
- end
- end
+ it { is_expected.to eq([internal_project]) }
end
end
end
diff --git a/spec/finders/todos_finder_spec.rb b/spec/finders/todos_finder_spec.rb
new file mode 100644
index 00000000000..f7e7e733cf7
--- /dev/null
+++ b/spec/finders/todos_finder_spec.rb
@@ -0,0 +1,70 @@
+require 'spec_helper'
+
+describe TodosFinder do
+ describe '#execute' do
+ let(:user) { create(:user) }
+ let(:project) { create(:empty_project) }
+ let(:finder) { described_class }
+
+ before { project.team << [user, :developer] }
+
+ describe '#sort' do
+ context 'by date' do
+ let!(:todo1) { create(:todo, user: user, project: project) }
+ let!(:todo2) { create(:todo, user: user, project: project) }
+ let!(:todo3) { create(:todo, user: user, project: project) }
+
+ it 'sorts with oldest created first' do
+ todos = finder.new(user, { sort: 'id_asc' }).execute
+
+ expect(todos.first).to eq(todo1)
+ expect(todos.second).to eq(todo2)
+ expect(todos.third).to eq(todo3)
+ end
+
+ it 'sorts with newest created first' do
+ todos = finder.new(user, { sort: 'id_desc' }).execute
+
+ expect(todos.first).to eq(todo3)
+ expect(todos.second).to eq(todo2)
+ expect(todos.third).to eq(todo1)
+ end
+ end
+
+ it "sorts by priority" do
+ label_1 = create(:label, title: 'label_1', project: project, priority: 1)
+ label_2 = create(:label, title: 'label_2', project: project, priority: 2)
+ label_3 = create(:label, title: 'label_3', project: project, priority: 3)
+
+ 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)
+ merge_request_1 = create(:merge_request, source_project: project)
+
+ merge_request_1.labels << label_1
+
+ # Covers the case where Todo has more than one label
+ issue_3.labels << label_1
+ issue_3.labels << label_3
+
+ issue_2.labels << label_3
+ issue_1.labels << label_2
+
+ todo_1 = create(:todo, user: user, project: project, target: issue_4)
+ todo_2 = create(:todo, user: user, project: project, target: issue_2)
+ todo_3 = create(:todo, user: user, project: project, target: issue_3, created_at: 2.hours.ago)
+ todo_4 = create(:todo, user: user, project: project, target: issue_1)
+ todo_5 = create(:todo, user: user, project: project, target: merge_request_1, created_at: 1.hour.ago)
+
+ todos = finder.new(user, { sort: 'priority' }).execute
+
+ expect(todos.first).to eq(todo_3)
+ expect(todos.second).to eq(todo_5)
+ expect(todos.third).to eq(todo_4)
+ expect(todos.fourth).to eq(todo_2)
+ expect(todos.fifth).to eq(todo_1)
+ end
+ end
+ end
+end
diff --git a/spec/fixtures/api/schemas/issue.json b/spec/fixtures/api/schemas/issue.json
new file mode 100644
index 00000000000..532ebb9640e
--- /dev/null
+++ b/spec/fixtures/api/schemas/issue.json
@@ -0,0 +1,48 @@
+{
+ "type": "object",
+ "required" : [
+ "iid",
+ "title",
+ "confidential"
+ ],
+ "properties" : {
+ "iid": { "type": "integer" },
+ "title": { "type": "string" },
+ "confidential": { "type": "boolean" },
+ "labels": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "required": [
+ "id",
+ "color",
+ "description",
+ "title",
+ "priority"
+ ],
+ "properties": {
+ "id": { "type": "integer" },
+ "color": {
+ "type": "string",
+ "pattern": "^#[0-9A-Fa-f]{3}{1,2}+$"
+ },
+ "description": { "type": ["string", "null"] },
+ "text_color": {
+ "type": "string",
+ "pattern": "^#[0-9A-Fa-f]{3}{1,2}+$"
+ },
+ "title": { "type": "string" },
+ "priority": { "type": ["integer", "null"] }
+ },
+ "additionalProperties": false
+ }
+ },
+ "assignee": {
+ "id": { "type": "integet" },
+ "name": { "type": "string" },
+ "username": { "type": "string" },
+ "avatar_url": { "type": "uri" }
+ }
+ },
+ "additionalProperties": false
+}
diff --git a/spec/fixtures/api/schemas/issues.json b/spec/fixtures/api/schemas/issues.json
new file mode 100644
index 00000000000..0d2067f704a
--- /dev/null
+++ b/spec/fixtures/api/schemas/issues.json
@@ -0,0 +1,4 @@
+{
+ "type": "array",
+ "items": { "$ref": "issue.json" }
+}
diff --git a/spec/fixtures/api/schemas/list.json b/spec/fixtures/api/schemas/list.json
new file mode 100644
index 00000000000..f070fa3b254
--- /dev/null
+++ b/spec/fixtures/api/schemas/list.json
@@ -0,0 +1,39 @@
+{
+ "type": "object",
+ "required" : [
+ "id",
+ "list_type",
+ "title",
+ "position"
+ ],
+ "properties" : {
+ "id": { "type": "integer" },
+ "list_type": {
+ "type": "string",
+ "enum": ["backlog", "label", "done"]
+ },
+ "label": {
+ "type": ["object"],
+ "required": [
+ "id",
+ "color",
+ "description",
+ "title",
+ "priority"
+ ],
+ "properties": {
+ "id": { "type": "integer" },
+ "color": {
+ "type": "string",
+ "pattern": "^#[0-9A-Fa-f]{3}{1,2}+$"
+ },
+ "description": { "type": ["string", "null"] },
+ "title": { "type": "string" },
+ "priority": { "type": ["integer", "null"] }
+ }
+ },
+ "title": { "type": "string" },
+ "position": { "type": ["integer", "null"] }
+ },
+ "additionalProperties": false
+}
diff --git a/spec/fixtures/api/schemas/lists.json b/spec/fixtures/api/schemas/lists.json
new file mode 100644
index 00000000000..9f618aa9de5
--- /dev/null
+++ b/spec/fixtures/api/schemas/lists.json
@@ -0,0 +1,4 @@
+{
+ "type": "array",
+ "items": { "$ref": "list.json" }
+}
diff --git a/spec/fixtures/config/redis_new_format_host.yml b/spec/fixtures/config/redis_new_format_host.yml
new file mode 100644
index 00000000000..13772677a45
--- /dev/null
+++ b/spec/fixtures/config/redis_new_format_host.yml
@@ -0,0 +1,29 @@
+# redis://[:password@]host[:port][/db-number][?option=value]
+# more details: http://www.iana.org/assignments/uri-schemes/prov/redis
+development:
+ url: redis://:mynewpassword@localhost:6379/99
+ sentinels:
+ -
+ host: localhost
+ port: 26380 # point to sentinel, not to redis port
+ -
+ host: slave2
+ port: 26381 # point to sentinel, not to redis port
+test:
+ url: redis://:mynewpassword@localhost:6379/99
+ sentinels:
+ -
+ host: localhost
+ port: 26380 # point to sentinel, not to redis port
+ -
+ host: slave2
+ port: 26381 # point to sentinel, not to redis port
+production:
+ url: redis://:mynewpassword@localhost:6379/99
+ sentinels:
+ -
+ host: slave1
+ port: 26380 # point to sentinel, not to redis port
+ -
+ host: slave2
+ port: 26381 # point to sentinel, not to redis port
diff --git a/spec/fixtures/config/redis_new_format_socket.yml b/spec/fixtures/config/redis_new_format_socket.yml
new file mode 100644
index 00000000000..4e76830c281
--- /dev/null
+++ b/spec/fixtures/config/redis_new_format_socket.yml
@@ -0,0 +1,6 @@
+development:
+ url: unix:/path/to/redis.sock
+test:
+ url: unix:/path/to/redis.sock
+production:
+ url: unix:/path/to/redis.sock
diff --git a/spec/fixtures/config/redis_old_format_host.yml b/spec/fixtures/config/redis_old_format_host.yml
new file mode 100644
index 00000000000..253d0a994f5
--- /dev/null
+++ b/spec/fixtures/config/redis_old_format_host.yml
@@ -0,0 +1,5 @@
+# redis://[:password@]host[:port][/db-number][?option=value]
+# more details: http://www.iana.org/assignments/uri-schemes/prov/redis
+development: redis://:mypassword@localhost:6379/99
+test: redis://:mypassword@localhost:6379/99
+production: redis://:mypassword@localhost:6379/99
diff --git a/spec/fixtures/config/redis_old_format_socket.yml b/spec/fixtures/config/redis_old_format_socket.yml
new file mode 100644
index 00000000000..fd31ce8ea3d
--- /dev/null
+++ b/spec/fixtures/config/redis_old_format_socket.yml
@@ -0,0 +1,3 @@
+development: unix:/path/to/old/redis.sock
+test: unix:/path/to/old/redis.sock
+production: unix:/path/to/old/redis.sock
diff --git a/spec/fixtures/domain_blacklist.txt b/spec/fixtures/domain_blacklist.txt
new file mode 100644
index 00000000000..baeb11eda9a
--- /dev/null
+++ b/spec/fixtures/domain_blacklist.txt
@@ -0,0 +1,3 @@
+example.com
+test.com
+foo.bar \ No newline at end of file
diff --git a/spec/fixtures/emails/commands_in_reply.eml b/spec/fixtures/emails/commands_in_reply.eml
new file mode 100644
index 00000000000..06bf60ab734
--- /dev/null
+++ b/spec/fixtures/emails/commands_in_reply.eml
@@ -0,0 +1,43 @@
+Return-Path: <jake@adventuretime.ooo>
+Received: from iceking.adventuretime.ooo ([unix socket]) by iceking (Cyrus v2.2.13-Debian-2.2.13-19+squeeze3) with LMTPA; Thu, 13 Jun 2013 17:03:50 -0400
+Received: from mail-ie0-x234.google.com (mail-ie0-x234.google.com [IPv6:2607:f8b0:4001:c03::234]) by iceking.adventuretime.ooo (8.14.3/8.14.3/Debian-9.4) with ESMTP id r5DL3nFJ016967 (version=TLSv1/SSLv3 cipher=RC4-SHA bits=128 verify=NOT) for <reply+59d8df8370b7e95c5a49fbf86aeb2c93@appmail.adventuretime.ooo>; Thu, 13 Jun 2013 17:03:50 -0400
+Received: by mail-ie0-f180.google.com with SMTP id f4so21977375iea.25 for <reply+59d8df8370b7e95c5a49fbf86aeb2c93@appmail.adventuretime.ooo>; Thu, 13 Jun 2013 14:03:48 -0700
+Received: by 10.0.0.1 with HTTP; Thu, 13 Jun 2013 14:03:48 -0700
+Date: Thu, 13 Jun 2013 17:03:48 -0400
+From: Jake the Dog <jake@adventuretime.ooo>
+To: reply+59d8df8370b7e95c5a49fbf86aeb2c93@appmail.adventuretime.ooo
+Message-ID: <CADkmRc+rNGAGGbV2iE5p918UVy4UyJqVcXRO2=otppgzduJSg@mail.gmail.com>
+In-Reply-To: <issue_1@localhost>
+References: <issue_1@localhost> <reply-59d8df8370b7e95c5a49fbf86aeb2c93@localhost>
+Subject: re: [Discourse Meta] eviltrout posted in 'Adventure Time Sux'
+Mime-Version: 1.0
+Content-Type: text/plain;
+ charset=ISO-8859-1
+Content-Transfer-Encoding: 7bit
+X-Sieve: CMU Sieve 2.2
+X-Received: by 10.0.0.1 with SMTP id n7mr11234144ipb.85.1371157428600; Thu,
+ 13 Jun 2013 14:03:48 -0700 (PDT)
+X-Scanned-By: MIMEDefang 2.69 on IPv6:2001:470:1d:165::1
+
+Cool!
+
+/close
+/todo
+/due tomorrow
+
+
+On Sun, Jun 9, 2013 at 1:39 PM, eviltrout via Discourse Meta
+<reply+59d8df8370b7e95c5a49fbf86aeb2c93@appmail.adventuretime.ooo> wrote:
+>
+>
+>
+> eviltrout posted in 'Adventure Time Sux' on Discourse Meta:
+>
+> ---
+> hey guys everyone knows adventure time sucks!
+>
+> ---
+> Please visit this link to respond: http://localhost:3000/t/adventure-time-sux/1234/3
+>
+> To unsubscribe from these emails, visit your [user preferences](http://localhost:3000/user_preferences).
+>
diff --git a/spec/fixtures/emails/commands_only_reply.eml b/spec/fixtures/emails/commands_only_reply.eml
new file mode 100644
index 00000000000..aed64224b06
--- /dev/null
+++ b/spec/fixtures/emails/commands_only_reply.eml
@@ -0,0 +1,41 @@
+Return-Path: <jake@adventuretime.ooo>
+Received: from iceking.adventuretime.ooo ([unix socket]) by iceking (Cyrus v2.2.13-Debian-2.2.13-19+squeeze3) with LMTPA; Thu, 13 Jun 2013 17:03:50 -0400
+Received: from mail-ie0-x234.google.com (mail-ie0-x234.google.com [IPv6:2607:f8b0:4001:c03::234]) by iceking.adventuretime.ooo (8.14.3/8.14.3/Debian-9.4) with ESMTP id r5DL3nFJ016967 (version=TLSv1/SSLv3 cipher=RC4-SHA bits=128 verify=NOT) for <reply+59d8df8370b7e95c5a49fbf86aeb2c93@appmail.adventuretime.ooo>; Thu, 13 Jun 2013 17:03:50 -0400
+Received: by mail-ie0-f180.google.com with SMTP id f4so21977375iea.25 for <reply+59d8df8370b7e95c5a49fbf86aeb2c93@appmail.adventuretime.ooo>; Thu, 13 Jun 2013 14:03:48 -0700
+Received: by 10.0.0.1 with HTTP; Thu, 13 Jun 2013 14:03:48 -0700
+Date: Thu, 13 Jun 2013 17:03:48 -0400
+From: Jake the Dog <jake@adventuretime.ooo>
+To: reply+59d8df8370b7e95c5a49fbf86aeb2c93@appmail.adventuretime.ooo
+Message-ID: <CADkmRc+rNGAGGbV2iE5p918UVy4UyJqVcXRO2=otppgzduJSg@mail.gmail.com>
+In-Reply-To: <issue_1@localhost>
+References: <issue_1@localhost> <reply-59d8df8370b7e95c5a49fbf86aeb2c93@localhost>
+Subject: re: [Discourse Meta] eviltrout posted in 'Adventure Time Sux'
+Mime-Version: 1.0
+Content-Type: text/plain;
+ charset=ISO-8859-1
+Content-Transfer-Encoding: 7bit
+X-Sieve: CMU Sieve 2.2
+X-Received: by 10.0.0.1 with SMTP id n7mr11234144ipb.85.1371157428600; Thu,
+ 13 Jun 2013 14:03:48 -0700 (PDT)
+X-Scanned-By: MIMEDefang 2.69 on IPv6:2001:470:1d:165::1
+
+/close
+/todo
+/due tomorrow
+
+
+On Sun, Jun 9, 2013 at 1:39 PM, eviltrout via Discourse Meta
+<reply+59d8df8370b7e95c5a49fbf86aeb2c93@appmail.adventuretime.ooo> wrote:
+>
+>
+>
+> eviltrout posted in 'Adventure Time Sux' on Discourse Meta:
+>
+> ---
+> hey guys everyone knows adventure time sucks!
+>
+> ---
+> Please visit this link to respond: http://localhost:3000/t/adventure-time-sux/1234/3
+>
+> To unsubscribe from these emails, visit your [user preferences](http://localhost:3000/user_preferences).
+>
diff --git a/spec/fixtures/emails/valid_new_issue.eml b/spec/fixtures/emails/valid_new_issue.eml
new file mode 100644
index 00000000000..3cf53a656a5
--- /dev/null
+++ b/spec/fixtures/emails/valid_new_issue.eml
@@ -0,0 +1,23 @@
+Return-Path: <jake@adventuretime.ooo>
+Received: from iceking.adventuretime.ooo ([unix socket]) by iceking (Cyrus v2.2.13-Debian-2.2.13-19+squeeze3) with LMTPA; Thu, 13 Jun 2013 17:03:50 -0400
+Received: from mail-ie0-x234.google.com (mail-ie0-x234.google.com [IPv6:2607:f8b0:4001:c03::234]) by iceking.adventuretime.ooo (8.14.3/8.14.3/Debian-9.4) with ESMTP id r5DL3nFJ016967 (version=TLSv1/SSLv3 cipher=RC4-SHA bits=128 verify=NOT) for <incoming+gitlabhq/gitlabhq@appmail.adventuretime.ooo>; Thu, 13 Jun 2013 17:03:50 -0400
+Received: by mail-ie0-f180.google.com with SMTP id f4so21977375iea.25 for <incoming+gitlabhq/gitlabhq@appmail.adventuretime.ooo>; Thu, 13 Jun 2013 14:03:48 -0700
+Received: by 10.0.0.1 with HTTP; Thu, 13 Jun 2013 14:03:48 -0700
+Date: Thu, 13 Jun 2013 17:03:48 -0400
+From: Jake the Dog <jake@adventuretime.ooo>
+To: incoming+gitlabhq/gitlabhq+auth_token@appmail.adventuretime.ooo
+Message-ID: <CADkmRc+rNGAGGbV2iE5p918UVy4UyJqVcXRO2=otppgzduJSg@mail.gmail.com>
+Subject: New Issue by email
+Mime-Version: 1.0
+Content-Type: text/plain;
+ charset=ISO-8859-1
+Content-Transfer-Encoding: 7bit
+X-Sieve: CMU Sieve 2.2
+X-Received: by 10.0.0.1 with SMTP id n7mr11234144ipb.85.1371157428600; Thu,
+ 13 Jun 2013 14:03:48 -0700 (PDT)
+X-Scanned-By: MIMEDefang 2.69 on IPv6:2001:470:1d:165::1
+
+The reply by email functionality should be extended to allow creating a new issue by email.
+
+* Allow an admin to specify which project the issue should be created under by checking the sender domain.
+* Possibly allow the use of regular expression matches within the subject/body to specify which project the issue should be created under.
diff --git a/spec/fixtures/emails/valid_new_issue_empty.eml b/spec/fixtures/emails/valid_new_issue_empty.eml
new file mode 100644
index 00000000000..fc1d52a3f42
--- /dev/null
+++ b/spec/fixtures/emails/valid_new_issue_empty.eml
@@ -0,0 +1,18 @@
+Return-Path: <jake@adventuretime.ooo>
+Received: from iceking.adventuretime.ooo ([unix socket]) by iceking (Cyrus v2.2.13-Debian-2.2.13-19+squeeze3) with LMTPA; Thu, 13 Jun 2013 17:03:50 -0400
+Received: from mail-ie0-x234.google.com (mail-ie0-x234.google.com [IPv6:2607:f8b0:4001:c03::234]) by iceking.adventuretime.ooo (8.14.3/8.14.3/Debian-9.4) with ESMTP id r5DL3nFJ016967 (version=TLSv1/SSLv3 cipher=RC4-SHA bits=128 verify=NOT) for <incoming+gitlabhq/gitlabhq@appmail.adventuretime.ooo>; Thu, 13 Jun 2013 17:03:50 -0400
+Received: by mail-ie0-f180.google.com with SMTP id f4so21977375iea.25 for <incoming+gitlabhq/gitlabhq@appmail.adventuretime.ooo>; Thu, 13 Jun 2013 14:03:48 -0700
+Received: by 10.0.0.1 with HTTP; Thu, 13 Jun 2013 14:03:48 -0700
+Date: Thu, 13 Jun 2013 17:03:48 -0400
+From: Jake the Dog <jake@adventuretime.ooo>
+To: incoming+gitlabhq/gitlabhq+auth_token@appmail.adventuretime.ooo
+Message-ID: <CADkmRc+rNGAGGbV2iE5p918UVy4UyJqVcXRO2=otppgzduJSg@mail.gmail.com>
+Subject: New Issue by email
+Mime-Version: 1.0
+Content-Type: text/plain;
+ charset=ISO-8859-1
+Content-Transfer-Encoding: 7bit
+X-Sieve: CMU Sieve 2.2
+X-Received: by 10.0.0.1 with SMTP id n7mr11234144ipb.85.1371157428600; Thu,
+ 13 Jun 2013 14:03:48 -0700 (PDT)
+X-Scanned-By: MIMEDefang 2.69 on IPv6:2001:470:1d:165::1
diff --git a/spec/fixtures/emails/wrong_authentication_token.eml b/spec/fixtures/emails/wrong_authentication_token.eml
new file mode 100644
index 00000000000..0994c2f7775
--- /dev/null
+++ b/spec/fixtures/emails/wrong_authentication_token.eml
@@ -0,0 +1,18 @@
+Return-Path: <jake@adventuretime.ooo>
+Received: from iceking.adventuretime.ooo ([unix socket]) by iceking (Cyrus v2.2.13-Debian-2.2.13-19+squeeze3) with LMTPA; Thu, 13 Jun 2013 17:03:50 -0400
+Received: from mail-ie0-x234.google.com (mail-ie0-x234.google.com [IPv6:2607:f8b0:4001:c03::234]) by iceking.adventuretime.ooo (8.14.3/8.14.3/Debian-9.4) with ESMTP id r5DL3nFJ016967 (version=TLSv1/SSLv3 cipher=RC4-SHA bits=128 verify=NOT) for <incoming+gitlabhq/gitlabhq@appmail.adventuretime.ooo>; Thu, 13 Jun 2013 17:03:50 -0400
+Received: by mail-ie0-f180.google.com with SMTP id f4so21977375iea.25 for <incoming+gitlabhq/gitlabhq@appmail.adventuretime.ooo>; Thu, 13 Jun 2013 14:03:48 -0700
+Received: by 10.0.0.1 with HTTP; Thu, 13 Jun 2013 14:03:48 -0700
+Date: Thu, 13 Jun 2013 17:03:48 -0400
+From: Jake the Dog <jake@adventuretime.ooo>
+To: incoming+gitlabhq/gitlabhq+bad_token@appmail.adventuretime.ooo
+Message-ID: <CADkmRc+rNGAGGbV2iE5p918UVy4UyJqVcXRO2=otppgzduJSg@mail.gmail.com>
+Subject: New Issue by email
+Mime-Version: 1.0
+Content-Type: text/plain;
+ charset=ISO-8859-1
+Content-Transfer-Encoding: 7bit
+X-Sieve: CMU Sieve 2.2
+X-Received: by 10.0.0.1 with SMTP id n7mr11234144ipb.85.1371157428600; Thu,
+ 13 Jun 2013 14:03:48 -0700 (PDT)
+X-Scanned-By: MIMEDefang 2.69 on IPv6:2001:470:1d:165::1
diff --git a/spec/fixtures/emails/wrong_reply_key.eml b/spec/fixtures/emails/wrong_mail_key.eml
index 491e078fb5b..491e078fb5b 100644
--- a/spec/fixtures/emails/wrong_reply_key.eml
+++ b/spec/fixtures/emails/wrong_mail_key.eml
diff --git a/spec/fixtures/markdown.md.erb b/spec/fixtures/markdown.md.erb
index c75d28d9801..f3e7c2d1a9f 100644
--- a/spec/fixtures/markdown.md.erb
+++ b/spec/fixtures/markdown.md.erb
@@ -256,3 +256,7 @@ However the wrapping tags can not be mixed as such -
- [+ additions +}
- {- delletions -]
- [- delletions -}
+
+### Videos
+
+![My Video](/assets/videos/gitlab-demo.mp4)
diff --git a/spec/fixtures/parallel_diff_result.yml b/spec/fixtures/parallel_diff_result.yml
deleted file mode 100644
index 7d01183e3ef..00000000000
--- a/spec/fixtures/parallel_diff_result.yml
+++ /dev/null
@@ -1,820 +0,0 @@
----
-- :left:
- :type: match
- :number: 6
- :text: "@@ -6,12 +6,18 @@ module Popen"
- :line_code:
- :position: !ruby/object:Gitlab::Diff::Position
- attributes:
- :old_path: files/ruby/popen.rb
- :new_path: files/ruby/popen.rb
- :old_line:
- :new_line:
- :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
- :right:
- :type: match
- :number: 6
- :text: "@@ -6,12 +6,18 @@ module Popen"
- :line_code:
- :position: !ruby/object:Gitlab::Diff::Position
- attributes:
- :old_path: files/ruby/popen.rb
- :new_path: files/ruby/popen.rb
- :old_line:
- :new_line:
- :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
-- :left:
- :type:
- :number: 6
- :text: |2
- <span id="LC6" class="line"></span>
- :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_6_6
- :position: !ruby/object:Gitlab::Diff::Position
- attributes:
- :old_path: files/ruby/popen.rb
- :new_path: files/ruby/popen.rb
- :old_line: 6
- :new_line: 6
- :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
- :right:
- :type:
- :number: 6
- :text: |2
- <span id="LC6" class="line"></span>
- :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_6_6
- :position: !ruby/object:Gitlab::Diff::Position
- attributes:
- :old_path: files/ruby/popen.rb
- :new_path: files/ruby/popen.rb
- :old_line: 6
- :new_line: 6
- :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
-- :left:
- :type:
- :number: 7
- :text: |2
- <span id="LC7" class="line"> <span class="k">def</span> <span class="nf">popen</span><span class="p">(</span><span class="n">cmd</span><span class="p">,</span> <span class="n">path</span><span class="o">=</span><span class="kp">nil</span><span class="p">)</span></span>
- :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_7_7
- :position: !ruby/object:Gitlab::Diff::Position
- attributes:
- :old_path: files/ruby/popen.rb
- :new_path: files/ruby/popen.rb
- :old_line: 7
- :new_line: 7
- :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
- :right:
- :type:
- :number: 7
- :text: |2
- <span id="LC7" class="line"> <span class="k">def</span> <span class="nf">popen</span><span class="p">(</span><span class="n">cmd</span><span class="p">,</span> <span class="n">path</span><span class="o">=</span><span class="kp">nil</span><span class="p">)</span></span>
- :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_7_7
- :position: !ruby/object:Gitlab::Diff::Position
- attributes:
- :old_path: files/ruby/popen.rb
- :new_path: files/ruby/popen.rb
- :old_line: 7
- :new_line: 7
- :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
-- :left:
- :type:
- :number: 8
- :text: |2
- <span id="LC8" class="line"> <span class="k">unless</span> <span class="n">cmd</span><span class="p">.</span><span class="nf">is_a?</span><span class="p">(</span><span class="no">Array</span><span class="p">)</span></span>
- :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_8_8
- :position: !ruby/object:Gitlab::Diff::Position
- attributes:
- :old_path: files/ruby/popen.rb
- :new_path: files/ruby/popen.rb
- :old_line: 8
- :new_line: 8
- :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
- :right:
- :type:
- :number: 8
- :text: |2
- <span id="LC8" class="line"> <span class="k">unless</span> <span class="n">cmd</span><span class="p">.</span><span class="nf">is_a?</span><span class="p">(</span><span class="no">Array</span><span class="p">)</span></span>
- :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_8_8
- :position: !ruby/object:Gitlab::Diff::Position
- attributes:
- :old_path: files/ruby/popen.rb
- :new_path: files/ruby/popen.rb
- :old_line: 8
- :new_line: 8
- :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
-- :left:
- :type: old
- :number: 9
- :text: |
- -<span id="LC9" class="line"> <span class="k">raise</span> <span class="s2">&quot;System commands must be given as an array of strings&quot;</span></span>
- :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_9_9
- :position: !ruby/object:Gitlab::Diff::Position
- attributes:
- :old_path: files/ruby/popen.rb
- :new_path: files/ruby/popen.rb
- :old_line: 9
- :new_line:
- :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
- :right:
- :type: new
- :number: 9
- :text: |
- +<span id="LC9" class="line"> <span class="k">raise</span> <span class="no"><span class='idiff left'>RuntimeError</span></span><span class="p"><span class='idiff'>,</span></span><span class='idiff right'> </span><span class="s2">&quot;System commands must be given as an array of strings&quot;</span></span>
- :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9
- :position: !ruby/object:Gitlab::Diff::Position
- attributes:
- :old_path: files/ruby/popen.rb
- :new_path: files/ruby/popen.rb
- :old_line:
- :new_line: 9
- :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
-- :left:
- :type:
- :number: 10
- :text: |2
- <span id="LC10" class="line"> <span class="k">end</span></span>
- :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_10
- :position: !ruby/object:Gitlab::Diff::Position
- attributes:
- :old_path: files/ruby/popen.rb
- :new_path: files/ruby/popen.rb
- :old_line: 10
- :new_line: 10
- :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
- :right:
- :type:
- :number: 10
- :text: |2
- <span id="LC10" class="line"> <span class="k">end</span></span>
- :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_10
- :position: !ruby/object:Gitlab::Diff::Position
- attributes:
- :old_path: files/ruby/popen.rb
- :new_path: files/ruby/popen.rb
- :old_line: 10
- :new_line: 10
- :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
-- :left:
- :type:
- :number: 11
- :text: |2
- <span id="LC11" class="line"></span>
- :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_11_11
- :position: !ruby/object:Gitlab::Diff::Position
- attributes:
- :old_path: files/ruby/popen.rb
- :new_path: files/ruby/popen.rb
- :old_line: 11
- :new_line: 11
- :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
- :right:
- :type:
- :number: 11
- :text: |2
- <span id="LC11" class="line"></span>
- :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_11_11
- :position: !ruby/object:Gitlab::Diff::Position
- attributes:
- :old_path: files/ruby/popen.rb
- :new_path: files/ruby/popen.rb
- :old_line: 11
- :new_line: 11
- :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
-- :left:
- :type:
- :number: 12
- :text: |2
- <span id="LC12" class="line"> <span class="n">path</span> <span class="o">||=</span> <span class="no">Dir</span><span class="p">.</span><span class="nf">pwd</span></span>
- :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_12_12
- :position: !ruby/object:Gitlab::Diff::Position
- attributes:
- :old_path: files/ruby/popen.rb
- :new_path: files/ruby/popen.rb
- :old_line: 12
- :new_line: 12
- :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
- :right:
- :type:
- :number: 12
- :text: |2
- <span id="LC12" class="line"> <span class="n">path</span> <span class="o">||=</span> <span class="no">Dir</span><span class="p">.</span><span class="nf">pwd</span></span>
- :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_12_12
- :position: !ruby/object:Gitlab::Diff::Position
- attributes:
- :old_path: files/ruby/popen.rb
- :new_path: files/ruby/popen.rb
- :old_line: 12
- :new_line: 12
- :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
-- :left:
- :type: old
- :number: 13
- :text: |
- -<span id="LC13" class="line"> <span class="n">vars</span> <span class="o">=</span> <span class="p">{</span> <span class="s2">&quot;PWD&quot;</span> <span class="o">=&gt;</span> <span class="n">path</span> <span class="p">}</span></span>
- :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_13_13
- :position: !ruby/object:Gitlab::Diff::Position
- attributes:
- :old_path: files/ruby/popen.rb
- :new_path: files/ruby/popen.rb
- :old_line: 13
- :new_line:
- :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
- :right:
- :type: old
- :number:
- :text: ''
- :line_code:
- :position:
-- :left:
- :type: old
- :number: 14
- :text: |
- -<span id="LC14" class="line"> <span class="n">options</span> <span class="o">=</span> <span class="p">{</span> <span class="ss">chdir: </span><span class="n">path</span> <span class="p">}</span></span>
- :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_14_13
- :position: !ruby/object:Gitlab::Diff::Position
- attributes:
- :old_path: files/ruby/popen.rb
- :new_path: files/ruby/popen.rb
- :old_line: 14
- :new_line:
- :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
- :right:
- :type: new
- :number: 13
- :text: |
- +<span id="LC13" class="line"></span>
- :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_13
- :position: !ruby/object:Gitlab::Diff::Position
- attributes:
- :old_path: files/ruby/popen.rb
- :new_path: files/ruby/popen.rb
- :old_line:
- :new_line: 13
- :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
-- :left:
- :type:
- :number:
- :text: ''
- :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_14
- :position: !ruby/object:Gitlab::Diff::Position
- attributes:
- :old_path: files/ruby/popen.rb
- :new_path: files/ruby/popen.rb
- :old_line:
- :new_line: 14
- :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
- :right:
- :type: new
- :number: 14
- :text: |
- +<span id="LC14" class="line"> <span class="n">vars</span> <span class="o">=</span> <span class="p">{</span></span>
- :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_14
- :position: !ruby/object:Gitlab::Diff::Position
- attributes:
- :old_path: files/ruby/popen.rb
- :new_path: files/ruby/popen.rb
- :old_line:
- :new_line: 14
- :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
-- :left:
- :type:
- :number:
- :text: ''
- :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_15
- :position: !ruby/object:Gitlab::Diff::Position
- attributes:
- :old_path: files/ruby/popen.rb
- :new_path: files/ruby/popen.rb
- :old_line:
- :new_line: 15
- :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
- :right:
- :type: new
- :number: 15
- :text: |
- +<span id="LC15" class="line"> <span class="s2">&quot;PWD&quot;</span> <span class="o">=&gt;</span> <span class="n">path</span></span>
- :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_15
- :position: !ruby/object:Gitlab::Diff::Position
- attributes:
- :old_path: files/ruby/popen.rb
- :new_path: files/ruby/popen.rb
- :old_line:
- :new_line: 15
- :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
-- :left:
- :type:
- :number:
- :text: ''
- :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_16
- :position: !ruby/object:Gitlab::Diff::Position
- attributes:
- :old_path: files/ruby/popen.rb
- :new_path: files/ruby/popen.rb
- :old_line:
- :new_line: 16
- :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
- :right:
- :type: new
- :number: 16
- :text: |
- +<span id="LC16" class="line"> <span class="p">}</span></span>
- :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_16
- :position: !ruby/object:Gitlab::Diff::Position
- attributes:
- :old_path: files/ruby/popen.rb
- :new_path: files/ruby/popen.rb
- :old_line:
- :new_line: 16
- :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
-- :left:
- :type:
- :number:
- :text: ''
- :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_17
- :position: !ruby/object:Gitlab::Diff::Position
- attributes:
- :old_path: files/ruby/popen.rb
- :new_path: files/ruby/popen.rb
- :old_line:
- :new_line: 17
- :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
- :right:
- :type: new
- :number: 17
- :text: |
- +<span id="LC17" class="line"></span>
- :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_17
- :position: !ruby/object:Gitlab::Diff::Position
- attributes:
- :old_path: files/ruby/popen.rb
- :new_path: files/ruby/popen.rb
- :old_line:
- :new_line: 17
- :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
-- :left:
- :type:
- :number:
- :text: ''
- :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_18
- :position: !ruby/object:Gitlab::Diff::Position
- attributes:
- :old_path: files/ruby/popen.rb
- :new_path: files/ruby/popen.rb
- :old_line:
- :new_line: 18
- :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
- :right:
- :type: new
- :number: 18
- :text: |
- +<span id="LC18" class="line"> <span class="n">options</span> <span class="o">=</span> <span class="p">{</span></span>
- :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_18
- :position: !ruby/object:Gitlab::Diff::Position
- attributes:
- :old_path: files/ruby/popen.rb
- :new_path: files/ruby/popen.rb
- :old_line:
- :new_line: 18
- :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
-- :left:
- :type:
- :number:
- :text: ''
- :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_19
- :position: !ruby/object:Gitlab::Diff::Position
- attributes:
- :old_path: files/ruby/popen.rb
- :new_path: files/ruby/popen.rb
- :old_line:
- :new_line: 19
- :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
- :right:
- :type: new
- :number: 19
- :text: |
- +<span id="LC19" class="line"> <span class="ss">chdir: </span><span class="n">path</span></span>
- :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_19
- :position: !ruby/object:Gitlab::Diff::Position
- attributes:
- :old_path: files/ruby/popen.rb
- :new_path: files/ruby/popen.rb
- :old_line:
- :new_line: 19
- :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
-- :left:
- :type:
- :number:
- :text: ''
- :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_20
- :position: !ruby/object:Gitlab::Diff::Position
- attributes:
- :old_path: files/ruby/popen.rb
- :new_path: files/ruby/popen.rb
- :old_line:
- :new_line: 20
- :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
- :right:
- :type: new
- :number: 20
- :text: |
- +<span id="LC20" class="line"> <span class="p">}</span></span>
- :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_20
- :position: !ruby/object:Gitlab::Diff::Position
- attributes:
- :old_path: files/ruby/popen.rb
- :new_path: files/ruby/popen.rb
- :old_line:
- :new_line: 20
- :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
-- :left:
- :type:
- :number: 15
- :text: |2
- <span id="LC21" class="line"></span>
- :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_21
- :position: !ruby/object:Gitlab::Diff::Position
- attributes:
- :old_path: files/ruby/popen.rb
- :new_path: files/ruby/popen.rb
- :old_line: 15
- :new_line: 21
- :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
- :right:
- :type:
- :number: 21
- :text: |2
- <span id="LC21" class="line"></span>
- :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_21
- :position: !ruby/object:Gitlab::Diff::Position
- attributes:
- :old_path: files/ruby/popen.rb
- :new_path: files/ruby/popen.rb
- :old_line: 15
- :new_line: 21
- :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
-- :left:
- :type:
- :number: 16
- :text: |2
- <span id="LC22" class="line"> <span class="k">unless</span> <span class="no">File</span><span class="p">.</span><span class="nf">directory?</span><span class="p">(</span><span class="n">path</span><span class="p">)</span></span>
- :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_16_22
- :position: !ruby/object:Gitlab::Diff::Position
- attributes:
- :old_path: files/ruby/popen.rb
- :new_path: files/ruby/popen.rb
- :old_line: 16
- :new_line: 22
- :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
- :right:
- :type:
- :number: 22
- :text: |2
- <span id="LC22" class="line"> <span class="k">unless</span> <span class="no">File</span><span class="p">.</span><span class="nf">directory?</span><span class="p">(</span><span class="n">path</span><span class="p">)</span></span>
- :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_16_22
- :position: !ruby/object:Gitlab::Diff::Position
- attributes:
- :old_path: files/ruby/popen.rb
- :new_path: files/ruby/popen.rb
- :old_line: 16
- :new_line: 22
- :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
-- :left:
- :type:
- :number: 17
- :text: |2
- <span id="LC23" class="line"> <span class="no">FileUtils</span><span class="p">.</span><span class="nf">mkdir_p</span><span class="p">(</span><span class="n">path</span><span class="p">)</span></span>
- :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_17_23
- :position: !ruby/object:Gitlab::Diff::Position
- attributes:
- :old_path: files/ruby/popen.rb
- :new_path: files/ruby/popen.rb
- :old_line: 17
- :new_line: 23
- :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
- :right:
- :type:
- :number: 23
- :text: |2
- <span id="LC23" class="line"> <span class="no">FileUtils</span><span class="p">.</span><span class="nf">mkdir_p</span><span class="p">(</span><span class="n">path</span><span class="p">)</span></span>
- :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_17_23
- :position: !ruby/object:Gitlab::Diff::Position
- attributes:
- :old_path: files/ruby/popen.rb
- :new_path: files/ruby/popen.rb
- :old_line: 17
- :new_line: 23
- :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
-- :left:
- :type: match
- :number: 19
- :text: "@@ -19,6 +25,7 @@ module Popen"
- :line_code:
- :position: !ruby/object:Gitlab::Diff::Position
- attributes:
- :old_path: files/ruby/popen.rb
- :new_path: files/ruby/popen.rb
- :old_line:
- :new_line:
- :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
- :right:
- :type: match
- :number: 25
- :text: "@@ -19,6 +25,7 @@ module Popen"
- :line_code:
- :position: !ruby/object:Gitlab::Diff::Position
- attributes:
- :old_path: files/ruby/popen.rb
- :new_path: files/ruby/popen.rb
- :old_line:
- :new_line:
- :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
-- :left:
- :type:
- :number: 19
- :text: |2
- <span id="LC25" class="line"></span>
- :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_19_25
- :position: !ruby/object:Gitlab::Diff::Position
- attributes:
- :old_path: files/ruby/popen.rb
- :new_path: files/ruby/popen.rb
- :old_line: 19
- :new_line: 25
- :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
- :right:
- :type:
- :number: 25
- :text: |2
- <span id="LC25" class="line"></span>
- :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_19_25
- :position: !ruby/object:Gitlab::Diff::Position
- attributes:
- :old_path: files/ruby/popen.rb
- :new_path: files/ruby/popen.rb
- :old_line: 19
- :new_line: 25
- :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
-- :left:
- :type:
- :number: 20
- :text: |2
- <span id="LC26" class="line"> <span class="vi">@cmd_output</span> <span class="o">=</span> <span class="s2">&quot;&quot;</span></span>
- :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_20_26
- :position: !ruby/object:Gitlab::Diff::Position
- attributes:
- :old_path: files/ruby/popen.rb
- :new_path: files/ruby/popen.rb
- :old_line: 20
- :new_line: 26
- :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
- :right:
- :type:
- :number: 26
- :text: |2
- <span id="LC26" class="line"> <span class="vi">@cmd_output</span> <span class="o">=</span> <span class="s2">&quot;&quot;</span></span>
- :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_20_26
- :position: !ruby/object:Gitlab::Diff::Position
- attributes:
- :old_path: files/ruby/popen.rb
- :new_path: files/ruby/popen.rb
- :old_line: 20
- :new_line: 26
- :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
-- :left:
- :type:
- :number: 21
- :text: |2
- <span id="LC27" class="line"> <span class="vi">@cmd_status</span> <span class="o">=</span> <span class="mi">0</span></span>
- :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_21_27
- :position: !ruby/object:Gitlab::Diff::Position
- attributes:
- :old_path: files/ruby/popen.rb
- :new_path: files/ruby/popen.rb
- :old_line: 21
- :new_line: 27
- :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
- :right:
- :type:
- :number: 27
- :text: |2
- <span id="LC27" class="line"> <span class="vi">@cmd_status</span> <span class="o">=</span> <span class="mi">0</span></span>
- :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_21_27
- :position: !ruby/object:Gitlab::Diff::Position
- attributes:
- :old_path: files/ruby/popen.rb
- :new_path: files/ruby/popen.rb
- :old_line: 21
- :new_line: 27
- :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
-- :left:
- :type:
- :number:
- :text: ''
- :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_22_28
- :position: !ruby/object:Gitlab::Diff::Position
- attributes:
- :old_path: files/ruby/popen.rb
- :new_path: files/ruby/popen.rb
- :old_line:
- :new_line: 28
- :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
- :right:
- :type: new
- :number: 28
- :text: |
- +<span id="LC28" class="line"></span>
- :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_22_28
- :position: !ruby/object:Gitlab::Diff::Position
- attributes:
- :old_path: files/ruby/popen.rb
- :new_path: files/ruby/popen.rb
- :old_line:
- :new_line: 28
- :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
-- :left:
- :type:
- :number: 22
- :text: |2
- <span id="LC29" class="line"> <span class="no">Open3</span><span class="p">.</span><span class="nf">popen3</span><span class="p">(</span><span class="n">vars</span><span class="p">,</span> <span class="o">*</span><span class="n">cmd</span><span class="p">,</span> <span class="n">options</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">stdin</span><span class="p">,</span> <span class="n">stdout</span><span class="p">,</span> <span class="n">stderr</span><span class="p">,</span> <span class="n">wait_thr</span><span class="o">|</span></span>
- :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_22_29
- :position: !ruby/object:Gitlab::Diff::Position
- attributes:
- :old_path: files/ruby/popen.rb
- :new_path: files/ruby/popen.rb
- :old_line: 22
- :new_line: 29
- :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
- :right:
- :type:
- :number: 29
- :text: |2
- <span id="LC29" class="line"> <span class="no">Open3</span><span class="p">.</span><span class="nf">popen3</span><span class="p">(</span><span class="n">vars</span><span class="p">,</span> <span class="o">*</span><span class="n">cmd</span><span class="p">,</span> <span class="n">options</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">stdin</span><span class="p">,</span> <span class="n">stdout</span><span class="p">,</span> <span class="n">stderr</span><span class="p">,</span> <span class="n">wait_thr</span><span class="o">|</span></span>
- :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_22_29
- :position: !ruby/object:Gitlab::Diff::Position
- attributes:
- :old_path: files/ruby/popen.rb
- :new_path: files/ruby/popen.rb
- :old_line: 22
- :new_line: 29
- :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
-- :left:
- :type:
- :number: 23
- :text: |2
- <span id="LC30" class="line"> <span class="vi">@cmd_output</span> <span class="o">&lt;&lt;</span> <span class="n">stdout</span><span class="p">.</span><span class="nf">read</span></span>
- :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_23_30
- :position: !ruby/object:Gitlab::Diff::Position
- attributes:
- :old_path: files/ruby/popen.rb
- :new_path: files/ruby/popen.rb
- :old_line: 23
- :new_line: 30
- :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
- :right:
- :type:
- :number: 30
- :text: |2
- <span id="LC30" class="line"> <span class="vi">@cmd_output</span> <span class="o">&lt;&lt;</span> <span class="n">stdout</span><span class="p">.</span><span class="nf">read</span></span>
- :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_23_30
- :position: !ruby/object:Gitlab::Diff::Position
- attributes:
- :old_path: files/ruby/popen.rb
- :new_path: files/ruby/popen.rb
- :old_line: 23
- :new_line: 30
- :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
-- :left:
- :type:
- :number: 24
- :text: |2
- <span id="LC31" class="line"> <span class="vi">@cmd_output</span> <span class="o">&lt;&lt;</span> <span class="n">stderr</span><span class="p">.</span><span class="nf">read</span></span>
- :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_24_31
- :position: !ruby/object:Gitlab::Diff::Position
- attributes:
- :old_path: files/ruby/popen.rb
- :new_path: files/ruby/popen.rb
- :old_line: 24
- :new_line: 31
- :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
- :right:
- :type:
- :number: 31
- :text: |2
- <span id="LC31" class="line"> <span class="vi">@cmd_output</span> <span class="o">&lt;&lt;</span> <span class="n">stderr</span><span class="p">.</span><span class="nf">read</span></span>
- :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_24_31
- :position: !ruby/object:Gitlab::Diff::Position
- attributes:
- :old_path: files/ruby/popen.rb
- :new_path: files/ruby/popen.rb
- :old_line: 24
- :new_line: 31
- :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
diff --git a/spec/fixtures/project_services/campfire/rooms.json b/spec/fixtures/project_services/campfire/rooms.json
new file mode 100644
index 00000000000..71e9645c955
--- /dev/null
+++ b/spec/fixtures/project_services/campfire/rooms.json
@@ -0,0 +1,22 @@
+{
+ "rooms": [
+ {
+ "name": "test-room",
+ "locked": false,
+ "created_at": "2009/01/07 20:43:11 +0000",
+ "updated_at": "2009/03/18 14:31:39 +0000",
+ "topic": "The room topic\n",
+ "id": 123,
+ "membership_limit": 4
+ },
+ {
+ "name": "another room",
+ "locked": true,
+ "created_at": "2009/03/18 14:30:42 +0000",
+ "updated_at": "2013/01/27 14:14:27 +0000",
+ "topic": "Comment, ideas, GitHub notifications for eCommittee App",
+ "id": 456,
+ "membership_limit": 4
+ }
+ ]
+}
diff --git a/spec/fixtures/project_services/campfire/rooms2.json b/spec/fixtures/project_services/campfire/rooms2.json
new file mode 100644
index 00000000000..3d5f635d8b3
--- /dev/null
+++ b/spec/fixtures/project_services/campfire/rooms2.json
@@ -0,0 +1,22 @@
+{
+ "rooms": [
+ {
+ "name": "test-room-not-found",
+ "locked": false,
+ "created_at": "2009/01/07 20:43:11 +0000",
+ "updated_at": "2009/03/18 14:31:39 +0000",
+ "topic": "The room topic\n",
+ "id": 123,
+ "membership_limit": 4
+ },
+ {
+ "name": "another room",
+ "locked": true,
+ "created_at": "2009/03/18 14:30:42 +0000",
+ "updated_at": "2013/01/27 14:14:27 +0000",
+ "topic": "Comment, ideas, GitHub notifications for eCommittee App",
+ "id": 456,
+ "membership_limit": 4
+ }
+ ]
+}
diff --git a/spec/fixtures/video_sample.mp4 b/spec/fixtures/video_sample.mp4
new file mode 100644
index 00000000000..acd45190998
--- /dev/null
+++ b/spec/fixtures/video_sample.mp4
Binary files differ
diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb
index bb28866f010..73f5470cf35 100644
--- a/spec/helpers/application_helper_spec.rb
+++ b/spec/helpers/application_helper_spec.rb
@@ -54,7 +54,7 @@ describe ApplicationHelper do
describe 'project_icon' do
let(:avatar_file_path) { File.join(Rails.root, 'spec', 'fixtures', 'banana_sample.gif') }
- it 'should return an url for the avatar' do
+ it 'returns an url for the avatar' do
project = create(:project, avatar: File.open(avatar_file_path))
avatar_url = "http://localhost/uploads/project/avatar/#{project.id}/banana_sample.gif"
@@ -62,7 +62,7 @@ describe ApplicationHelper do
to eq "<img src=\"#{avatar_url}\" alt=\"Banana sample\" />"
end
- it 'should give uploaded icon when present' do
+ it 'gives uploaded icon when present' do
project = create(:project)
allow_any_instance_of(Project).to receive(:avatar_in_git).and_return(true)
@@ -76,14 +76,14 @@ describe ApplicationHelper do
describe 'avatar_icon' do
let(:avatar_file_path) { File.join(Rails.root, 'spec', 'fixtures', 'banana_sample.gif') }
- it 'should return an url for the avatar' do
+ it 'returns an url for the avatar' do
user = create(:user, avatar: File.open(avatar_file_path))
expect(helper.avatar_icon(user.email).to_s).
to match("/uploads/user/avatar/#{user.id}/banana_sample.gif")
end
- it 'should return an url for the avatar with relative url' do
+ it 'returns an url for the avatar with relative url' do
stub_config_setting(relative_url_root: '/gitlab')
# Must be stubbed after the stub above, and separately
stub_config_setting(url: Settings.send(:build_gitlab_url))
@@ -94,14 +94,14 @@ describe ApplicationHelper do
to match("/gitlab/uploads/user/avatar/#{user.id}/banana_sample.gif")
end
- it 'should call gravatar_icon when no User exists with the given email' do
+ it 'calls gravatar_icon when no User exists with the given email' do
expect(helper).to receive(:gravatar_icon).with('foo@example.com', 20, 2)
helper.avatar_icon('foo@example.com', 20, 2)
end
describe 'using a User' do
- it 'should return an URL for the avatar' do
+ it 'returns an URL for the avatar' do
user = create(:user, avatar: File.open(avatar_file_path))
expect(helper.avatar_icon(user).to_s).
@@ -146,7 +146,7 @@ describe ApplicationHelper do
to match('https://secure.gravatar.com')
end
- it 'should return custom gravatar path when gravatar_url is set' do
+ it 'returns custom gravatar path when gravatar_url is set' do
stub_gravatar_setting(plain_url: 'http://example.local/?s=%{size}&hash=%{hash}')
expect(gravatar_icon(user_email, 20)).
@@ -218,12 +218,12 @@ describe ApplicationHelper do
end
it 'includes a default js-timeago class' do
- expect(element.attr('class')).to eq 'time_ago js-timeago js-timeago-pending'
+ expect(element.attr('class')).to eq 'js-timeago js-timeago-pending'
end
it 'accepts a custom html_class' do
expect(element(html_class: 'custom_class').attr('class')).
- to eq 'custom_class js-timeago js-timeago-pending'
+ to eq 'js-timeago custom_class js-timeago-pending'
end
it 'accepts a custom tooltip placement' do
@@ -244,6 +244,19 @@ describe ApplicationHelper do
it 'converts to Time' do
expect { helper.time_ago_with_tooltip(Date.today) }.not_to raise_error
end
+
+ it 'add class for the short format and includes inline script' do
+ timeago_element = element(short_format: 'short')
+ expect(timeago_element.attr('class')).to eq 'js-short-timeago js-timeago-pending'
+ script_element = timeago_element.next_element
+ expect(script_element.name).to eq 'script'
+ end
+
+ it 'add class for the short format and does not include inline script' do
+ timeago_element = element(short_format: 'short', skip_js: true)
+ expect(timeago_element.attr('class')).to eq 'js-short-timeago'
+ expect(timeago_element.next_element).to eq nil
+ end
end
describe 'render_markup' do
@@ -253,19 +266,19 @@ describe ApplicationHelper do
allow(helper).to receive(:current_user).and_return(user)
end
- it 'should preserve encoding' do
+ it 'preserves encoding' do
expect(content.encoding.name).to eq('UTF-8')
expect(helper.render_markup('foo.rst', content).encoding.name).to eq('UTF-8')
end
- it "should delegate to #markdown when file name corresponds to Markdown" do
+ it "delegates to #markdown when file name corresponds to Markdown" do
expect(helper).to receive(:gitlab_markdown?).with('foo.md').and_return(true)
expect(helper).to receive(:markdown).and_return('NOEL')
expect(helper.render_markup('foo.md', content)).to eq('NOEL')
end
- it "should delegate to #asciidoc when file name corresponds to AsciiDoc" do
+ it "delegates to #asciidoc when file name corresponds to AsciiDoc" do
expect(helper).to receive(:asciidoc?).with('foo.adoc').and_return(true)
expect(helper).to receive(:asciidoc).and_return('NOEL')
diff --git a/spec/helpers/blob_helper_spec.rb b/spec/helpers/blob_helper_spec.rb
index 6d1c02db297..a43a7238c70 100644
--- a/spec/helpers/blob_helper_spec.rb
+++ b/spec/helpers/blob_helper_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe BlobHelper do
+ include TreeHelper
+
let(:blob_name) { 'test.lisp' }
let(:no_context_content) { ":type \"assem\"))" }
let(:blob_content) { "(make-pathname :defaults name\n#{no_context_content}" }
@@ -15,20 +17,20 @@ describe BlobHelper do
end
describe '#highlight' do
- it 'should return plaintext for unknown lexer context' do
- result = helper.highlight(blob_name, no_context_content, nowrap: true)
- expect(result).to eq('<span id="LC1" class="line">:type &quot;assem&quot;))</span>')
+ it 'returns plaintext for unknown lexer context' do
+ result = helper.highlight(blob_name, no_context_content)
+ expect(result).to eq(%[<pre class="code highlight"><code><span id="LC1" class="line">:type "assem"))</span></code></pre>])
end
- it 'should highlight single block' do
- expected = %Q[<span id="LC1" class="line"><span class="p">(</span><span class="nb">make-pathname</span> <span class="ss">:defaults</span> <span class="nv">name</span></span>
-<span id="LC2" class="line"><span class="ss">:type</span> <span class="s">&quot;assem&quot;</span><span class="p">))</span></span>]
+ it 'highlights single block' do
+ expected = %Q[<pre class="code highlight"><code><span id="LC1" class="line"><span class="p">(</span><span class="nb">make-pathname</span> <span class="ss">:defaults</span> <span class="nv">name</span></span>
+<span id="LC2" class="line"><span class="ss">:type</span> <span class="s">"assem"</span><span class="p">))</span></span></code></pre>]
- expect(helper.highlight(blob_name, blob_content, nowrap: true)).to eq(expected)
+ expect(helper.highlight(blob_name, blob_content)).to eq(expected)
end
- it 'should highlight multi-line comments' do
- result = helper.highlight(blob_name, multiline_content, nowrap: true)
+ it 'highlights multi-line comments' do
+ result = helper.highlight(blob_name, multiline_content)
html = Nokogiri::HTML(result)
lines = html.search('.s')
expect(lines.count).to eq(3)
@@ -41,42 +43,66 @@ describe BlobHelper do
let(:blob_name) { 'test.diff' }
let(:blob_content) { "+aaa\n+bbb\n- ccc\n ddd\n"}
let(:expected) do
- %q(<span id="LC1" class="line"><span class="gi">+aaa</span></span>
+ %q(<pre class="code highlight"><code><span id="LC1" class="line"><span class="gi">+aaa</span></span>
<span id="LC2" class="line"><span class="gi">+bbb</span></span>
<span id="LC3" class="line"><span class="gd">- ccc</span></span>
-<span id="LC4" class="line"> ddd</span>)
+<span id="LC4" class="line"> ddd</span></code></pre>)
end
- it 'should highlight each line properly' do
- result = helper.highlight(blob_name, blob_content, nowrap: true)
+ it 'highlights each line properly' do
+ result = helper.highlight(blob_name, blob_content)
expect(result).to eq(expected)
end
end
end
- describe "#highlighter" do
- it 'should highlight continued blocks' do
- # Both lines have LC1 as ID since formatter doesn't support continue at the moment
- expected = [
- '<span id="LC1" class="line"><span class="p">(</span><span class="nb">make-pathname</span> <span class="ss">:defaults</span> <span class="nv">name</span></span>',
- '<span id="LC1" class="line"><span class="ss">:type</span> <span class="s">&quot;assem&quot;</span><span class="p">))</span></span>'
- ]
-
- highlighter = helper.highlighter(blob_name, blob_content, nowrap: true)
- result = split_content.map{ |content| highlighter.highlight(content) }
- expect(result).to eq(expected)
- end
- end
-
describe "#sanitize_svg" do
let(:input_svg_path) { File.join(Rails.root, 'spec', 'fixtures', 'unsanitized.svg') }
let(:data) { open(input_svg_path).read }
let(:expected_svg_path) { File.join(Rails.root, 'spec', 'fixtures', 'sanitized.svg') }
let(:expected) { open(expected_svg_path).read }
- it 'should retain essential elements' do
+ it 'retains essential elements' do
blob = OpenStruct.new(data: data)
expect(sanitize_svg(blob).data).to eq(expected)
end
end
+
+ describe "#edit_blob_link" do
+ let(:namespace) { create(:namespace, name: 'gitlab' )}
+ let(:project) { create(:project, namespace: namespace) }
+
+ before do
+ allow(self).to receive(:current_user).and_return(double)
+ allow(self).to receive(:can_collaborate_with_project?).and_return(true)
+ end
+
+ it 'verifies blob is text' do
+ expect(helper).not_to receive(:blob_text_viewable?)
+
+ button = edit_blob_link(project, 'refs/heads/master', 'README.md')
+
+ expect(button).to start_with('<button')
+ end
+
+ it 'uses the passed blob instead retrieve from repository' do
+ blob = project.repository.blob_at('refs/heads/master', 'README.md')
+
+ expect(project.repository).not_to receive(:blob_at)
+
+ edit_blob_link(project, 'refs/heads/master', 'README.md', blob: blob)
+ end
+
+ it 'returns a link with the proper route' do
+ link = edit_blob_link(project, 'master', 'README.md')
+
+ expect(Capybara.string(link).find_link('Edit')[:href]).to eq('/gitlab/gitlabhq/edit/master/README.md')
+ end
+
+ it 'returns a link with the passed link_opts on the expected route' do
+ link = edit_blob_link(project, 'master', 'README.md', link_opts: { mr_id: 10 })
+
+ expect(Capybara.string(link).find_link('Edit')[:href]).to eq('/gitlab/gitlabhq/edit/master/README.md?mr_id=10')
+ end
+ end
end
diff --git a/spec/helpers/ci_status_helper_spec.rb b/spec/helpers/ci_status_helper_spec.rb
index 45199d0f09d..637b02d9388 100644
--- a/spec/helpers/ci_status_helper_spec.rb
+++ b/spec/helpers/ci_status_helper_spec.rb
@@ -7,7 +7,13 @@ describe CiStatusHelper do
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') }
- it { expect(helper.ci_icon_for_status(failed_commit.status)).to include('fa-close') }
+ it 'renders to correct svg on success' do
+ expect(helper).to receive(:render).with('shared/icons/icon_status_success.svg', anything)
+ helper.ci_icon_for_status(success_commit.status)
+ end
+ it 'renders the correct svg on failure' do
+ expect(helper).to receive(:render).with('shared/icons/icon_status_failed.svg', anything)
+ helper.ci_icon_for_status(failed_commit.status)
+ end
end
end
diff --git a/spec/helpers/diff_helper_spec.rb b/spec/helpers/diff_helper_spec.rb
index e2db33d8345..9c7c79f57c6 100644
--- a/spec/helpers/diff_helper_spec.rb
+++ b/spec/helpers/diff_helper_spec.rb
@@ -6,7 +6,7 @@ describe DiffHelper do
let(:project) { create(:project) }
let(:repository) { project.repository }
let(:commit) { project.commit(sample_commit.id) }
- let(:diffs) { commit.diffs }
+ let(:diffs) { commit.raw_diffs }
let(:diff) { diffs.first }
let(:diff_refs) { [commit.parent, commit] }
let(:diff_file) { Gitlab::Diff::File.new(diff, diff_refs: diff_refs, repository: repository) }
@@ -15,70 +15,56 @@ describe DiffHelper do
it 'returns a valid value when cookie is set' do
helper.request.cookies[:diff_view] = 'parallel'
- expect(helper.diff_view).to eq 'parallel'
+ expect(helper.diff_view).to eq :parallel
end
it 'returns a default value when cookie is invalid' do
helper.request.cookies[:diff_view] = 'invalid'
- expect(helper.diff_view).to eq 'inline'
+ expect(helper.diff_view).to eq :inline
end
it 'returns a default value when cookie is nil' do
expect(helper.request.cookies).to be_empty
- expect(helper.diff_view).to eq 'inline'
- end
- end
-
- describe 'diff_hard_limit_enabled?' do
- it 'should return true if param is provided' do
- allow(controller).to receive(:params) { { force_show_diff: true } }
- expect(diff_hard_limit_enabled?).to be_truthy
- end
-
- it 'should return false if param is not provided' do
- expect(diff_hard_limit_enabled?).to be_falsey
+ expect(helper.diff_view).to eq :inline
end
end
describe 'diff_options' do
- it 'should return hard limit for a diff if force diff is true' do
- allow(controller).to receive(:params) { { force_show_diff: true } }
- expect(diff_options).to include(Commit.max_diff_options)
+ it 'returns no collapse false' do
+ expect(diff_options).to include(no_collapse: false)
end
- it 'should return safe limit for a diff if force diff is false' do
- expect(diff_options).not_to include(:max_lines, :max_files)
+ it 'returns no collapse true if expand_all_diffs' do
+ allow(controller).to receive(:params) { { expand_all_diffs: true } }
+ expect(diff_options).to include(no_collapse: true)
end
- end
- describe 'unfold_bottom_class' do
- it 'should return empty string when bottom line shouldnt be unfolded' do
- expect(unfold_bottom_class(false)).to eq('')
+ it 'returns no collapse true if action name diff_for_path' do
+ allow(controller).to receive(:action_name) { 'diff_for_path' }
+ expect(diff_options).to include(no_collapse: true)
end
- it 'should return js class when bottom lines should be unfolded' do
- expect(unfold_bottom_class(true)).to eq('js-unfold-bottom')
+ it 'returns paths if action name diff_for_path and param old path' do
+ allow(controller).to receive(:params) { { old_path: 'lib/wadus.rb' } }
+ allow(controller).to receive(:action_name) { 'diff_for_path' }
+ expect(diff_options[:paths]).to include('lib/wadus.rb')
end
- end
- describe 'unfold_class' do
- it 'returns empty on false' do
- expect(unfold_class(false)).to eq('')
- end
-
- it 'returns a class on true' do
- expect(unfold_class(true)).to eq('unfold js-unfold')
+ it 'returns paths if action name diff_for_path and param new path' do
+ allow(controller).to receive(:params) { { new_path: 'lib/wadus.rb' } }
+ allow(controller).to receive(:action_name) { 'diff_for_path' }
+ expect(diff_options[:paths]).to include('lib/wadus.rb')
end
end
describe '#diff_line_content' do
- it 'should return non breaking space when line is empty' do
+ it 'returns non breaking space when line is empty' do
expect(diff_line_content(nil)).to eq(' &nbsp;')
end
- it 'should return the line itself' do
+ it 'returns the line itself' do
expect(diff_line_content(diff_file.diff_lines.first.text)).
to eq('@@ -6,12 +6,18 @@ module Popen')
expect(diff_line_content(diff_file.diff_lines.first.type)).to eq('match')
@@ -99,4 +85,56 @@ describe DiffHelper do
expect(marked_new_line).to be_html_safe
end
end
+
+ describe "#diff_match_line" do
+ let(:old_pos) { 40 }
+ let(:new_pos) { 50 }
+ let(:text) { 'some_text' }
+
+ it "should generate foldable top match line for inline view with empty text by default" do
+ output = diff_match_line old_pos, new_pos
+
+ expect(output).to be_html_safe
+ expect(output).to have_css "td:nth-child(1):not(.js-unfold-bottom).diff-line-num.unfold.js-unfold.old_line[data-linenumber='#{old_pos}']", text: '...'
+ expect(output).to have_css "td:nth-child(2):not(.js-unfold-bottom).diff-line-num.unfold.js-unfold.new_line[data-linenumber='#{new_pos}']", text: '...'
+ expect(output).to have_css 'td:nth-child(3):not(.parallel).line_content.match', text: ''
+ end
+
+ it "should allow to define text and bottom option" do
+ output = diff_match_line old_pos, new_pos, text: text, bottom: true
+
+ expect(output).to be_html_safe
+ expect(output).to have_css "td:nth-child(1).diff-line-num.unfold.js-unfold.js-unfold-bottom.old_line[data-linenumber='#{old_pos}']", text: '...'
+ expect(output).to have_css "td:nth-child(2).diff-line-num.unfold.js-unfold.js-unfold-bottom.new_line[data-linenumber='#{new_pos}']", text: '...'
+ expect(output).to have_css 'td:nth-child(3):not(.parallel).line_content.match', text: text
+ end
+
+ it "should generate match line for parallel view" do
+ output = diff_match_line old_pos, new_pos, text: text, view: :parallel
+
+ expect(output).to be_html_safe
+ expect(output).to have_css "td:nth-child(1):not(.js-unfold-bottom).diff-line-num.unfold.js-unfold.old_line[data-linenumber='#{old_pos}']", text: '...'
+ expect(output).to have_css 'td:nth-child(2).line_content.match.parallel', text: text
+ expect(output).to have_css "td:nth-child(3):not(.js-unfold-bottom).diff-line-num.unfold.js-unfold.new_line[data-linenumber='#{new_pos}']", text: '...'
+ expect(output).to have_css 'td:nth-child(4).line_content.match.parallel', text: text
+ end
+
+ it "should allow to generate only left match line for parallel view" do
+ output = diff_match_line old_pos, nil, text: text, view: :parallel
+
+ expect(output).to be_html_safe
+ expect(output).to have_css "td:nth-child(1):not(.js-unfold-bottom).diff-line-num.unfold.js-unfold.old_line[data-linenumber='#{old_pos}']", text: '...'
+ expect(output).to have_css 'td:nth-child(2).line_content.match.parallel', text: text
+ expect(output).not_to have_css 'td:nth-child(3)'
+ end
+
+ it "should allow to generate only right match line for parallel view" do
+ output = diff_match_line nil, new_pos, text: text, view: :parallel
+
+ expect(output).to be_html_safe
+ expect(output).to have_css "td:nth-child(1):not(.js-unfold-bottom).diff-line-num.unfold.js-unfold.new_line[data-linenumber='#{new_pos}']", text: '...'
+ expect(output).to have_css 'td:nth-child(2).line_content.match.parallel', text: text
+ expect(output).not_to have_css 'td:nth-child(3)'
+ end
+ end
end
diff --git a/spec/helpers/emails_helper_spec.rb b/spec/helpers/emails_helper_spec.rb
index 7a3e38d7e63..3223556e1d3 100644
--- a/spec/helpers/emails_helper_spec.rb
+++ b/spec/helpers/emails_helper_spec.rb
@@ -8,37 +8,37 @@ describe EmailsHelper do
end
context 'when time limit is less than 2 hours' do
- it 'should display the time in hours using a singular unit' do
+ it 'displays the time in hours using a singular unit' do
validate_time_string(1.hour, '1 hour')
end
end
context 'when time limit is 2 or more hours' do
- it 'should display the time in hours using a plural unit' do
+ it 'displays the time in hours using a plural unit' do
validate_time_string(2.hours, '2 hours')
end
end
context 'when time limit contains fractions of an hour' do
- it 'should round down to the nearest hour' do
+ it 'rounds down to the nearest hour' do
validate_time_string(96.minutes, '1 hour')
end
end
context 'when time limit is 24 or more hours' do
- it 'should display the time in days using a singular unit' do
+ it 'displays the time in days using a singular unit' do
validate_time_string(24.hours, '1 day')
end
end
context 'when time limit is 2 or more days' do
- it 'should display the time in days using a plural unit' do
+ it 'displays the time in days using a plural unit' do
validate_time_string(2.days, '2 days')
end
end
context 'when time limit contains fractions of a day' do
- it 'should round down to the nearest day' do
+ it 'rounds down to the nearest day' do
validate_time_string(57.hours, '2 days')
end
end
diff --git a/spec/helpers/events_helper_spec.rb b/spec/helpers/events_helper_spec.rb
index c0d2be98e85..022aba0c0d0 100644
--- a/spec/helpers/events_helper_spec.rb
+++ b/spec/helpers/events_helper_spec.rb
@@ -6,34 +6,34 @@ describe EventsHelper do
allow(helper).to receive(:current_user).and_return(double)
end
- it 'should display one line of plain text without alteration' do
+ it 'displays one line of plain text without alteration' do
input = 'A short, plain note'
expect(helper.event_note(input)).to match(input)
expect(helper.event_note(input)).not_to match(/\.\.\.\z/)
end
- it 'should display inline code' do
+ it 'displays inline code' do
input = 'A note with `inline code`'
expected = 'A note with <code>inline code</code>'
expect(helper.event_note(input)).to match(expected)
end
- it 'should truncate a note with multiple paragraphs' do
+ it 'truncates a note with multiple paragraphs' do
input = "Paragraph 1\n\nParagraph 2"
expected = 'Paragraph 1...'
expect(helper.event_note(input)).to match(expected)
end
- it 'should display the first line of a code block' do
+ it 'displays the first line of a code block' do
input = "```\nCode block\nwith two lines\n```"
expected = %r{<pre.+><code>Code block\.\.\.</code></pre>}
expect(helper.event_note(input)).to match(expected)
end
- it 'should truncate a single long line of text' do
+ it 'truncates a single long line of text' do
text = 'The quick brown fox jumped over the lazy dog twice' # 50 chars
input = text * 4
expected = (text * 2).sub(/.{3}/, '...')
@@ -41,7 +41,7 @@ describe EventsHelper do
expect(helper.event_note(input)).to match(expected)
end
- it 'should preserve a link href when link text is truncated' do
+ it 'preserves a link href when link text is truncated' do
text = 'The quick brown fox jumped over the lazy dog' # 44 chars
input = "#{text}#{text}#{text} " # 133 chars
link_url = 'http://example.com/foo/bar/baz' # 30 chars
@@ -52,12 +52,12 @@ describe EventsHelper do
expect(helper.event_note(input)).to match(expected_link_text)
end
- it 'should preserve code color scheme' do
+ it 'preserves code color scheme' do
input = "```ruby\ndef test\n 'hello world'\nend\n```"
expected = '<pre class="code highlight js-syntax-highlight ruby">' \
"<code><span class=\"k\">def</span> <span class=\"nf\">test</span>\n" \
" <span class=\"s1\">\'hello world\'</span>\n" \
- "<span class=\"k\">end</span>" \
+ "<span class=\"k\">end</span>\n" \
'</code></pre>'
expect(helper.event_note(input)).to eq(expected)
end
diff --git a/spec/helpers/gitlab_markdown_helper_spec.rb b/spec/helpers/gitlab_markdown_helper_spec.rb
index ade5c3b02d9..5368e5fab06 100644
--- a/spec/helpers/gitlab_markdown_helper_spec.rb
+++ b/spec/helpers/gitlab_markdown_helper_spec.rb
@@ -26,17 +26,17 @@ describe GitlabMarkdownHelper do
describe "referencing multiple objects" do
let(:actual) { "#{merge_request.to_reference} -> #{commit.to_reference} -> #{issue.to_reference}" }
- it "should link to the merge request" do
+ it "links to the merge request" do
expected = namespace_project_merge_request_path(project.namespace, project, merge_request)
expect(helper.markdown(actual)).to match(expected)
end
- it "should link to the commit" do
+ it "links to the commit" do
expected = namespace_project_commit_path(project.namespace, project, commit)
expect(helper.markdown(actual)).to match(expected)
end
- it "should link to the issue" do
+ it "links to the issue" do
expected = namespace_project_issue_path(project.namespace, project, issue)
expect(helper.markdown(actual)).to match(expected)
end
@@ -47,7 +47,7 @@ describe GitlabMarkdownHelper do
let(:second_project) { create(:project, :public) }
let(:second_issue) { create(:issue, project: second_project) }
- it 'should link to the issue' do
+ it 'links to the issue' do
expected = namespace_project_issue_path(second_project.namespace, second_project, second_issue)
expect(markdown(actual, project: second_project)).to match(expected)
end
@@ -58,7 +58,7 @@ describe GitlabMarkdownHelper do
let(:commit_path) { namespace_project_commit_path(project.namespace, project, commit) }
let(:issues) { create_list(:issue, 2, project: project) }
- it 'should handle references nested in links with all the text' do
+ it 'handles references nested in links with all the text' do
actual = helper.link_to_gfm("This should finally fix #{issues[0].to_reference} and #{issues[1].to_reference} for real", commit_path)
doc = Nokogiri::HTML.parse(actual)
@@ -88,7 +88,7 @@ describe GitlabMarkdownHelper do
expect(doc.css('a')[4].text).to eq ' for real'
end
- it 'should forward HTML options' do
+ it 'forwards HTML options' do
actual = helper.link_to_gfm("Fixed in #{commit.id}", commit_path, class: 'foo')
doc = Nokogiri::HTML.parse(actual)
@@ -110,7 +110,7 @@ describe GitlabMarkdownHelper do
expect(act).to eq %Q(<a href="/foo">#{issues[0].to_reference}</a>)
end
- it 'should replace commit message with emoji to link' do
+ it 'replaces commit message with emoji to link' do
actual = link_to_gfm(':book:Book', '/foo')
expect(actual).
to eq %Q(<img class="emoji" title=":book:" alt=":book:" src="http://localhost/assets/1F4D6.png" height="20" width="20" align="absmiddle"><a href="/foo">Book</a>)
@@ -125,7 +125,7 @@ describe GitlabMarkdownHelper do
helper.instance_variable_set(:@project_wiki, @wiki)
end
- it "should use Wiki pipeline for markdown files" do
+ it "uses 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, page_slug: "nested/page")
@@ -133,7 +133,7 @@ describe GitlabMarkdownHelper do
helper.render_wiki_content(@wiki)
end
- it "should use Asciidoctor for asciidoc files" do
+ it "uses Asciidoctor for asciidoc files" do
allow(@wiki).to receive(:format).and_return(:asciidoc)
expect(helper).to receive(:asciidoc).with('wiki content')
@@ -141,7 +141,7 @@ describe GitlabMarkdownHelper do
helper.render_wiki_content(@wiki)
end
- it "should use the Gollum renderer for all other file types" do
+ it "uses the Gollum renderer for all other file types" do
allow(@wiki).to receive(:format).and_return(:rdoc)
formatted_content_stub = double('formatted_content')
expect(formatted_content_stub).to receive(:html_safe)
diff --git a/spec/helpers/graph_helper_spec.rb b/spec/helpers/graph_helper_spec.rb
index 4acf38771b7..51c49f0e587 100644
--- a/spec/helpers/graph_helper_spec.rb
+++ b/spec/helpers/graph_helper_spec.rb
@@ -6,7 +6,7 @@ describe GraphHelper do
let(:commit) { project.commit("master") }
let(:graph) { Network::Graph.new(project, 'master', commit, '') }
- it 'filter our refs used by GitLab' do
+ it 'filters our refs used by GitLab' do
allow(commit).to receive(:ref_names).and_return(['refs/merge-requests/abc', 'master', 'refs/tmp/xyz'])
self.instance_variable_set(:@graph, graph)
refs = get_refs(project.repository, commit)
diff --git a/spec/helpers/groups_helper_spec.rb b/spec/helpers/groups_helper_spec.rb
index 4ea90a80a92..0807534720a 100644
--- a/spec/helpers/groups_helper_spec.rb
+++ b/spec/helpers/groups_helper_spec.rb
@@ -4,7 +4,7 @@ describe GroupsHelper do
describe 'group_icon' do
avatar_file_path = File.join(Rails.root, 'spec', 'fixtures', 'banana_sample.gif')
- it 'should return an url for the avatar' do
+ it 'returns an url for the avatar' do
group = create(:group)
group.avatar = File.open(avatar_file_path)
group.save!
@@ -12,7 +12,7 @@ describe GroupsHelper do
to match("/uploads/group/avatar/#{group.id}/banana_sample.gif")
end
- it 'should give default avatar_icon when no avatar is present' do
+ it 'gives default avatar_icon when no avatar is present' do
group = create(:group)
group.save!
expect(group_icon(group.path)).to match('group_avatar.png')
diff --git a/spec/helpers/issuables_helper_spec.rb b/spec/helpers/issuables_helper_spec.rb
new file mode 100644
index 00000000000..2dd2eab0524
--- /dev/null
+++ b/spec/helpers/issuables_helper_spec.rb
@@ -0,0 +1,16 @@
+require 'spec_helper'
+
+describe IssuablesHelper do
+ let(:label) { build_stubbed(:label) }
+ let(:label2) { build_stubbed(:label) }
+
+ context 'label tooltip' do
+ it 'returns label text' do
+ expect(issuable_labels_tooltip([label])).to eq(label.title)
+ end
+
+ it 'returns label text' do
+ expect(issuable_labels_tooltip([label, label2], limit: 1)).to eq("#{label.title}, and 1 more")
+ end
+ end
+end
diff --git a/spec/helpers/issues_helper_spec.rb b/spec/helpers/issues_helper_spec.rb
index 831ae7fb69c..67bac782591 100644
--- a/spec/helpers/issues_helper_spec.rb
+++ b/spec/helpers/issues_helper_spec.rb
@@ -5,69 +5,24 @@ describe IssuesHelper do
let(:issue) { create :issue, project: project }
let(:ext_project) { create :redmine_project }
- describe "url_for_project_issues" do
- let(:project_url) { ext_project.external_issue_tracker.project_url }
- 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
- @project = project
- expect(url_for_project_issues).to match(int_expected)
- end
-
- it "should return path to external tracker" do
- @project = ext_project
-
- expect(url_for_project_issues).to match(ext_expected)
- end
-
- it "should return empty string if project nil" do
- @project = nil
-
- expect(url_for_project_issues).to eq ""
- end
-
- it 'returns an empty string if project_url is invalid' do
- expect(project).to receive_message_chain('issues_tracker.project_url') { 'javascript:alert("foo");' }
-
- expect(url_for_project_issues(project)).to eq ''
- end
-
- it 'returns an empty string if project_path is invalid' do
- expect(project).to receive_message_chain('issues_tracker.project_path') { 'javascript:alert("foo");' }
-
- expect(url_for_project_issues(project, only_path: true)).to eq ''
- end
-
- describe "when external tracker was enabled and then config removed" do
- before do
- @project = ext_project
- allow(Gitlab.config).to receive(:issues_tracker).and_return(nil)
- end
-
- it "should return path to external tracker" do
- expect(url_for_project_issues).to match(ext_expected)
- end
- end
- end
-
describe "url_for_issue" do
let(:issues_url) { ext_project.external_issue_tracker.issues_url}
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
+ it "returns internal path if used internal tracker" do
@project = project
+
expect(url_for_issue(issue.iid)).to match(int_expected)
end
- it "should return path to external tracker" do
+ it "returns path to external tracker" do
@project = ext_project
expect(url_for_issue(issue.iid)).to match(ext_expected)
end
- it "should return empty string if project nil" do
+ it "returns empty string if project nil" do
@project = nil
expect(url_for_issue(issue.iid)).to eq ""
@@ -91,66 +46,46 @@ describe IssuesHelper do
allow(Gitlab.config).to receive(:issues_tracker).and_return(nil)
end
- it "should return external path" do
+ it "returns external path" do
expect(url_for_issue(issue.iid)).to match(ext_expected)
end
end
end
- describe 'url_for_new_issue' do
- let(:issues_url) { ext_project.external_issue_tracker.new_issue_url }
- 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
- @project = project
- expect(url_for_new_issue).to match(int_expected)
+ describe "merge_requests_sentence" do
+ subject { merge_requests_sentence(merge_requests)}
+ let(:merge_requests) do
+ [ build(:merge_request, iid: 1), build(:merge_request, iid: 2),
+ build(:merge_request, iid: 3)]
end
- it "should return path to external tracker" do
- @project = ext_project
-
- expect(url_for_new_issue).to match(ext_expected)
- end
+ it { is_expected.to eq("!1, !2, or !3") }
+ end
- it "should return empty string if project nil" do
- @project = nil
+ describe '#award_user_list' do
+ let!(:awards) { build_list(:award_emoji, 15) }
- expect(url_for_new_issue).to eq ""
+ it "returns a comma seperated list of 1-9 users" do
+ expect(award_user_list(awards.first(9), nil)).to eq(awards.first(9).map { |a| a.user.name }.to_sentence)
end
- it 'returns an empty string if issue_url is invalid' do
- expect(project).to receive_message_chain('issues_tracker.new_issue_url') { 'javascript:alert("foo");' }
-
- expect(url_for_new_issue(project)).to eq ''
+ it "displays the current user's name as 'You'" do
+ expect(award_user_list(awards.first(1), awards[0].user)).to eq('You')
end
- it 'returns an empty string if issue_path is invalid' do
- expect(project).to receive_message_chain('issues_tracker.new_issue_path') { 'javascript:alert("foo");' }
-
- expect(url_for_new_issue(project, only_path: true)).to eq ''
+ it "truncates lists of larger than 9 users" do
+ expect(award_user_list(awards, nil)).to eq(awards.first(9).map { |a| a.user.name }.join(', ') + ", and 6 more.")
end
- describe "when external tracker was enabled and then config removed" do
- before do
- @project = ext_project
- allow(Gitlab.config).to receive(:issues_tracker).and_return(nil)
- end
-
- it "should return internal path" do
- expect(url_for_new_issue).to match(ext_expected)
- end
+ it "displays the current user in front of 0-9 other users" do
+ expect(award_user_list(awards, awards[0].user)).
+ to eq("You, " + awards[1..9].map { |a| a.user.name }.join(', ') + ", and 5 more.")
end
- end
- describe "merge_requests_sentence" do
- subject { merge_requests_sentence(merge_requests)}
- let(:merge_requests) do
- [ build(:merge_request, iid: 1), build(:merge_request, iid: 2),
- build(:merge_request, iid: 3)]
+ it "displays the current user in front regardless of position in the list" do
+ expect(award_user_list(awards, awards[12].user)).
+ to eq("You, " + awards[0..8].map { |a| a.user.name }.join(', ') + ", and 5 more.")
end
-
- it { is_expected.to eq("!1, !2, or !3") }
end
describe '#award_active_class' do
diff --git a/spec/helpers/members_helper_spec.rb b/spec/helpers/members_helper_spec.rb
index f75fdb739f6..7998209b7b0 100644
--- a/spec/helpers/members_helper_spec.rb
+++ b/spec/helpers/members_helper_spec.rb
@@ -9,54 +9,6 @@ describe MembersHelper do
it { expect(action_member_permission(:admin, group_member)).to eq :admin_group_member }
end
- describe '#default_show_roles' do
- let(:user) { double }
- let(:member) { build(:project_member) }
-
- before do
- allow(helper).to receive(:current_user).and_return(user)
- allow(helper).to receive(:can?).with(user, :update_project_member, member).and_return(false)
- allow(helper).to receive(:can?).with(user, :destroy_project_member, member).and_return(false)
- allow(helper).to receive(:can?).with(user, :admin_project_member, member.source).and_return(false)
- end
-
- context 'when the current cannot update, destroy or admin the passed member' do
- it 'returns false' do
- expect(helper.default_show_roles(member)).to be_falsy
- end
- end
-
- context 'when the current can update the passed member' do
- before do
- allow(helper).to receive(:can?).with(user, :update_project_member, member).and_return(true)
- end
-
- it 'returns true' do
- expect(helper.default_show_roles(member)).to be_truthy
- end
- end
-
- context 'when the current can destroy the passed member' do
- before do
- allow(helper).to receive(:can?).with(user, :destroy_project_member, member).and_return(true)
- end
-
- it 'returns true' do
- expect(helper.default_show_roles(member)).to be_truthy
- end
- end
-
- context 'when the current can admin the passed member source' do
- before do
- allow(helper).to receive(:can?).with(user, :admin_project_member, member.source).and_return(true)
- end
-
- it 'returns true' do
- expect(helper.default_show_roles(member)).to be_truthy
- end
- end
- end
-
describe '#remove_member_message' do
let(:requester) { build(:user) }
let(:project) { create(:project) }
diff --git a/spec/helpers/notes_helper_spec.rb b/spec/helpers/notes_helper_spec.rb
index 08a93503258..9c577501f00 100644
--- a/spec/helpers/notes_helper_spec.rb
+++ b/spec/helpers/notes_helper_spec.rb
@@ -1,37 +1,30 @@
require "spec_helper"
describe NotesHelper do
- describe "#notes_max_access_for_users" do
- let(:owner) { create(:owner) }
- let(:group) { create(:group) }
- let(:project) { create(:empty_project, namespace: group) }
- let(:master) { create(:user) }
- let(:reporter) { create(:user) }
- let(:guest) { create(:user) }
-
- let(:owner_note) { create(:note, author: owner, project: project) }
- let(:master_note) { create(:note, author: master, project: project) }
- let(:reporter_note) { create(:note, author: reporter, project: project) }
- let!(:notes) { [owner_note, master_note, reporter_note] }
+ let(:owner) { create(:owner) }
+ let(:group) { create(:group) }
+ let(:project) { create(:empty_project, namespace: group) }
+ let(:master) { create(:user) }
+ let(:reporter) { create(:user) }
+ let(:guest) { create(:user) }
- before do
- group.add_owner(owner)
- project.team << [master, :master]
- project.team << [reporter, :reporter]
- project.team << [guest, :guest]
- end
+ let(:owner_note) { create(:note, author: owner, project: project) }
+ let(:master_note) { create(:note, author: master, project: project) }
+ let(:reporter_note) { create(:note, author: reporter, project: project) }
+ let!(:notes) { [owner_note, master_note, reporter_note] }
- it 'return human access levels' do
- original_method = project.team.method(:human_max_access)
- expect_any_instance_of(ProjectTeam).to receive(:human_max_access).exactly(3).times do |*args|
- original_method.call(args[1])
- end
+ before do
+ group.add_owner(owner)
+ project.team << [master, :master]
+ project.team << [reporter, :reporter]
+ project.team << [guest, :guest]
+ end
+ describe "#notes_max_access_for_users" do
+ it 'returns human access levels' do
expect(helper.note_max_access_for_user(owner_note)).to eq('Owner')
expect(helper.note_max_access_for_user(master_note)).to eq('Master')
expect(helper.note_max_access_for_user(reporter_note)).to eq('Reporter')
- # Call it again to ensure value is cached
- expect(helper.note_max_access_for_user(owner_note)).to eq('Owner')
end
it 'handles access in different projects' do
@@ -43,4 +36,21 @@ describe NotesHelper do
expect(helper.note_max_access_for_user(other_note)).to eq('Reporter')
end
end
+
+ describe '#preload_max_access_for_authors' do
+ before do
+ # This method reads cache from RequestStore, so make sure it's clean.
+ RequestStore.clear!
+ end
+
+ it 'loads multiple users' do
+ expected_access = {
+ owner.id => Gitlab::Access::OWNER,
+ master.id => Gitlab::Access::MASTER,
+ reporter.id => Gitlab::Access::REPORTER
+ }
+
+ expect(helper.preload_max_access_for_authors(notes, project)).to eq(expected_access)
+ end
+ end
end
diff --git a/spec/helpers/page_layout_helper_spec.rb b/spec/helpers/page_layout_helper_spec.rb
index cf632f594c7..dc07657e101 100644
--- a/spec/helpers/page_layout_helper_spec.rb
+++ b/spec/helpers/page_layout_helper_spec.rb
@@ -97,5 +97,14 @@ describe PageLayoutHelper do
expect(tags).to include %q(<meta property="twitter:data1" content="bar" />)
end
end
+
+ it 'escapes content' do
+ allow(helper).to receive(:page_card_attributes)
+ .and_return(foo: %q{foo" http-equiv="refresh}.html_safe)
+
+ tags = helper.page_card_meta_tags
+
+ expect(tags).to include(%q{content="foo&quot; http-equiv=&quot;refresh"})
+ end
end
end
diff --git a/spec/helpers/projects_helper_spec.rb b/spec/helpers/projects_helper_spec.rb
index 604204cca0a..284b58d8d5c 100644
--- a/spec/helpers/projects_helper_spec.rb
+++ b/spec/helpers/projects_helper_spec.rb
@@ -136,4 +136,42 @@ describe ProjectsHelper do
expect(sanitize_repo_path(project, import_error)).to eq('Could not clone [REPOS PATH]/namespace/test.git')
end
end
+
+ describe '#last_push_event' do
+ let(:user) { double(:user, fork_of: nil) }
+ let(:project) { double(:project, id: 1) }
+
+ before do
+ allow(helper).to receive(:current_user).and_return(user)
+ helper.instance_variable_set(:@project, project)
+ end
+
+ context 'when there is no current_user' do
+ let(:user) { nil }
+
+ it 'returns nil' do
+ expect(helper.last_push_event).to eq(nil)
+ end
+ end
+
+ it 'returns recent push on the current project' do
+ event = double(:event)
+ expect(user).to receive(:recent_push).with([project.id]).and_return(event)
+
+ expect(helper.last_push_event).to eq(event)
+ end
+
+ context 'when current user has a fork of the current project' do
+ let(:fork) { double(:fork, id: 2) }
+
+ it 'returns recent push considering fork events' do
+ expect(user).to receive(:fork_of).with(project).and_return(fork)
+
+ event_on_fork = double(:event)
+ expect(user).to receive(:recent_push).with([project.id, fork.id]).and_return(event_on_fork)
+
+ expect(helper.last_push_event).to eq(event_on_fork)
+ end
+ end
+ end
end
diff --git a/spec/helpers/search_helper_spec.rb b/spec/helpers/search_helper_spec.rb
index 601b6915e27..b0bb991539b 100644
--- a/spec/helpers/search_helper_spec.rb
+++ b/spec/helpers/search_helper_spec.rb
@@ -42,7 +42,7 @@ describe SearchHelper do
expect(search_autocomplete_opts(project.name).size).to eq(1)
end
- it "should not include the public group" do
+ it "does not include the public group" do
group = create(:group)
expect(search_autocomplete_opts(group.name).size).to eq(0)
end
diff --git a/spec/helpers/submodule_helper_spec.rb b/spec/helpers/submodule_helper_spec.rb
index 10121759132..37ac6a2699d 100644
--- a/spec/helpers/submodule_helper_spec.rb
+++ b/spec/helpers/submodule_helper_spec.rb
@@ -17,35 +17,35 @@ describe SubmoduleHelper do
allow(Gitlab.config.gitlab).to receive(:protocol).and_return('http') # set this just to be sure
end
- it 'should detect ssh on standard port' do
+ it 'detects ssh on standard port' do
allow(Gitlab.config.gitlab_shell).to receive(:ssh_port).and_return(22) # set this just to be sure
allow(Gitlab.config.gitlab_shell).to receive(:ssh_path_prefix).and_return(Settings.send(:build_gitlab_shell_ssh_path_prefix))
stub_url([ config.user, '@', config.host, ':gitlab-org/gitlab-ce.git' ].join(''))
expect(submodule_links(submodule_item)).to eq([ namespace_project_path('gitlab-org', 'gitlab-ce'), namespace_project_tree_path('gitlab-org', 'gitlab-ce', 'hash') ])
end
- it 'should detect ssh on non-standard port' do
+ it 'detects ssh on non-standard port' do
allow(Gitlab.config.gitlab_shell).to receive(:ssh_port).and_return(2222)
allow(Gitlab.config.gitlab_shell).to receive(:ssh_path_prefix).and_return(Settings.send(:build_gitlab_shell_ssh_path_prefix))
stub_url([ 'ssh://', config.user, '@', config.host, ':2222/gitlab-org/gitlab-ce.git' ].join(''))
expect(submodule_links(submodule_item)).to eq([ namespace_project_path('gitlab-org', 'gitlab-ce'), namespace_project_tree_path('gitlab-org', 'gitlab-ce', 'hash') ])
end
- it 'should detect http on standard port' do
+ it 'detects http on standard port' do
allow(Gitlab.config.gitlab).to receive(:port).and_return(80)
allow(Gitlab.config.gitlab).to receive(:url).and_return(Settings.send(:build_gitlab_url))
stub_url([ 'http://', config.host, '/gitlab-org/gitlab-ce.git' ].join(''))
expect(submodule_links(submodule_item)).to eq([ namespace_project_path('gitlab-org', 'gitlab-ce'), namespace_project_tree_path('gitlab-org', 'gitlab-ce', 'hash') ])
end
- it 'should detect http on non-standard port' do
+ it 'detects http on non-standard port' do
allow(Gitlab.config.gitlab).to receive(:port).and_return(3000)
allow(Gitlab.config.gitlab).to receive(:url).and_return(Settings.send(:build_gitlab_url))
stub_url([ 'http://', config.host, ':3000/gitlab-org/gitlab-ce.git' ].join(''))
expect(submodule_links(submodule_item)).to eq([ namespace_project_path('gitlab-org', 'gitlab-ce'), namespace_project_tree_path('gitlab-org', 'gitlab-ce', 'hash') ])
end
- it 'should work with relative_url_root' do
+ it 'works with relative_url_root' do
allow(Gitlab.config.gitlab).to receive(:port).and_return(80) # set this just to be sure
allow(Gitlab.config.gitlab).to receive(:relative_url_root).and_return('/gitlab/root')
allow(Gitlab.config.gitlab).to receive(:url).and_return(Settings.send(:build_gitlab_url))
@@ -55,22 +55,22 @@ describe SubmoduleHelper do
end
context 'submodule on github.com' do
- it 'should detect ssh' do
+ it 'detects ssh' do
stub_url('git@github.com:gitlab-org/gitlab-ce.git')
expect(submodule_links(submodule_item)).to eq([ 'https://github.com/gitlab-org/gitlab-ce', 'https://github.com/gitlab-org/gitlab-ce/tree/hash' ])
end
- it 'should detect http' do
+ it 'detects http' do
stub_url('http://github.com/gitlab-org/gitlab-ce.git')
expect(submodule_links(submodule_item)).to eq([ 'https://github.com/gitlab-org/gitlab-ce', 'https://github.com/gitlab-org/gitlab-ce/tree/hash' ])
end
- it 'should detect https' do
+ it 'detects https' do
stub_url('https://github.com/gitlab-org/gitlab-ce.git')
expect(submodule_links(submodule_item)).to eq([ 'https://github.com/gitlab-org/gitlab-ce', 'https://github.com/gitlab-org/gitlab-ce/tree/hash' ])
end
- it 'should return original with non-standard url' do
+ it 'returns original with non-standard url' do
stub_url('http://github.com/gitlab-org/gitlab-ce')
expect(submodule_links(submodule_item)).to eq([ repo.submodule_url_for, nil ])
@@ -80,22 +80,22 @@ describe SubmoduleHelper do
end
context 'submodule on gitlab.com' do
- it 'should detect ssh' do
+ it 'detects ssh' do
stub_url('git@gitlab.com:gitlab-org/gitlab-ce.git')
expect(submodule_links(submodule_item)).to eq([ 'https://gitlab.com/gitlab-org/gitlab-ce', 'https://gitlab.com/gitlab-org/gitlab-ce/tree/hash' ])
end
- it 'should detect http' do
+ it 'detects http' do
stub_url('http://gitlab.com/gitlab-org/gitlab-ce.git')
expect(submodule_links(submodule_item)).to eq([ 'https://gitlab.com/gitlab-org/gitlab-ce', 'https://gitlab.com/gitlab-org/gitlab-ce/tree/hash' ])
end
- it 'should detect https' do
+ it 'detects https' do
stub_url('https://gitlab.com/gitlab-org/gitlab-ce.git')
expect(submodule_links(submodule_item)).to eq([ 'https://gitlab.com/gitlab-org/gitlab-ce', 'https://gitlab.com/gitlab-org/gitlab-ce/tree/hash' ])
end
- it 'should return original with non-standard url' do
+ it 'returns original with non-standard url' do
stub_url('http://gitlab.com/gitlab-org/gitlab-ce')
expect(submodule_links(submodule_item)).to eq([ repo.submodule_url_for, nil ])
@@ -105,7 +105,7 @@ describe SubmoduleHelper do
end
context 'submodule on unsupported' do
- it 'should return original' do
+ it 'returns original' do
stub_url('http://mygitserver.com/gitlab-org/gitlab-ce')
expect(submodule_links(submodule_item)).to eq([ repo.submodule_url_for, nil ])
diff --git a/spec/helpers/time_helper_spec.rb b/spec/helpers/time_helper_spec.rb
index 3f62527c5bb..21f35585367 100644
--- a/spec/helpers/time_helper_spec.rb
+++ b/spec/helpers/time_helper_spec.rb
@@ -1,36 +1,34 @@
require 'spec_helper'
describe TimeHelper do
- describe "#duration_in_words" do
+ describe "#time_interval_in_words" do
it "returns minutes and seconds" do
intervals_in_words = {
100 => "1 minute 40 seconds",
+ 100.32 => "1 minute 40 seconds",
121 => "2 minutes 1 second",
3721 => "62 minutes 1 second",
0 => "0 seconds"
}
intervals_in_words.each do |interval, expectation|
- expect(duration_in_words(Time.now + interval, Time.now)).to eq(expectation)
+ expect(time_interval_in_words(interval)).to eq(expectation)
end
end
-
- it "calculates interval from now if there is no finished_at" do
- expect(duration_in_words(nil, Time.now - 5)).to eq("5 seconds")
- end
end
- describe "#time_interval_in_words" do
+ describe "#duration_in_numbers" do
it "returns minutes and seconds" do
- intervals_in_words = {
- 100 => "1 minute 40 seconds",
- 121 => "2 minutes 1 second",
- 3721 => "62 minutes 1 second",
- 0 => "0 seconds"
+ durations_and_expectations = {
+ 100 => "01:40",
+ 121 => "02:01",
+ 3721 => "01:02:01",
+ 0 => "00:00",
+ 42 => "00:42"
}
- intervals_in_words.each do |interval, expectation|
- expect(time_interval_in_words(interval)).to eq(expectation)
+ durations_and_expectations.each do |duration, expectation|
+ expect(duration_in_numbers(duration)).to eq(expectation)
end
end
end
diff --git a/spec/helpers/tree_helper_spec.rb b/spec/helpers/tree_helper_spec.rb
index c70dd8076e0..8d6537ba4b5 100644
--- a/spec/helpers/tree_helper_spec.rb
+++ b/spec/helpers/tree_helper_spec.rb
@@ -12,7 +12,7 @@ describe TreeHelper do
context "on a directory containing more than one file/directory" do
let(:tree_item) { double(name: "files", path: "files") }
- it "should return the directory name" do
+ it "returns the directory name" do
expect(flatten_tree(tree_item)).to match('files')
end
end
@@ -20,7 +20,7 @@ describe TreeHelper do
context "on a directory containing only one directory" do
let(:tree_item) { double(name: "foo", path: "foo") }
- it "should return the flattened path" do
+ it "returns the flattened path" do
expect(flatten_tree(tree_item)).to match('foo/bar')
end
end
diff --git a/spec/initializers/6_validations_spec.rb b/spec/initializers/6_validations_spec.rb
index 5178bd130f4..baab30f482f 100644
--- a/spec/initializers/6_validations_spec.rb
+++ b/spec/initializers/6_validations_spec.rb
@@ -1,41 +1,58 @@
require 'spec_helper'
+require_relative '../../config/initializers/6_validations.rb'
describe '6_validations', lib: true do
+ before :all do
+ FileUtils.mkdir_p('tmp/tests/paths/a/b/c/d')
+ FileUtils.mkdir_p('tmp/tests/paths/a/b/c2')
+ FileUtils.mkdir_p('tmp/tests/paths/a/b/d')
+ end
+
+ after :all do
+ FileUtils.rm_rf('tmp/tests/paths')
+ end
+
context 'with correct settings' do
before do
- mock_storages('foo' => '/a/b/c', 'bar' => 'a/b/d')
+ mock_storages('foo' => 'tmp/tests/paths/a/b/c', 'bar' => 'tmp/tests/paths/a/b/d')
end
it 'passes through' do
- expect { load_validations }.not_to raise_error
+ expect { validate_storages }.not_to raise_error
end
end
context 'with invalid storage names' do
before do
- mock_storages('name with spaces' => '/a/b/c')
+ mock_storages('name with spaces' => 'tmp/tests/paths/a/b/c')
end
it 'throws an error' do
- expect { load_validations }.to raise_error('"name with spaces" is not a valid storage name. Please fix this in your gitlab.yml before starting GitLab.')
+ expect { validate_storages }.to raise_error('"name with spaces" is not a valid storage name. Please fix this in your gitlab.yml before starting GitLab.')
end
end
context 'with nested storage paths' do
before do
- mock_storages('foo' => '/a/b/c', 'bar' => '/a/b/c/d')
+ mock_storages('foo' => 'tmp/tests/paths/a/b/c', 'bar' => 'tmp/tests/paths/a/b/c/d')
end
it 'throws an error' do
- expect { load_validations }.to raise_error('bar is a nested path of foo. Nested paths are not supported for repository storages. Please fix this in your gitlab.yml before starting GitLab.')
+ expect { validate_storages }.to raise_error('bar is a nested path of foo. Nested paths are not supported for repository storages. Please fix this in your gitlab.yml before starting GitLab.')
end
end
- def mock_storages(storages)
- allow(Gitlab.config.repositories).to receive(:storages).and_return(storages)
+ context 'with similar but un-nested storage paths' do
+ before do
+ mock_storages('foo' => 'tmp/tests/paths/a/b/c', 'bar' => 'tmp/tests/paths/a/b/c2')
+ end
+
+ it 'passes through' do
+ expect { validate_storages }.not_to raise_error
+ end
end
- def load_validations
- load File.join(__dir__, '../../config/initializers/6_validations.rb')
+ def mock_storages(storages)
+ allow(Gitlab.config.repositories).to receive(:storages).and_return(storages)
end
end
diff --git a/spec/initializers/secret_token_spec.rb b/spec/initializers/secret_token_spec.rb
new file mode 100644
index 00000000000..837b0de9a4c
--- /dev/null
+++ b/spec/initializers/secret_token_spec.rb
@@ -0,0 +1,200 @@
+require 'spec_helper'
+require_relative '../../config/initializers/secret_token'
+
+describe 'create_tokens', lib: true do
+ let(:secrets) { ActiveSupport::OrderedOptions.new }
+
+ before do
+ allow(ENV).to receive(:[]).and_call_original
+ allow(File).to receive(:write)
+ allow(File).to receive(:delete)
+ allow(Rails).to receive_message_chain(:application, :secrets).and_return(secrets)
+ allow(Rails).to receive_message_chain(:root, :join) { |string| string }
+ allow(self).to receive(:warn)
+ allow(self).to receive(:exit)
+ end
+
+ context 'setting secret_key_base and otp_key_base' do
+ context 'when none of the secrets exist' do
+ before do
+ allow(ENV).to receive(:[]).with('SECRET_KEY_BASE').and_return(nil)
+ allow(File).to receive(:exist?).with('.secret').and_return(false)
+ allow(File).to receive(:exist?).with('config/secrets.yml').and_return(false)
+ allow(self).to receive(:warn_missing_secret)
+ end
+
+ it 'generates different secrets for secret_key_base, otp_key_base, and db_key_base' do
+ create_tokens
+
+ keys = secrets.values_at(:secret_key_base, :otp_key_base, :db_key_base)
+
+ expect(keys.uniq).to eq(keys)
+ expect(keys.map(&:length)).to all(eq(128))
+ end
+
+ it 'warns about the secrets to add to secrets.yml' do
+ expect(self).to receive(:warn_missing_secret).with('secret_key_base')
+ expect(self).to receive(:warn_missing_secret).with('otp_key_base')
+ expect(self).to receive(:warn_missing_secret).with('db_key_base')
+
+ create_tokens
+ end
+
+ it 'writes the secrets to secrets.yml' do
+ expect(File).to receive(:write).with('config/secrets.yml', any_args) do |filename, contents, options|
+ new_secrets = YAML.load(contents)[Rails.env]
+
+ expect(new_secrets['secret_key_base']).to eq(secrets.secret_key_base)
+ expect(new_secrets['otp_key_base']).to eq(secrets.otp_key_base)
+ expect(new_secrets['db_key_base']).to eq(secrets.db_key_base)
+ end
+
+ create_tokens
+ end
+
+ it 'does not write a .secret file' do
+ expect(File).not_to receive(:write).with('.secret')
+
+ create_tokens
+ end
+ end
+
+ context 'when the other secrets all exist' do
+ before do
+ secrets.db_key_base = 'db_key_base'
+
+ allow(File).to receive(:exist?).with('.secret').and_return(true)
+ allow(File).to receive(:read).with('.secret').and_return('file_key')
+ end
+
+ context 'when secret_key_base exists in the environment and secrets.yml' do
+ before do
+ allow(ENV).to receive(:[]).with('SECRET_KEY_BASE').and_return('env_key')
+ secrets.secret_key_base = 'secret_key_base'
+ secrets.otp_key_base = 'otp_key_base'
+ end
+
+ it 'does not issue a warning' do
+ expect(self).not_to receive(:warn)
+
+ create_tokens
+ end
+
+ it 'uses the environment variable' do
+ create_tokens
+
+ expect(secrets.secret_key_base).to eq('env_key')
+ end
+
+ it 'does not update secrets.yml' do
+ expect(File).not_to receive(:write)
+
+ create_tokens
+ end
+ end
+
+ context 'when secret_key_base and otp_key_base exist' do
+ before do
+ secrets.secret_key_base = 'secret_key_base'
+ secrets.otp_key_base = 'otp_key_base'
+ end
+
+ it 'does not write any files' do
+ expect(File).not_to receive(:write)
+
+ create_tokens
+ end
+
+ it 'sets the the keys to the values from the environment and secrets.yml' do
+ create_tokens
+
+ expect(secrets.secret_key_base).to eq('secret_key_base')
+ expect(secrets.otp_key_base).to eq('otp_key_base')
+ expect(secrets.db_key_base).to eq('db_key_base')
+ end
+
+ it 'deletes the .secret file' do
+ expect(File).to receive(:delete).with('.secret')
+
+ create_tokens
+ end
+ end
+
+ context 'when secret_key_base and otp_key_base do not exist' do
+ before do
+ allow(File).to receive(:exist?).with('config/secrets.yml').and_return(true)
+ allow(YAML).to receive(:load_file).with('config/secrets.yml').and_return('test' => secrets.to_h.stringify_keys)
+ allow(self).to receive(:warn_missing_secret)
+ end
+
+ it 'uses the file secret' do
+ expect(File).to receive(:write) do |filename, contents, options|
+ new_secrets = YAML.load(contents)[Rails.env]
+
+ expect(new_secrets['secret_key_base']).to eq('file_key')
+ expect(new_secrets['otp_key_base']).to eq('file_key')
+ expect(new_secrets['db_key_base']).to eq('db_key_base')
+ end
+
+ create_tokens
+
+ expect(secrets.otp_key_base).to eq('file_key')
+ end
+
+ it 'keeps the other secrets as they were' do
+ create_tokens
+
+ expect(secrets.db_key_base).to eq('db_key_base')
+ end
+
+ it 'warns about the missing secrets' do
+ expect(self).to receive(:warn_missing_secret).with('secret_key_base')
+ expect(self).to receive(:warn_missing_secret).with('otp_key_base')
+
+ create_tokens
+ end
+
+ it 'deletes the .secret file' do
+ expect(File).to receive(:delete).with('.secret')
+
+ create_tokens
+ end
+ end
+ end
+
+ context 'when db_key_base is blank but exists in secrets.yml' do
+ before do
+ secrets.otp_key_base = 'otp_key_base'
+ secrets.secret_key_base = 'secret_key_base'
+ yaml_secrets = secrets.to_h.stringify_keys.merge('db_key_base' => '<%= an_erb_expression %>')
+
+ allow(File).to receive(:exist?).with('.secret').and_return(false)
+ allow(File).to receive(:exist?).with('config/secrets.yml').and_return(true)
+ allow(YAML).to receive(:load_file).with('config/secrets.yml').and_return('test' => yaml_secrets)
+ allow(self).to receive(:warn_missing_secret)
+ end
+
+ it 'warns about updating db_key_base' do
+ expect(self).to receive(:warn_missing_secret).with('db_key_base')
+
+ create_tokens
+ end
+
+ it 'warns about the blank value existing in secrets.yml and exits' do
+ expect(self).to receive(:warn) do |warning|
+ expect(warning).to include('db_key_base')
+ expect(warning).to include('<%= an_erb_expression %>')
+ end
+
+ create_tokens
+ end
+
+ it 'does not update secrets.yml' do
+ expect(self).to receive(:exit).with(1).and_call_original
+ expect(File).not_to receive(:write)
+
+ expect { create_tokens }.to raise_error(SystemExit)
+ end
+ end
+ end
+end
diff --git a/spec/initializers/trusted_proxies_spec.rb b/spec/initializers/trusted_proxies_spec.rb
index 14c8df954a6..290e47763eb 100644
--- a/spec/initializers/trusted_proxies_spec.rb
+++ b/spec/initializers/trusted_proxies_spec.rb
@@ -17,6 +17,12 @@ describe 'trusted_proxies', lib: true do
expect(request.remote_ip).to eq('10.1.5.89')
expect(request.ip).to eq('10.1.5.89')
end
+
+ it 'filters out bad values' do
+ request = stub_request('HTTP_X_FORWARDED_FOR' => '(null), 10.1.5.89')
+ expect(request.remote_ip).to eq('10.1.5.89')
+ expect(request.ip).to eq('10.1.5.89')
+ end
end
context 'with private IP ranges added' do
@@ -41,6 +47,12 @@ describe 'trusted_proxies', lib: true do
expect(request.remote_ip).to eq('1.1.1.1')
expect(request.ip).to eq('1.1.1.1')
end
+
+ it 'handles invalid ip addresses' do
+ request = stub_request('HTTP_X_FORWARDED_FOR' => '(null), 1.1.1.1:12345, 1.1.1.1')
+ expect(request.remote_ip).to eq('1.1.1.1')
+ expect(request.ip).to eq('1.1.1.1')
+ end
end
def stub_request(headers = {})
diff --git a/spec/javascripts/abuse_reports_spec.js.es6 b/spec/javascripts/abuse_reports_spec.js.es6
new file mode 100644
index 00000000000..6bcfdf191c2
--- /dev/null
+++ b/spec/javascripts/abuse_reports_spec.js.es6
@@ -0,0 +1,41 @@
+/*= require abuse_reports */
+
+/*= require jquery */
+
+((global) => {
+ const FIXTURE = 'abuse_reports.html';
+ const MAX_MESSAGE_LENGTH = 500;
+
+ function assertMaxLength($message) {
+ expect($message.text().length).toEqual(MAX_MESSAGE_LENGTH);
+ }
+
+ describe('Abuse Reports', function() {
+ fixture.preload(FIXTURE);
+
+ beforeEach(function() {
+ fixture.load(FIXTURE);
+ new global.AbuseReports();
+ });
+
+ it('should truncate long messages', function() {
+ const $longMessage = $('#long');
+ expect($longMessage.data('original-message')).toEqual(jasmine.anything());
+ assertMaxLength($longMessage);
+ });
+
+ it('should not truncate short messages', function() {
+ const $shortMessage = $('#short');
+ expect($shortMessage.data('original-message')).not.toEqual(jasmine.anything());
+ });
+
+ it('should allow clicking a truncated message to expand and collapse the full message', function() {
+ const $longMessage = $('#long');
+ $longMessage.click();
+ expect($longMessage.data('original-message').length).toEqual($longMessage.text().length);
+ $longMessage.click();
+ assertMaxLength($longMessage);
+ });
+ });
+
+})(window.gl);
diff --git a/spec/javascripts/application_spec.js b/spec/javascripts/application_spec.js
new file mode 100644
index 00000000000..b48026c3b77
--- /dev/null
+++ b/spec/javascripts/application_spec.js
@@ -0,0 +1,32 @@
+
+/*= require lib/utils/common_utils */
+
+(function() {
+ describe('Application', function() {
+ return describe('disable buttons', function() {
+ fixture.preload('application.html');
+ beforeEach(function() {
+ return fixture.load('application.html');
+ });
+ it('should prevent default action for disabled buttons', function() {
+ var $button, isClicked;
+ gl.utils.preventDisabledButtons();
+ isClicked = false;
+ $button = $('#test-button');
+ $button.click(function() {
+ return isClicked = true;
+ });
+ $button.trigger('click');
+ return expect(isClicked).toBe(false);
+ });
+ return it('should be on the same page if a disabled link clicked', function() {
+ var locationBeforeLinkClick;
+ locationBeforeLinkClick = window.location.href;
+ gl.utils.preventDisabledButtons();
+ $('#test-link').click();
+ return expect(window.location.href).toBe(locationBeforeLinkClick);
+ });
+ });
+ });
+
+}).call(this);
diff --git a/spec/javascripts/application_spec.js.coffee b/spec/javascripts/application_spec.js.coffee
deleted file mode 100644
index 4b6a2bb5440..00000000000
--- a/spec/javascripts/application_spec.js.coffee
+++ /dev/null
@@ -1,30 +0,0 @@
-#= require lib/utils/common_utils
-
-describe 'Application', ->
- describe 'disable buttons', ->
- fixture.preload('application.html')
-
- beforeEach ->
- fixture.load('application.html')
-
- it 'should prevent default action for disabled buttons', ->
-
- gl.utils.preventDisabledButtons()
-
- isClicked = false
- $button = $ '#test-button'
-
- $button.click -> isClicked = true
- $button.trigger 'click'
-
- expect(isClicked).toBe false
-
-
- it 'should be on the same page if a disabled link clicked', ->
-
- locationBeforeLinkClick = window.location.href
- gl.utils.preventDisabledButtons()
-
- $('#test-link').click()
-
- expect(window.location.href).toBe locationBeforeLinkClick
diff --git a/spec/javascripts/awards_handler_spec.js b/spec/javascripts/awards_handler_spec.js
new file mode 100644
index 00000000000..c1c12b57b53
--- /dev/null
+++ b/spec/javascripts/awards_handler_spec.js
@@ -0,0 +1,261 @@
+
+/*= require awards_handler */
+
+
+/*= require jquery */
+
+
+/*= require jquery.cookie */
+
+
+/*= require ./fixtures/emoji_menu */
+
+(function() {
+ var awardsHandler, lazyAssert, urlRoot;
+
+ awardsHandler = null;
+
+ window.gl || (window.gl = {});
+
+ window.gon || (window.gon = {});
+
+ gl.emojiAliases = function() {
+ return {
+ '+1': 'thumbsup',
+ '-1': 'thumbsdown'
+ };
+ };
+
+ gon.award_menu_url = '/emojis';
+ urlRoot = gon.relative_url_root;
+
+ lazyAssert = function(done, assertFn) {
+ return setTimeout(function() {
+ assertFn();
+ return done();
+ }, 333);
+ };
+
+ describe('AwardsHandler', function() {
+ fixture.preload('awards_handler.html');
+ beforeEach(function() {
+ fixture.load('awards_handler.html');
+ awardsHandler = new AwardsHandler;
+ spyOn(awardsHandler, 'postEmoji').and.callFake((function(_this) {
+ return function(url, emoji, cb) {
+ return cb();
+ };
+ })(this));
+ spyOn(jQuery, 'get').and.callFake(function(req, cb) {
+ return cb(window.emojiMenu);
+ });
+ spyOn(jQuery, 'cookie');
+ });
+ afterEach(function() {
+ // restore original url root value
+ gon.relative_url_root = urlRoot;
+ });
+ describe('::showEmojiMenu', function() {
+ it('should show emoji menu when Add emoji button clicked', function(done) {
+ $('.js-add-award').eq(0).click();
+ return lazyAssert(done, function() {
+ var $emojiMenu;
+ $emojiMenu = $('.emoji-menu');
+ expect($emojiMenu.length).toBe(1);
+ expect($emojiMenu.hasClass('is-visible')).toBe(true);
+ expect($emojiMenu.find('#emoji_search').length).toBe(1);
+ return expect($('.js-awards-block.current').length).toBe(1);
+ });
+ });
+ it('should also show emoji menu for the smiley icon in notes', function(done) {
+ $('.note-action-button').click();
+ return lazyAssert(done, function() {
+ var $emojiMenu;
+ $emojiMenu = $('.emoji-menu');
+ return expect($emojiMenu.length).toBe(1);
+ });
+ });
+ return it('should remove emoji menu when body is clicked', function(done) {
+ $('.js-add-award').eq(0).click();
+ return lazyAssert(done, function() {
+ var $emojiMenu;
+ $emojiMenu = $('.emoji-menu');
+ $('body').click();
+ expect($emojiMenu.length).toBe(1);
+ expect($emojiMenu.hasClass('is-visible')).toBe(false);
+ return expect($('.js-awards-block.current').length).toBe(0);
+ });
+ });
+ });
+ describe('::addAwardToEmojiBar', function() {
+ it('should add emoji to votes block', function() {
+ var $emojiButton, $votesBlock;
+ $votesBlock = $('.js-awards-block').eq(0);
+ awardsHandler.addAwardToEmojiBar($votesBlock, 'heart', false);
+ $emojiButton = $votesBlock.find('[data-emoji=heart]');
+ expect($emojiButton.length).toBe(1);
+ expect($emojiButton.next('.js-counter').text()).toBe('1');
+ return expect($votesBlock.hasClass('hidden')).toBe(false);
+ });
+ it('should remove the emoji when we click again', function() {
+ var $emojiButton, $votesBlock;
+ $votesBlock = $('.js-awards-block').eq(0);
+ awardsHandler.addAwardToEmojiBar($votesBlock, 'heart', false);
+ awardsHandler.addAwardToEmojiBar($votesBlock, 'heart', false);
+ $emojiButton = $votesBlock.find('[data-emoji=heart]');
+ return expect($emojiButton.length).toBe(0);
+ });
+ return it('should decrement the emoji counter', function() {
+ var $emojiButton, $votesBlock;
+ $votesBlock = $('.js-awards-block').eq(0);
+ awardsHandler.addAwardToEmojiBar($votesBlock, 'heart', false);
+ $emojiButton = $votesBlock.find('[data-emoji=heart]');
+ $emojiButton.next('.js-counter').text(5);
+ awardsHandler.addAwardToEmojiBar($votesBlock, 'heart', false);
+ expect($emojiButton.length).toBe(1);
+ return expect($emojiButton.next('.js-counter').text()).toBe('4');
+ });
+ });
+ describe('::getAwardUrl', function() {
+ return it('should return the url for request', function() {
+ return expect(awardsHandler.getAwardUrl()).toBe('/gitlab-org/gitlab-test/issues/8/toggle_award_emoji');
+ });
+ });
+ describe('::addAward and ::checkMutuality', function() {
+ return it('should handle :+1: and :-1: mutuality', function() {
+ var $thumbsDownEmoji, $thumbsUpEmoji, $votesBlock, awardUrl;
+ 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', false);
+ expect($thumbsUpEmoji.hasClass('active')).toBe(true);
+ expect($thumbsDownEmoji.hasClass('active')).toBe(false);
+ $thumbsUpEmoji.tooltip();
+ $thumbsDownEmoji.tooltip();
+ awardsHandler.addAward($votesBlock, awardUrl, 'thumbsdown', true);
+ expect($thumbsUpEmoji.hasClass('active')).toBe(false);
+ return expect($thumbsDownEmoji.hasClass('active')).toBe(true);
+ });
+ });
+ describe('::removeEmoji', function() {
+ return it('should remove emoji', function() {
+ var $votesBlock, awardUrl;
+ awardUrl = awardsHandler.getAwardUrl();
+ $votesBlock = $('.js-awards-block').eq(0);
+ awardsHandler.addAward($votesBlock, awardUrl, 'fire', false);
+ expect($votesBlock.find('[data-emoji=fire]').length).toBe(1);
+ awardsHandler.removeEmoji($votesBlock.find('[data-emoji=fire]').closest('button'));
+ return expect($votesBlock.find('[data-emoji=fire]').length).toBe(0);
+ });
+ });
+ describe('::addYouToUserList', function() {
+ it('should prepend "You" to the award tooltip', function() {
+ var $thumbsUpEmoji, $votesBlock, awardUrl;
+ awardUrl = awardsHandler.getAwardUrl();
+ $votesBlock = $('.js-awards-block').eq(0);
+ $thumbsUpEmoji = $votesBlock.find('[data-emoji=thumbsup]').parent();
+ $thumbsUpEmoji.attr('data-title', 'sam, jerry, max, and andy');
+ awardsHandler.addAward($votesBlock, awardUrl, 'thumbsup', false);
+ $thumbsUpEmoji.tooltip();
+ return expect($thumbsUpEmoji.data("original-title")).toBe('You, sam, jerry, max, and andy');
+ });
+ return it('handles the special case where "You" is not cleanly comma seperated', function() {
+ var $thumbsUpEmoji, $votesBlock, awardUrl;
+ awardUrl = awardsHandler.getAwardUrl();
+ $votesBlock = $('.js-awards-block').eq(0);
+ $thumbsUpEmoji = $votesBlock.find('[data-emoji=thumbsup]').parent();
+ $thumbsUpEmoji.attr('data-title', 'sam');
+ awardsHandler.addAward($votesBlock, awardUrl, 'thumbsup', false);
+ $thumbsUpEmoji.tooltip();
+ return expect($thumbsUpEmoji.data("original-title")).toBe('You and sam');
+ });
+ });
+ describe('::removeYouToUserList', function() {
+ it('removes "You" from the front of the tooltip', function() {
+ var $thumbsUpEmoji, $votesBlock, awardUrl;
+ awardUrl = awardsHandler.getAwardUrl();
+ $votesBlock = $('.js-awards-block').eq(0);
+ $thumbsUpEmoji = $votesBlock.find('[data-emoji=thumbsup]').parent();
+ $thumbsUpEmoji.attr('data-title', 'You, sam, jerry, max, and andy');
+ $thumbsUpEmoji.addClass('active');
+ awardsHandler.addAward($votesBlock, awardUrl, 'thumbsup', false);
+ $thumbsUpEmoji.tooltip();
+ return expect($thumbsUpEmoji.data("original-title")).toBe('sam, jerry, max, and andy');
+ });
+ return it('handles the special case where "You" is not cleanly comma seperated', function() {
+ var $thumbsUpEmoji, $votesBlock, awardUrl;
+ awardUrl = awardsHandler.getAwardUrl();
+ $votesBlock = $('.js-awards-block').eq(0);
+ $thumbsUpEmoji = $votesBlock.find('[data-emoji=thumbsup]').parent();
+ $thumbsUpEmoji.attr('data-title', 'You and sam');
+ $thumbsUpEmoji.addClass('active');
+ awardsHandler.addAward($votesBlock, awardUrl, 'thumbsup', false);
+ $thumbsUpEmoji.tooltip();
+ return expect($thumbsUpEmoji.data("original-title")).toBe('sam');
+ });
+ });
+ describe('::addEmojiToFrequentlyUsedList', function() {
+ it('should set a cookie with the correct default path', function() {
+ gon.relative_url_root = '';
+ awardsHandler.addEmojiToFrequentlyUsedList('sunglasses');
+ expect(jQuery.cookie)
+ .toHaveBeenCalledWith('frequently_used_emojis', 'sunglasses', {
+ path: '/',
+ expires: 365
+ })
+ ;
+ });
+ it('should set a cookie with the correct custom root path', function() {
+ gon.relative_url_root = '/gitlab/subdir';
+ awardsHandler.addEmojiToFrequentlyUsedList('alien');
+ expect(jQuery.cookie)
+ .toHaveBeenCalledWith('frequently_used_emojis', 'alien', {
+ path: '/gitlab/subdir',
+ expires: 365
+ })
+ ;
+ });
+ });
+ describe('search', function() {
+ return it('should filter the emoji', function() {
+ $('.js-add-award').eq(0).click();
+ expect($('[data-emoji=angel]').is(':visible')).toBe(true);
+ expect($('[data-emoji=anger]').is(':visible')).toBe(true);
+ $('#emoji_search').val('ali').trigger('keyup');
+ expect($('[data-emoji=angel]').is(':visible')).toBe(false);
+ expect($('[data-emoji=anger]').is(':visible')).toBe(false);
+ return expect($('[data-emoji=alien]').is(':visible')).toBe(true);
+ });
+ });
+ return describe('emoji menu', function() {
+ var openEmojiMenuAndAddEmoji, selector;
+ selector = '[data-emoji=sunglasses]';
+ openEmojiMenuAndAddEmoji = function() {
+ var $block, $emoji, $menu;
+ $('.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(false);
+ return expect($block.find(selector).length).toBe(1);
+ };
+ it('should add selected emoji to awards block', function() {
+ return openEmojiMenuAndAddEmoji();
+ });
+ return it('should remove already selected emoji', function() {
+ var $block, $emoji;
+ openEmojiMenuAndAddEmoji();
+ $('.js-add-award').eq(0).click();
+ $block = $('.js-awards-block');
+ $emoji = $('.emoji-menu').find(".emoji-menu-list-item " + selector);
+ $emoji.click();
+ return expect($block.find(selector).length).toBe(0);
+ });
+ });
+ });
+
+}).call(this);
diff --git a/spec/javascripts/awards_handler_spec.js.coffee b/spec/javascripts/awards_handler_spec.js.coffee
deleted file mode 100644
index d7f9c6fc076..00000000000
--- a/spec/javascripts/awards_handler_spec.js.coffee
+++ /dev/null
@@ -1,200 +0,0 @@
-#= 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
-
-
- 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/autosize_spec.js b/spec/javascripts/behaviors/autosize_spec.js
new file mode 100644
index 00000000000..78795f7654a
--- /dev/null
+++ b/spec/javascripts/behaviors/autosize_spec.js
@@ -0,0 +1,21 @@
+
+/*= require behaviors/autosize */
+
+(function() {
+ describe('Autosize behavior', function() {
+ var load;
+ beforeEach(function() {
+ return fixture.set('<textarea class="js-autosize" style="resize: vertical"></textarea>');
+ });
+ it('does not overwrite the resize property', function() {
+ load();
+ return expect($('textarea')).toHaveCss({
+ resize: 'vertical'
+ });
+ });
+ return load = function() {
+ return $(document).trigger('page:load');
+ };
+ });
+
+}).call(this);
diff --git a/spec/javascripts/behaviors/autosize_spec.js.coffee b/spec/javascripts/behaviors/autosize_spec.js.coffee
deleted file mode 100644
index 7fc1d19c35f..00000000000
--- a/spec/javascripts/behaviors/autosize_spec.js.coffee
+++ /dev/null
@@ -1,11 +0,0 @@
-#= require behaviors/autosize
-
-describe 'Autosize behavior', ->
- beforeEach ->
- fixture.set('<textarea class="js-autosize" style="resize: vertical"></textarea>')
-
- it 'does not overwrite the resize property', ->
- load()
- expect($('textarea')).toHaveCss(resize: 'vertical')
-
- load = -> $(document).trigger('page:load')
diff --git a/spec/javascripts/behaviors/quick_submit_spec.js b/spec/javascripts/behaviors/quick_submit_spec.js
new file mode 100644
index 00000000000..4c52ecd903d
--- /dev/null
+++ b/spec/javascripts/behaviors/quick_submit_spec.js
@@ -0,0 +1,93 @@
+
+/*= require behaviors/quick_submit */
+
+(function() {
+ describe('Quick Submit behavior', function() {
+ var keydownEvent;
+ fixture.preload('behaviors/quick_submit.html');
+ beforeEach(function() {
+ fixture.load('behaviors/quick_submit.html');
+ $('form').submit(function(e) {
+ return e.preventDefault();
+ });
+ return this.spies = {
+ submit: spyOnEvent('form', 'submit')
+ };
+ });
+ it('does not respond to other keyCodes', function() {
+ $('input.quick-submit-input').trigger(keydownEvent({
+ keyCode: 32
+ }));
+ return expect(this.spies.submit).not.toHaveBeenTriggered();
+ });
+ it('does not respond to Enter alone', function() {
+ $('input.quick-submit-input').trigger(keydownEvent({
+ ctrlKey: false,
+ metaKey: false
+ }));
+ return expect(this.spies.submit).not.toHaveBeenTriggered();
+ });
+ it('does not respond to repeated events', function() {
+ $('input.quick-submit-input').trigger(keydownEvent({
+ repeat: true
+ }));
+ return expect(this.spies.submit).not.toHaveBeenTriggered();
+ });
+ it('disables submit buttons', function() {
+ $('textarea').trigger(keydownEvent());
+ expect($('input[type=submit]')).toBeDisabled();
+ return expect($('button[type=submit]')).toBeDisabled();
+ });
+ if (navigator.userAgent.match(/Macintosh/)) {
+ it('responds to Meta+Enter', function() {
+ $('input.quick-submit-input').trigger(keydownEvent());
+ return expect(this.spies.submit).toHaveBeenTriggered();
+ });
+ it('excludes other modifier keys', function() {
+ $('input.quick-submit-input').trigger(keydownEvent({
+ altKey: true
+ }));
+ $('input.quick-submit-input').trigger(keydownEvent({
+ ctrlKey: true
+ }));
+ $('input.quick-submit-input').trigger(keydownEvent({
+ shiftKey: true
+ }));
+ return expect(this.spies.submit).not.toHaveBeenTriggered();
+ });
+ } else {
+ it('responds to Ctrl+Enter', function() {
+ $('input.quick-submit-input').trigger(keydownEvent());
+ return expect(this.spies.submit).toHaveBeenTriggered();
+ });
+ it('excludes other modifier keys', function() {
+ $('input.quick-submit-input').trigger(keydownEvent({
+ altKey: true
+ }));
+ $('input.quick-submit-input').trigger(keydownEvent({
+ metaKey: true
+ }));
+ $('input.quick-submit-input').trigger(keydownEvent({
+ shiftKey: true
+ }));
+ return expect(this.spies.submit).not.toHaveBeenTriggered();
+ });
+ }
+ return keydownEvent = function(options) {
+ var defaults;
+ if (navigator.userAgent.match(/Macintosh/)) {
+ defaults = {
+ keyCode: 13,
+ metaKey: true
+ };
+ } else {
+ defaults = {
+ keyCode: 13,
+ ctrlKey: true
+ };
+ }
+ return $.Event('keydown', $.extend({}, defaults, options));
+ };
+ });
+
+}).call(this);
diff --git a/spec/javascripts/behaviors/quick_submit_spec.js.coffee b/spec/javascripts/behaviors/quick_submit_spec.js.coffee
deleted file mode 100644
index d3b003a328a..00000000000
--- a/spec/javascripts/behaviors/quick_submit_spec.js.coffee
+++ /dev/null
@@ -1,70 +0,0 @@
-#= require behaviors/quick_submit
-
-describe 'Quick Submit behavior', ->
- fixture.preload('behaviors/quick_submit.html')
-
- beforeEach ->
- fixture.load('behaviors/quick_submit.html')
-
- # Prevent a form submit from moving us off the testing page
- $('form').submit (e) -> e.preventDefault()
-
- @spies = {
- submit: spyOnEvent('form', 'submit')
- }
-
- it 'does not respond to other keyCodes', ->
- $('input.quick-submit-input').trigger(keydownEvent(keyCode: 32))
-
- expect(@spies.submit).not.toHaveBeenTriggered()
-
- it 'does not respond to Enter alone', ->
- $('input.quick-submit-input').trigger(keydownEvent(ctrlKey: false, metaKey: false))
-
- expect(@spies.submit).not.toHaveBeenTriggered()
-
- it 'does not respond to repeated events', ->
- $('input.quick-submit-input').trigger(keydownEvent(repeat: true))
-
- expect(@spies.submit).not.toHaveBeenTriggered()
-
- it 'disables submit buttons', ->
- $('textarea').trigger(keydownEvent())
-
- expect($('input[type=submit]')).toBeDisabled()
- expect($('button[type=submit]')).toBeDisabled()
-
- # We cannot stub `navigator.userAgent` for CI's `rake teaspoon` task, so we'll
- # only run the tests that apply to the current platform
- if navigator.userAgent.match(/Macintosh/)
- it 'responds to Meta+Enter', ->
- $('input.quick-submit-input').trigger(keydownEvent())
-
- expect(@spies.submit).toHaveBeenTriggered()
-
- it 'excludes other modifier keys', ->
- $('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.quick-submit-input').trigger(keydownEvent())
-
- expect(@spies.submit).toHaveBeenTriggered()
-
- it 'excludes other modifier keys', ->
- $('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()
-
- keydownEvent = (options) ->
- if navigator.userAgent.match(/Macintosh/)
- defaults = { keyCode: 13, metaKey: true }
- else
- defaults = { keyCode: 13, ctrlKey: true }
-
- $.Event('keydown', $.extend({}, defaults, options))
diff --git a/spec/javascripts/behaviors/requires_input_spec.js b/spec/javascripts/behaviors/requires_input_spec.js
new file mode 100644
index 00000000000..724c3baf989
--- /dev/null
+++ b/spec/javascripts/behaviors/requires_input_spec.js
@@ -0,0 +1,44 @@
+
+/*= require behaviors/requires_input */
+
+(function() {
+ describe('requiresInput', function() {
+ fixture.preload('behaviors/requires_input.html');
+ beforeEach(function() {
+ return fixture.load('behaviors/requires_input.html');
+ });
+ it('disables submit when any field is required', function() {
+ $('.js-requires-input').requiresInput();
+ return expect($('.submit')).toBeDisabled();
+ });
+ it('enables submit when no field is required', function() {
+ $('*[required=required]').removeAttr('required');
+ $('.js-requires-input').requiresInput();
+ return expect($('.submit')).not.toBeDisabled();
+ });
+ it('enables submit when all required fields are pre-filled', function() {
+ $('*[required=required]').remove();
+ $('.js-requires-input').requiresInput();
+ return expect($('.submit')).not.toBeDisabled();
+ });
+ it('enables submit when all required fields receive input', function() {
+ $('.js-requires-input').requiresInput();
+ $('#required1').val('input1').change();
+ expect($('.submit')).toBeDisabled();
+ $('#optional1').val('input1').change();
+ expect($('.submit')).toBeDisabled();
+ $('#required2').val('input2').change();
+ $('#required3').val('input3').change();
+ $('#required4').val('input4').change();
+ $('#required5').val('1').change();
+ return expect($('.submit')).not.toBeDisabled();
+ });
+ return it('is called on page:load event', function() {
+ var spy;
+ spy = spyOn($.fn, 'requiresInput');
+ $(document).trigger('page:load');
+ return expect(spy).toHaveBeenCalled();
+ });
+ });
+
+}).call(this);
diff --git a/spec/javascripts/behaviors/requires_input_spec.js.coffee b/spec/javascripts/behaviors/requires_input_spec.js.coffee
deleted file mode 100644
index 61a17632173..00000000000
--- a/spec/javascripts/behaviors/requires_input_spec.js.coffee
+++ /dev/null
@@ -1,49 +0,0 @@
-#= require behaviors/requires_input
-
-describe 'requiresInput', ->
- fixture.preload('behaviors/requires_input.html')
-
- beforeEach ->
- fixture.load('behaviors/requires_input.html')
-
- it 'disables submit when any field is required', ->
- $('.js-requires-input').requiresInput()
-
- expect($('.submit')).toBeDisabled()
-
- it 'enables submit when no field is required', ->
- $('*[required=required]').removeAttr('required')
-
- $('.js-requires-input').requiresInput()
-
- expect($('.submit')).not.toBeDisabled()
-
- it 'enables submit when all required fields are pre-filled', ->
- $('*[required=required]').remove()
-
- $('.js-requires-input').requiresInput()
-
- expect($('.submit')).not.toBeDisabled()
-
- it 'enables submit when all required fields receive input', ->
- $('.js-requires-input').requiresInput()
-
- $('#required1').val('input1').change()
- expect($('.submit')).toBeDisabled()
-
- $('#optional1').val('input1').change()
- expect($('.submit')).toBeDisabled()
-
- $('#required2').val('input2').change()
- $('#required3').val('input3').change()
- $('#required4').val('input4').change()
- $('#required5').val('1').change()
-
- expect($('.submit')).not.toBeDisabled()
-
- it 'is called on page:load event', ->
- spy = spyOn($.fn, 'requiresInput')
-
- $(document).trigger('page:load')
-
- expect(spy).toHaveBeenCalled()
diff --git a/spec/javascripts/boards/boards_store_spec.js.es6 b/spec/javascripts/boards/boards_store_spec.js.es6
new file mode 100644
index 00000000000..078e4b00023
--- /dev/null
+++ b/spec/javascripts/boards/boards_store_spec.js.es6
@@ -0,0 +1,164 @@
+//= require jquery
+//= require jquery_ujs
+//= require jquery.cookie
+//= require vue
+//= require vue-resource
+//= require lib/utils/url_utility
+//= require boards/models/issue
+//= require boards/models/label
+//= require boards/models/list
+//= require boards/models/user
+//= require boards/services/board_service
+//= require boards/stores/boards_store
+//= require ./mock_data
+
+(() => {
+ beforeEach(() => {
+ gl.boardService = new BoardService('/test/issue-boards/board');
+ gl.issueBoards.BoardsStore.create();
+
+ $.cookie('issue_board_welcome_hidden', 'false');
+ });
+
+ describe('Store', () => {
+ it('starts with a blank state', () => {
+ expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(0);
+ });
+
+ describe('lists', () => {
+ it('creates new list without persisting to DB', () => {
+ gl.issueBoards.BoardsStore.addList(listObj);
+
+ expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(1);
+ });
+
+ it('finds list by ID', () => {
+ gl.issueBoards.BoardsStore.addList(listObj);
+ const list = gl.issueBoards.BoardsStore.findList('id', 1);
+
+ expect(list.id).toBe(1);
+ });
+
+ it('finds list by type', () => {
+ gl.issueBoards.BoardsStore.addList(listObj);
+ const list = gl.issueBoards.BoardsStore.findList('type', 'label');
+
+ expect(list).toBeDefined();
+ });
+
+ it('finds list limited by type', () => {
+ gl.issueBoards.BoardsStore.addList({
+ id: 1,
+ position: 0,
+ title: 'Test',
+ list_type: 'backlog'
+ });
+ const list = gl.issueBoards.BoardsStore.findList('id', 1, 'backlog');
+
+ expect(list).toBeDefined();
+ });
+
+ it('gets issue when new list added', (done) => {
+ gl.issueBoards.BoardsStore.addList(listObj);
+ const list = gl.issueBoards.BoardsStore.findList('id', 1);
+
+ expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(1);
+
+ setTimeout(() => {
+ expect(list.issues.length).toBe(1);
+ expect(list.issues[0].id).toBe(1);
+ done();
+ }, 0);
+ });
+
+ it('persists new list', (done) => {
+ gl.issueBoards.BoardsStore.new({
+ title: 'Test',
+ type: 'label',
+ label: {
+ id: 1,
+ title: 'Testing',
+ color: 'red',
+ description: 'testing;'
+ }
+ });
+ expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(1);
+
+ setTimeout(() => {
+ const list = gl.issueBoards.BoardsStore.findList('id', 1);
+ expect(list).toBeDefined();
+ expect(list.id).toBe(1);
+ expect(list.position).toBe(0);
+ done();
+ }, 0);
+ });
+
+ it('check for blank state adding', () => {
+ expect(gl.issueBoards.BoardsStore.shouldAddBlankState()).toBe(true);
+ });
+
+ it('check for blank state not adding', () => {
+ gl.issueBoards.BoardsStore.addList(listObj);
+ expect(gl.issueBoards.BoardsStore.shouldAddBlankState()).toBe(false);
+ });
+
+ it('check for blank state adding when backlog & done list exist', () => {
+ gl.issueBoards.BoardsStore.addList({
+ list_type: 'backlog'
+ });
+ gl.issueBoards.BoardsStore.addList({
+ list_type: 'done'
+ });
+
+ expect(gl.issueBoards.BoardsStore.shouldAddBlankState()).toBe(true);
+ });
+
+ it('adds the blank state', () => {
+ gl.issueBoards.BoardsStore.addBlankState();
+
+ const list = gl.issueBoards.BoardsStore.findList('type', 'blank', 'blank');
+ expect(list).toBeDefined();
+ });
+
+ it('removes list from state', () => {
+ gl.issueBoards.BoardsStore.addList(listObj);
+
+ expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(1);
+
+ gl.issueBoards.BoardsStore.removeList(1, 'label');
+
+ expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(0);
+ });
+
+ it('moves the position of lists', () => {
+ const listOne = gl.issueBoards.BoardsStore.addList(listObj),
+ listTwo = gl.issueBoards.BoardsStore.addList(listObjDuplicate);
+
+ expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(2);
+
+ gl.issueBoards.BoardsStore.moveList(listOne, ['2', '1']);
+
+ expect(listOne.position).toBe(1);
+ });
+
+ it('moves an issue from one list to another', (done) => {
+ const listOne = gl.issueBoards.BoardsStore.addList(listObj),
+ listTwo = gl.issueBoards.BoardsStore.addList(listObjDuplicate);
+
+ expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(2);
+
+ setTimeout(() => {
+ expect(listOne.issues.length).toBe(1);
+ expect(listTwo.issues.length).toBe(1);
+
+ gl.issueBoards.BoardsStore.moveIssueToList(listOne, listTwo, listOne.findIssue(1));
+
+ expect(listOne.issues.length).toBe(0);
+ expect(listTwo.issues.length).toBe(1);
+
+ done();
+ }, 0);
+ });
+ });
+ });
+})();
diff --git a/spec/javascripts/boards/issue_spec.js.es6 b/spec/javascripts/boards/issue_spec.js.es6
new file mode 100644
index 00000000000..3569d1b98bd
--- /dev/null
+++ b/spec/javascripts/boards/issue_spec.js.es6
@@ -0,0 +1,83 @@
+//= require jquery
+//= require jquery_ujs
+//= require jquery.cookie
+//= require vue
+//= require vue-resource
+//= require lib/utils/url_utility
+//= require boards/models/issue
+//= require boards/models/label
+//= require boards/models/list
+//= require boards/models/user
+//= require boards/services/board_service
+//= require boards/stores/boards_store
+//= require ./mock_data
+
+describe('Issue model', () => {
+ let issue;
+
+ beforeEach(() => {
+ gl.boardService = new BoardService('/test/issue-boards/board');
+ gl.issueBoards.BoardsStore.create();
+
+ issue = new ListIssue({
+ title: 'Testing',
+ iid: 1,
+ confidential: false,
+ labels: [{
+ id: 1,
+ title: 'test',
+ color: 'red',
+ description: 'testing'
+ }]
+ });
+ });
+
+ it('has label', () => {
+ expect(issue.labels.length).toBe(1);
+ });
+
+ it('add new label', () => {
+ issue.addLabel({
+ id: 2,
+ title: 'bug',
+ color: 'blue',
+ description: 'bugs!'
+ });
+ expect(issue.labels.length).toBe(2);
+ });
+
+ it('does not add existing label', () => {
+ issue.addLabel({
+ id: 2,
+ title: 'test',
+ color: 'blue',
+ description: 'bugs!'
+ });
+
+ expect(issue.labels.length).toBe(1);
+ });
+
+ it('finds label', () => {
+ const label = issue.findLabel(issue.labels[0]);
+ expect(label).toBeDefined();
+ });
+
+ it('removes label', () => {
+ const label = issue.findLabel(issue.labels[0]);
+ issue.removeLabel(label);
+ expect(issue.labels.length).toBe(0);
+ });
+
+ it('removes multiple labels', () => {
+ issue.addLabel({
+ id: 2,
+ title: 'bug',
+ color: 'blue',
+ description: 'bugs!'
+ });
+ expect(issue.labels.length).toBe(2);
+
+ issue.removeLabels([issue.labels[0], issue.labels[1]]);
+ expect(issue.labels.length).toBe(0);
+ });
+});
diff --git a/spec/javascripts/boards/list_spec.js.es6 b/spec/javascripts/boards/list_spec.js.es6
new file mode 100644
index 00000000000..c206b794442
--- /dev/null
+++ b/spec/javascripts/boards/list_spec.js.es6
@@ -0,0 +1,89 @@
+//= require jquery
+//= require jquery_ujs
+//= require jquery.cookie
+//= require vue
+//= require vue-resource
+//= require lib/utils/url_utility
+//= require boards/models/issue
+//= require boards/models/label
+//= require boards/models/list
+//= require boards/models/user
+//= require boards/services/board_service
+//= require boards/stores/boards_store
+//= require ./mock_data
+
+describe('List model', () => {
+ let list;
+
+ beforeEach(() => {
+ gl.boardService = new BoardService('/test/issue-boards/board');
+ gl.issueBoards.BoardsStore.create();
+
+ list = new List(listObj);
+ });
+
+ it('gets issues when created', (done) => {
+ setTimeout(() => {
+ expect(list.issues.length).toBe(1);
+ done();
+ }, 0);
+ });
+
+ it('saves list and returns ID', (done) => {
+ list = new List({
+ title: 'test',
+ label: {
+ id: 1,
+ title: 'test',
+ color: 'red'
+ }
+ });
+ list.save();
+
+ setTimeout(() => {
+ expect(list.id).toBe(1);
+ expect(list.type).toBe('label');
+ expect(list.position).toBe(0);
+ done();
+ }, 0);
+ });
+
+ it('destroys the list', (done) => {
+ gl.issueBoards.BoardsStore.addList(listObj);
+ list = gl.issueBoards.BoardsStore.findList('id', 1);
+ expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(1);
+ list.destroy();
+
+ setTimeout(() => {
+ expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(0);
+ done();
+ }, 0);
+ });
+
+ it('can\'t search when not backlog', () => {
+ expect(list.canSearch()).toBe(false);
+ });
+
+ it('can search when backlog', () => {
+ list.type = 'backlog';
+ expect(list.canSearch()).toBe(true);
+ });
+
+ it('gets issue from list', (done) => {
+ setTimeout(() => {
+ const issue = list.findIssue(1);
+ expect(issue).toBeDefined();
+ done();
+ }, 0);
+ });
+
+ it('removes issue', (done) => {
+ setTimeout(() => {
+ const issue = list.findIssue(1);
+ expect(list.issues.length).toBe(1);
+ list.removeIssue(issue);
+ expect(list.issues.length).toBe(0);
+ done();
+ }, 0);
+ });
+});
diff --git a/spec/javascripts/boards/mock_data.js.es6 b/spec/javascripts/boards/mock_data.js.es6
new file mode 100644
index 00000000000..0c37ec8354f
--- /dev/null
+++ b/spec/javascripts/boards/mock_data.js.es6
@@ -0,0 +1,53 @@
+const listObj = {
+ id: 1,
+ position: 0,
+ title: 'Test',
+ list_type: 'label',
+ label: {
+ id: 1,
+ title: 'Testing',
+ color: 'red',
+ description: 'testing;'
+ }
+};
+
+const listObjDuplicate = {
+ id: 2,
+ position: 1,
+ title: 'Test',
+ list_type: 'label',
+ label: {
+ id: 2,
+ title: 'Testing',
+ color: 'red',
+ description: 'testing;'
+ }
+};
+
+const BoardsMockData = {
+ 'GET': {
+ '/test/issue-boards/board/lists{/id}/issues': [{
+ title: 'Testing',
+ iid: 1,
+ confidential: false,
+ labels: []
+ }]
+ },
+ 'POST': {
+ '/test/issue-boards/board/lists{/id}': listObj
+ },
+ 'PUT': {
+ '/test/issue-boards/board/lists{/id}': {}
+ },
+ 'DELETE': {
+ '/test/issue-boards/board/lists{/id}': {}
+ }
+};
+
+Vue.http.interceptors.push((request, next) => {
+ const body = BoardsMockData[request.method][request.url];
+
+ next(request.respondWith(JSON.stringify(body), {
+ status: 200
+ }));
+});
diff --git a/spec/javascripts/datetime_utility_spec.js.coffee b/spec/javascripts/datetime_utility_spec.js.coffee
new file mode 100644
index 00000000000..8bd113e7d86
--- /dev/null
+++ b/spec/javascripts/datetime_utility_spec.js.coffee
@@ -0,0 +1,50 @@
+#= require lib/utils/datetime_utility
+
+describe 'Date time utils', ->
+ describe 'get day name', ->
+ it 'should return Sunday', ->
+ day = gl.utils.getDayName(new Date('07/17/2016'))
+ expect(day).toBe('Sunday')
+
+ it 'should return Monday', ->
+ day = gl.utils.getDayName(new Date('07/18/2016'))
+ expect(day).toBe('Monday')
+
+ it 'should return Tuesday', ->
+ day = gl.utils.getDayName(new Date('07/19/2016'))
+ expect(day).toBe('Tuesday')
+
+ it 'should return Wednesday', ->
+ day = gl.utils.getDayName(new Date('07/20/2016'))
+ expect(day).toBe('Wednesday')
+
+ it 'should return Thursday', ->
+ day = gl.utils.getDayName(new Date('07/21/2016'))
+ expect(day).toBe('Thursday')
+
+ it 'should return Friday', ->
+ day = gl.utils.getDayName(new Date('07/22/2016'))
+ expect(day).toBe('Friday')
+
+ it 'should return Saturday', ->
+ day = gl.utils.getDayName(new Date('07/23/2016'))
+ expect(day).toBe('Saturday')
+
+ describe 'get day difference', ->
+ it 'should return 7', ->
+ firstDay = new Date('07/01/2016')
+ secondDay = new Date('07/08/2016')
+ difference = gl.utils.getDayDifference(firstDay, secondDay)
+ expect(difference).toBe(7)
+
+ it 'should return 31', ->
+ firstDay = new Date('07/01/2016')
+ secondDay = new Date('08/01/2016')
+ difference = gl.utils.getDayDifference(firstDay, secondDay)
+ expect(difference).toBe(31)
+
+ it 'should return 365', ->
+ firstDay = new Date('07/02/2015')
+ secondDay = new Date('07/01/2016')
+ difference = gl.utils.getDayDifference(firstDay, secondDay)
+ expect(difference).toBe(365) \ No newline at end of file
diff --git a/spec/javascripts/diff_comments_store_spec.js.es6 b/spec/javascripts/diff_comments_store_spec.js.es6
new file mode 100644
index 00000000000..22293d4de87
--- /dev/null
+++ b/spec/javascripts/diff_comments_store_spec.js.es6
@@ -0,0 +1,122 @@
+//= require vue
+//= require diff_notes/models/discussion
+//= require diff_notes/models/note
+//= require diff_notes/stores/comments
+(() => {
+ function createDiscussion(noteId = 1, resolved = true) {
+ CommentsStore.create('a', noteId, true, resolved, 'test');
+ };
+
+ beforeEach(() => {
+ CommentsStore.state = {};
+ });
+
+ describe('New discussion', () => {
+ it('creates new discussion', () => {
+ expect(Object.keys(CommentsStore.state).length).toBe(0);
+ createDiscussion();
+ expect(Object.keys(CommentsStore.state).length).toBe(1);
+ });
+
+ it('creates new note in discussion', () => {
+ createDiscussion();
+ createDiscussion(2);
+
+ const discussion = CommentsStore.state['a'];
+ expect(Object.keys(discussion.notes).length).toBe(2);
+ });
+ });
+
+ describe('Get note', () => {
+ beforeEach(() => {
+ expect(Object.keys(CommentsStore.state).length).toBe(0);
+ createDiscussion();
+ });
+
+ it('gets note by ID', () => {
+ const note = CommentsStore.get('a', 1);
+ expect(note).toBeDefined();
+ expect(note.id).toBe(1);
+ });
+ });
+
+ describe('Delete discussion', () => {
+ beforeEach(() => {
+ expect(Object.keys(CommentsStore.state).length).toBe(0);
+ createDiscussion();
+ });
+
+ it('deletes discussion by ID', () => {
+ CommentsStore.delete('a', 1);
+ expect(Object.keys(CommentsStore.state).length).toBe(0);
+ });
+
+ it('deletes discussion when no more notes', () => {
+ createDiscussion();
+ createDiscussion(2);
+ expect(Object.keys(CommentsStore.state).length).toBe(1);
+ expect(Object.keys(CommentsStore.state['a'].notes).length).toBe(2);
+
+ CommentsStore.delete('a', 1);
+ CommentsStore.delete('a', 2);
+ expect(Object.keys(CommentsStore.state).length).toBe(0);
+ });
+ });
+
+ describe('Update note', () => {
+ beforeEach(() => {
+ expect(Object.keys(CommentsStore.state).length).toBe(0);
+ createDiscussion();
+ });
+
+ it('updates note to be unresolved', () => {
+ CommentsStore.update('a', 1, false, 'test');
+
+ const note = CommentsStore.get('a', 1);
+ expect(note.resolved).toBe(false);
+ });
+ });
+
+ describe('Discussion resolved', () => {
+ beforeEach(() => {
+ expect(Object.keys(CommentsStore.state).length).toBe(0);
+ createDiscussion();
+ });
+
+ it('is resolved with single note', () => {
+ const discussion = CommentsStore.state['a'];
+ expect(discussion.isResolved()).toBe(true);
+ });
+
+ it('is unresolved with 2 notes', () => {
+ const discussion = CommentsStore.state['a'];
+ createDiscussion(2, false);
+ console.log(discussion.isResolved());
+
+ expect(discussion.isResolved()).toBe(false);
+ });
+
+ it('is resolved with 2 notes', () => {
+ const discussion = CommentsStore.state['a'];
+ createDiscussion(2);
+
+ expect(discussion.isResolved()).toBe(true);
+ });
+
+ it('resolve all notes', () => {
+ const discussion = CommentsStore.state['a'];
+ createDiscussion(2, false);
+
+ discussion.resolveAllNotes();
+ expect(discussion.isResolved()).toBe(true);
+ });
+
+ it('unresolve all notes', () => {
+ const discussion = CommentsStore.state['a'];
+ createDiscussion(2);
+
+ discussion.unResolveAllNotes();
+ expect(discussion.isResolved()).toBe(false);
+ });
+ });
+})();
diff --git a/spec/javascripts/extensions/array_spec.js b/spec/javascripts/extensions/array_spec.js
new file mode 100644
index 00000000000..eced2f6575d
--- /dev/null
+++ b/spec/javascripts/extensions/array_spec.js
@@ -0,0 +1,22 @@
+
+/*= require extensions/array */
+
+(function() {
+ describe('Array extensions', function() {
+ describe('first', function() {
+ return it('returns the first item', function() {
+ var arr;
+ arr = [0, 1, 2, 3, 4, 5];
+ return expect(arr.first()).toBe(0);
+ });
+ });
+ return describe('last', function() {
+ return it('returns the last item', function() {
+ var arr;
+ arr = [0, 1, 2, 3, 4, 5];
+ return expect(arr.last()).toBe(5);
+ });
+ });
+ });
+
+}).call(this);
diff --git a/spec/javascripts/extensions/array_spec.js.coffee b/spec/javascripts/extensions/array_spec.js.coffee
deleted file mode 100644
index 4ceac619422..00000000000
--- a/spec/javascripts/extensions/array_spec.js.coffee
+++ /dev/null
@@ -1,12 +0,0 @@
-#= require extensions/array
-
-describe 'Array extensions', ->
- describe 'first', ->
- it 'returns the first item', ->
- arr = [0, 1, 2, 3, 4, 5]
- expect(arr.first()).toBe(0)
-
- describe 'last', ->
- it 'returns the last item', ->
- arr = [0, 1, 2, 3, 4, 5]
- expect(arr.last()).toBe(5)
diff --git a/spec/javascripts/extensions/jquery_spec.js b/spec/javascripts/extensions/jquery_spec.js
new file mode 100644
index 00000000000..b644344b95a
--- /dev/null
+++ b/spec/javascripts/extensions/jquery_spec.js
@@ -0,0 +1,42 @@
+
+/*= require extensions/jquery */
+
+(function() {
+ describe('jQuery extensions', function() {
+ describe('disable', function() {
+ beforeEach(function() {
+ return fixture.set('<input type="text" />');
+ });
+ it('adds the disabled attribute', function() {
+ var $input;
+ $input = $('input').first();
+ $input.disable();
+ return expect($input).toHaveAttr('disabled', 'disabled');
+ });
+ return it('adds the disabled class', function() {
+ var $input;
+ $input = $('input').first();
+ $input.disable();
+ return expect($input).toHaveClass('disabled');
+ });
+ });
+ return describe('enable', function() {
+ beforeEach(function() {
+ return fixture.set('<input type="text" disabled="disabled" class="disabled" />');
+ });
+ it('removes the disabled attribute', function() {
+ var $input;
+ $input = $('input').first();
+ $input.enable();
+ return expect($input).not.toHaveAttr('disabled');
+ });
+ return it('removes the disabled class', function() {
+ var $input;
+ $input = $('input').first();
+ $input.enable();
+ return expect($input).not.toHaveClass('disabled');
+ });
+ });
+ });
+
+}).call(this);
diff --git a/spec/javascripts/extensions/jquery_spec.js.coffee b/spec/javascripts/extensions/jquery_spec.js.coffee
deleted file mode 100644
index b10e16b7d01..00000000000
--- a/spec/javascripts/extensions/jquery_spec.js.coffee
+++ /dev/null
@@ -1,34 +0,0 @@
-#= require extensions/jquery
-
-describe 'jQuery extensions', ->
- describe 'disable', ->
- beforeEach ->
- fixture.set '<input type="text" />'
-
- it 'adds the disabled attribute', ->
- $input = $('input').first()
-
- $input.disable()
- expect($input).toHaveAttr('disabled', 'disabled')
-
- it 'adds the disabled class', ->
- $input = $('input').first()
-
- $input.disable()
- expect($input).toHaveClass('disabled')
-
- describe 'enable', ->
- beforeEach ->
- fixture.set '<input type="text" disabled="disabled" class="disabled" />'
-
- it 'removes the disabled attribute', ->
- $input = $('input').first()
-
- $input.enable()
- expect($input).not.toHaveAttr('disabled')
-
- it 'removes the disabled class', ->
- $input = $('input').first()
-
- $input.enable()
- expect($input).not.toHaveClass('disabled')
diff --git a/spec/javascripts/fixtures/abuse_reports.html.haml b/spec/javascripts/fixtures/abuse_reports.html.haml
new file mode 100644
index 00000000000..2ec302abcb7
--- /dev/null
+++ b/spec/javascripts/fixtures/abuse_reports.html.haml
@@ -0,0 +1,16 @@
+.abuse-reports
+ .message#long
+ Cat ipsum dolor sit amet, hide head under blanket so no one can see.
+ Gate keepers of hell eat and than sleep on your face but hunt by meowing
+ loudly at 5am next to human slave food dispenser cats go for world
+ domination or chase laser, yet poop on grasses chirp at birds. Cat is love,
+ cat is life chase after silly colored fish toys around the house climb a
+ tree, wait for a fireman jump to fireman then scratch his face fall asleep
+ on the washing machine lies down always hungry so caticus cuteicus. Sit on
+ human. Spot something, big eyes, big eyes, crouch, shake butt, prepare to
+ pounce sleep in the bathroom sink hiss at vacuum cleaner hide head under
+ blanket so no one can see throwup on your pillow.
+ .message#short
+ Cat ipsum dolor sit amet, groom yourself 4 hours - checked, have your
+ beauty sleep 18 hours - checked, be fabulous for the rest of the day -
+ checked! for shake treat bag.
diff --git a/spec/javascripts/fixtures/awards_handler.html.haml b/spec/javascripts/fixtures/awards_handler.html.haml
index d55936ee4f9..1ef2e8f8624 100644
--- a/spec/javascripts/fixtures/awards_handler.html.haml
+++ b/spec/javascripts/fixtures/awards_handler.html.haml
@@ -39,7 +39,7 @@
%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
+ %i.fa.fa-smile-o.link-highlight
.js-task-list-container.note-body.is-task-list-enabled
.note-text
%p Suscipit sunt quia quisquam sed eveniet ipsam.
diff --git a/spec/javascripts/fixtures/emoji_menu.coffee b/spec/javascripts/fixtures/emoji_menu.coffee
deleted file mode 100644
index ce1a41390d2..00000000000
--- a/spec/javascripts/fixtures/emoji_menu.coffee
+++ /dev/null
@@ -1,957 +0,0 @@
-window.emojiMenu = """
- <div class='emoji-menu'>
- <input type="text" name="emoji_search" id="emoji_search" value="" class="emoji-search search-input form-control" />
- <div class='emoji-menu-content'>
- <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/emoji_menu.js b/spec/javascripts/fixtures/emoji_menu.js
new file mode 100644
index 00000000000..99e3f7247bd
--- /dev/null
+++ b/spec/javascripts/fixtures/emoji_menu.js
@@ -0,0 +1,4 @@
+(function() {
+ window.emojiMenu = "<div class='emoji-menu'>\n <input type=\"text\" name=\"emoji_search\" id=\"emoji_search\" value=\"\" class=\"emoji-search search-input form-control\" />\n <div class='emoji-menu-content'>\n <h5 class='emoji-menu-title'>\n Emoticons\n </h5>\n <ul class='clearfix emoji-menu-list'>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F47D\" title=\"alien\" data-aliases=\"\" data-emoji=\"alien\" data-unicode-name=\"1F47D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F47C\" title=\"angel\" data-aliases=\"\" data-emoji=\"angel\" data-unicode-name=\"1F47C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4A2\" title=\"anger\" data-aliases=\"\" data-emoji=\"anger\" data-unicode-name=\"1F4A2\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F620\" title=\"angry\" data-aliases=\"\" data-emoji=\"angry\" data-unicode-name=\"1F620\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F627\" title=\"anguished\" data-aliases=\"\" data-emoji=\"anguished\" data-unicode-name=\"1F627\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F632\" title=\"astonished\" data-aliases=\"\" data-emoji=\"astonished\" data-unicode-name=\"1F632\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F45F\" title=\"athletic_shoe\" data-aliases=\"\" data-emoji=\"athletic_shoe\" data-unicode-name=\"1F45F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F476\" title=\"baby\" data-aliases=\"\" data-emoji=\"baby\" data-unicode-name=\"1F476\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F459\" title=\"bikini\" data-aliases=\"\" data-emoji=\"bikini\" data-unicode-name=\"1F459\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F499\" title=\"blue_heart\" data-aliases=\"\" data-emoji=\"blue_heart\" data-unicode-name=\"1F499\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F60A\" title=\"blush\" data-aliases=\"\" data-emoji=\"blush\" data-unicode-name=\"1F60A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4A5\" title=\"boom\" data-aliases=\"\" data-emoji=\"boom\" data-unicode-name=\"1F4A5\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F462\" title=\"boot\" data-aliases=\"\" data-emoji=\"boot\" data-unicode-name=\"1F462\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F647\" title=\"bow\" data-aliases=\"\" data-emoji=\"bow\" data-unicode-name=\"1F647\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F466\" title=\"boy\" data-aliases=\"\" data-emoji=\"boy\" data-unicode-name=\"1F466\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F470\" title=\"bride_with_veil\" data-aliases=\"\" data-emoji=\"bride_with_veil\" data-unicode-name=\"1F470\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4BC\" title=\"briefcase\" data-aliases=\"\" data-emoji=\"briefcase\" data-unicode-name=\"1F4BC\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F494\" title=\"broken_heart\" data-aliases=\"\" data-emoji=\"broken_heart\" data-unicode-name=\"1F494\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F464\" title=\"bust_in_silhouette\" data-aliases=\"\" data-emoji=\"bust_in_silhouette\" data-unicode-name=\"1F464\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F465\" title=\"busts_in_silhouette\" data-aliases=\"\" data-emoji=\"busts_in_silhouette\" data-unicode-name=\"1F465\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F44F\" title=\"clap\" data-aliases=\"\" data-emoji=\"clap\" data-unicode-name=\"1F44F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F302\" title=\"closed_umbrella\" data-aliases=\"\" data-emoji=\"closed_umbrella\" data-unicode-name=\"1F302\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F630\" title=\"cold_sweat\" data-aliases=\"\" data-emoji=\"cold_sweat\" data-unicode-name=\"1F630\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F616\" title=\"confounded\" data-aliases=\"\" data-emoji=\"confounded\" data-unicode-name=\"1F616\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F615\" title=\"confused\" data-aliases=\"\" data-emoji=\"confused\" data-unicode-name=\"1F615\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F477\" title=\"construction_worker\" data-aliases=\"\" data-emoji=\"construction_worker\" data-unicode-name=\"1F477\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F46E\" title=\"cop\" data-aliases=\"\" data-emoji=\"cop\" data-unicode-name=\"1F46E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F46B\" title=\"couple\" data-aliases=\"\" data-emoji=\"couple\" data-unicode-name=\"1F46B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F491\" title=\"couple_with_heart\" data-aliases=\"\" data-emoji=\"couple_with_heart\" data-unicode-name=\"1F491\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F48F\" title=\"couplekiss\" data-aliases=\"\" data-emoji=\"couplekiss\" data-unicode-name=\"1F48F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F451\" title=\"crown\" data-aliases=\"\" data-emoji=\"crown\" data-unicode-name=\"1F451\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F622\" title=\"cry\" data-aliases=\"\" data-emoji=\"cry\" data-unicode-name=\"1F622\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F63F\" title=\"crying_cat_face\" data-aliases=\"\" data-emoji=\"crying_cat_face\" data-unicode-name=\"1F63F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F498\" title=\"cupid\" data-aliases=\"\" data-emoji=\"cupid\" data-unicode-name=\"1F498\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F483\" title=\"dancer\" data-aliases=\"\" data-emoji=\"dancer\" data-unicode-name=\"1F483\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F46F\" title=\"dancers\" data-aliases=\"\" data-emoji=\"dancers\" data-unicode-name=\"1F46F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4A8\" title=\"dash\" data-aliases=\"\" data-emoji=\"dash\" data-unicode-name=\"1F4A8\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F61E\" title=\"disappointed\" data-aliases=\"\" data-emoji=\"disappointed\" data-unicode-name=\"1F61E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F625\" title=\"disappointed_relieved\" data-aliases=\"\" data-emoji=\"disappointed_relieved\" data-unicode-name=\"1F625\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4AB\" title=\"dizzy\" data-aliases=\"\" data-emoji=\"dizzy\" data-unicode-name=\"1F4AB\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F635\" title=\"dizzy_face\" data-aliases=\"\" data-emoji=\"dizzy_face\" data-unicode-name=\"1F635\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F457\" title=\"dress\" data-aliases=\"\" data-emoji=\"dress\" data-unicode-name=\"1F457\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4A7\" title=\"droplet\" data-aliases=\"\" data-emoji=\"droplet\" data-unicode-name=\"1F4A7\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F442\" title=\"ear\" data-aliases=\"\" data-emoji=\"ear\" data-unicode-name=\"1F442\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F611\" title=\"expressionless\" data-aliases=\"\" data-emoji=\"expressionless\" data-unicode-name=\"1F611\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F453\" title=\"eyeglasses\" data-aliases=\"\" data-emoji=\"eyeglasses\" data-unicode-name=\"1F453\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F440\" title=\"eyes\" data-aliases=\"\" data-emoji=\"eyes\" data-unicode-name=\"1F440\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F46A\" title=\"family\" data-aliases=\"\" data-emoji=\"family\" data-unicode-name=\"1F46A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F628\" title=\"fearful\" data-aliases=\"\" data-emoji=\"fearful\" data-unicode-name=\"1F628\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F525\" title=\"fire\" data-aliases=\":flame:\" data-emoji=\"fire\" data-unicode-name=\"1F525\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-270A\" title=\"fist\" data-aliases=\"\" data-emoji=\"fist\" data-unicode-name=\"270A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F633\" title=\"flushed\" data-aliases=\"\" data-emoji=\"flushed\" data-unicode-name=\"1F633\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F463\" title=\"footprints\" data-aliases=\"\" data-emoji=\"footprints\" data-unicode-name=\"1F463\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F626\" title=\"frowning\" data-aliases=\":anguished:\" data-emoji=\"frowning\" data-unicode-name=\"1F626\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F48E\" title=\"gem\" data-aliases=\"\" data-emoji=\"gem\" data-unicode-name=\"1F48E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F467\" title=\"girl\" data-aliases=\"\" data-emoji=\"girl\" data-unicode-name=\"1F467\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F49A\" title=\"green_heart\" data-aliases=\"\" data-emoji=\"green_heart\" data-unicode-name=\"1F49A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F62C\" title=\"grimacing\" data-aliases=\"\" data-emoji=\"grimacing\" data-unicode-name=\"1F62C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F601\" title=\"grin\" data-aliases=\"\" data-emoji=\"grin\" data-unicode-name=\"1F601\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F600\" title=\"grinning\" data-aliases=\"\" data-emoji=\"grinning\" data-unicode-name=\"1F600\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F482\" title=\"guardsman\" data-aliases=\"\" data-emoji=\"guardsman\" data-unicode-name=\"1F482\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F487\" title=\"haircut\" data-aliases=\"\" data-emoji=\"haircut\" data-unicode-name=\"1F487\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F45C\" title=\"handbag\" data-aliases=\"\" data-emoji=\"handbag\" data-unicode-name=\"1F45C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F649\" title=\"hear_no_evil\" data-aliases=\"\" data-emoji=\"hear_no_evil\" data-unicode-name=\"1F649\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-2764\" title=\"heart\" data-aliases=\"\" data-emoji=\"heart\" data-unicode-name=\"2764\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F60D\" title=\"heart_eyes\" data-aliases=\"\" data-emoji=\"heart_eyes\" data-unicode-name=\"1F60D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F63B\" title=\"heart_eyes_cat\" data-aliases=\"\" data-emoji=\"heart_eyes_cat\" data-unicode-name=\"1F63B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F493\" title=\"heartbeat\" data-aliases=\"\" data-emoji=\"heartbeat\" data-unicode-name=\"1F493\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F497\" title=\"heartpulse\" data-aliases=\"\" data-emoji=\"heartpulse\" data-unicode-name=\"1F497\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F460\" title=\"high_heel\" data-aliases=\"\" data-emoji=\"high_heel\" data-unicode-name=\"1F460\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F62F\" title=\"hushed\" data-aliases=\"\" data-emoji=\"hushed\" data-unicode-name=\"1F62F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F47F\" title=\"imp\" data-aliases=\"\" data-emoji=\"imp\" data-unicode-name=\"1F47F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F481\" title=\"information_desk_person\" data-aliases=\"\" data-emoji=\"information_desk_person\" data-unicode-name=\"1F481\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F607\" title=\"innocent\" data-aliases=\"\" data-emoji=\"innocent\" data-unicode-name=\"1F607\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F47A\" title=\"japanese_goblin\" data-aliases=\"\" data-emoji=\"japanese_goblin\" data-unicode-name=\"1F47A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F479\" title=\"japanese_ogre\" data-aliases=\"\" data-emoji=\"japanese_ogre\" data-unicode-name=\"1F479\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F456\" title=\"jeans\" data-aliases=\"\" data-emoji=\"jeans\" data-unicode-name=\"1F456\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F602\" title=\"joy\" data-aliases=\"\" data-emoji=\"joy\" data-unicode-name=\"1F602\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F639\" title=\"joy_cat\" data-aliases=\"\" data-emoji=\"joy_cat\" data-unicode-name=\"1F639\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F458\" title=\"kimono\" data-aliases=\"\" data-emoji=\"kimono\" data-unicode-name=\"1F458\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F48B\" title=\"kiss\" data-aliases=\"\" data-emoji=\"kiss\" data-unicode-name=\"1F48B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F617\" title=\"kissing\" data-aliases=\"\" data-emoji=\"kissing\" data-unicode-name=\"1F617\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F63D\" title=\"kissing_cat\" data-aliases=\"\" data-emoji=\"kissing_cat\" data-unicode-name=\"1F63D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F61A\" title=\"kissing_closed_eyes\" data-aliases=\"\" data-emoji=\"kissing_closed_eyes\" data-unicode-name=\"1F61A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F618\" title=\"kissing_heart\" data-aliases=\"\" data-emoji=\"kissing_heart\" data-unicode-name=\"1F618\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F619\" title=\"kissing_smiling_eyes\" data-aliases=\"\" data-emoji=\"kissing_smiling_eyes\" data-unicode-name=\"1F619\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F606\" title=\"laughing\" data-aliases=\":satisfied:\" data-emoji=\"laughing\" data-unicode-name=\"1F606\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F444\" title=\"lips\" data-aliases=\"\" data-emoji=\"lips\" data-unicode-name=\"1F444\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F484\" title=\"lipstick\" data-aliases=\"\" data-emoji=\"lipstick\" data-unicode-name=\"1F484\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F48C\" title=\"love_letter\" data-aliases=\"\" data-emoji=\"love_letter\" data-unicode-name=\"1F48C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F468\" title=\"man\" data-aliases=\"\" data-emoji=\"man\" data-unicode-name=\"1F468\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <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>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F473\" title=\"man_with_turban\" data-aliases=\"\" data-emoji=\"man_with_turban\" data-unicode-name=\"1F473\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F45E\" title=\"mans_shoe\" data-aliases=\"\" data-emoji=\"mans_shoe\" data-unicode-name=\"1F45E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F637\" title=\"mask\" data-aliases=\"\" data-emoji=\"mask\" data-unicode-name=\"1F637\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F486\" title=\"massage\" data-aliases=\"\" data-emoji=\"massage\" data-unicode-name=\"1F486\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4AA\" title=\"muscle\" data-aliases=\"\" data-emoji=\"muscle\" data-unicode-name=\"1F4AA\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F485\" title=\"nail_care\" data-aliases=\"\" data-emoji=\"nail_care\" data-unicode-name=\"1F485\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F454\" title=\"necktie\" data-aliases=\"\" data-emoji=\"necktie\" data-unicode-name=\"1F454\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F610\" title=\"neutral_face\" data-aliases=\"\" data-emoji=\"neutral_face\" data-unicode-name=\"1F610\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F645\" title=\"no_good\" data-aliases=\"\" data-emoji=\"no_good\" data-unicode-name=\"1F645\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F636\" title=\"no_mouth\" data-aliases=\"\" data-emoji=\"no_mouth\" data-unicode-name=\"1F636\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F443\" title=\"nose\" data-aliases=\"\" data-emoji=\"nose\" data-unicode-name=\"1F443\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F44C\" title=\"ok_hand\" data-aliases=\"\" data-emoji=\"ok_hand\" data-unicode-name=\"1F44C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F646\" title=\"ok_woman\" data-aliases=\"\" data-emoji=\"ok_woman\" data-unicode-name=\"1F646\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F474\" title=\"older_man\" data-aliases=\"\" data-emoji=\"older_man\" data-unicode-name=\"1F474\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F475\" title=\"older_woman\" data-aliases=\":grandma:\" data-emoji=\"older_woman\" data-unicode-name=\"1F475\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F450\" title=\"open_hands\" data-aliases=\"\" data-emoji=\"open_hands\" data-unicode-name=\"1F450\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F62E\" title=\"open_mouth\" data-aliases=\"\" data-emoji=\"open_mouth\" data-unicode-name=\"1F62E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F614\" title=\"pensive\" data-aliases=\"\" data-emoji=\"pensive\" data-unicode-name=\"1F614\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F623\" title=\"persevere\" data-aliases=\"\" data-emoji=\"persevere\" data-unicode-name=\"1F623\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F64D\" title=\"person_frowning\" data-aliases=\"\" data-emoji=\"person_frowning\" data-unicode-name=\"1F64D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <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>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <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>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F447\" title=\"point_down\" data-aliases=\"\" data-emoji=\"point_down\" data-unicode-name=\"1F447\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F448\" title=\"point_left\" data-aliases=\"\" data-emoji=\"point_left\" data-unicode-name=\"1F448\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F449\" title=\"point_right\" data-aliases=\"\" data-emoji=\"point_right\" data-unicode-name=\"1F449\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-261D\" title=\"point_up\" data-aliases=\"\" data-emoji=\"point_up\" data-unicode-name=\"261D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F446\" title=\"point_up_2\" data-aliases=\"\" data-emoji=\"point_up_2\" data-unicode-name=\"1F446\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4A9\" title=\"poop\" data-aliases=\":shit: :hankey: :poo:\" data-emoji=\"poop\" data-unicode-name=\"1F4A9\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F45D\" title=\"pouch\" data-aliases=\"\" data-emoji=\"pouch\" data-unicode-name=\"1F45D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F63E\" title=\"pouting_cat\" data-aliases=\"\" data-emoji=\"pouting_cat\" data-unicode-name=\"1F63E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F64F\" title=\"pray\" data-aliases=\"\" data-emoji=\"pray\" data-unicode-name=\"1F64F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F478\" title=\"princess\" data-aliases=\"\" data-emoji=\"princess\" data-unicode-name=\"1F478\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F44A\" title=\"punch\" data-aliases=\"\" data-emoji=\"punch\" data-unicode-name=\"1F44A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F49C\" title=\"purple_heart\" data-aliases=\"\" data-emoji=\"purple_heart\" data-unicode-name=\"1F49C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F45B\" title=\"purse\" data-aliases=\"\" data-emoji=\"purse\" data-unicode-name=\"1F45B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F621\" title=\"rage\" data-aliases=\"\" data-emoji=\"rage\" data-unicode-name=\"1F621\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-270B\" title=\"raised_hand\" data-aliases=\"\" data-emoji=\"raised_hand\" data-unicode-name=\"270B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F64C\" title=\"raised_hands\" data-aliases=\"\" data-emoji=\"raised_hands\" data-unicode-name=\"1F64C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F64B\" title=\"raising_hand\" data-aliases=\"\" data-emoji=\"raising_hand\" data-unicode-name=\"1F64B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-263A\" title=\"relaxed\" data-aliases=\"\" data-emoji=\"relaxed\" data-unicode-name=\"263A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F60C\" title=\"relieved\" data-aliases=\"\" data-emoji=\"relieved\" data-unicode-name=\"1F60C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F49E\" title=\"revolving_hearts\" data-aliases=\"\" data-emoji=\"revolving_hearts\" data-unicode-name=\"1F49E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F380\" title=\"ribbon\" data-aliases=\"\" data-emoji=\"ribbon\" data-unicode-name=\"1F380\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F48D\" title=\"ring\" data-aliases=\"\" data-emoji=\"ring\" data-unicode-name=\"1F48D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F3C3\" title=\"runner\" data-aliases=\"\" data-emoji=\"runner\" data-unicode-name=\"1F3C3\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <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>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F461\" title=\"sandal\" data-aliases=\"\" data-emoji=\"sandal\" data-unicode-name=\"1F461\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F631\" title=\"scream\" data-aliases=\"\" data-emoji=\"scream\" data-unicode-name=\"1F631\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F640\" title=\"scream_cat\" data-aliases=\"\" data-emoji=\"scream_cat\" data-unicode-name=\"1F640\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F648\" title=\"see_no_evil\" data-aliases=\"\" data-emoji=\"see_no_evil\" data-unicode-name=\"1F648\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F455\" title=\"shirt\" data-aliases=\"\" data-emoji=\"shirt\" data-unicode-name=\"1F455\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F480\" title=\"skull\" data-aliases=\":skeleton:\" data-emoji=\"skull\" data-unicode-name=\"1F480\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F634\" title=\"sleeping\" data-aliases=\"\" data-emoji=\"sleeping\" data-unicode-name=\"1F634\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F62A\" title=\"sleepy\" data-aliases=\"\" data-emoji=\"sleepy\" data-unicode-name=\"1F62A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F604\" title=\"smile\" data-aliases=\"\" data-emoji=\"smile\" data-unicode-name=\"1F604\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F638\" title=\"smile_cat\" data-aliases=\"\" data-emoji=\"smile_cat\" data-unicode-name=\"1F638\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F603\" title=\"smiley\" data-aliases=\"\" data-emoji=\"smiley\" data-unicode-name=\"1F603\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F63A\" title=\"smiley_cat\" data-aliases=\"\" data-emoji=\"smiley_cat\" data-unicode-name=\"1F63A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F608\" title=\"smiling_imp\" data-aliases=\"\" data-emoji=\"smiling_imp\" data-unicode-name=\"1F608\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F60F\" title=\"smirk\" data-aliases=\"\" data-emoji=\"smirk\" data-unicode-name=\"1F60F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F63C\" title=\"smirk_cat\" data-aliases=\"\" data-emoji=\"smirk_cat\" data-unicode-name=\"1F63C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F62D\" title=\"sob\" data-aliases=\"\" data-emoji=\"sob\" data-unicode-name=\"1F62D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-2728\" title=\"sparkles\" data-aliases=\"\" data-emoji=\"sparkles\" data-unicode-name=\"2728\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F496\" title=\"sparkling_heart\" data-aliases=\"\" data-emoji=\"sparkling_heart\" data-unicode-name=\"1F496\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F64A\" title=\"speak_no_evil\" data-aliases=\"\" data-emoji=\"speak_no_evil\" data-unicode-name=\"1F64A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4AC\" title=\"speech_balloon\" data-aliases=\"\" data-emoji=\"speech_balloon\" data-unicode-name=\"1F4AC\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F31F\" title=\"star2\" data-aliases=\"\" data-emoji=\"star2\" data-unicode-name=\"1F31F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F61B\" title=\"stuck_out_tongue\" data-aliases=\"\" data-emoji=\"stuck_out_tongue\" data-unicode-name=\"1F61B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <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>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <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>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F60E\" title=\"sunglasses\" data-aliases=\"\" data-emoji=\"sunglasses\" data-unicode-name=\"1F60E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F613\" title=\"sweat\" data-aliases=\"\" data-emoji=\"sweat\" data-unicode-name=\"1F613\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4A6\" title=\"sweat_drops\" data-aliases=\"\" data-emoji=\"sweat_drops\" data-unicode-name=\"1F4A6\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F605\" title=\"sweat_smile\" data-aliases=\"\" data-emoji=\"sweat_smile\" data-unicode-name=\"1F605\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4AD\" title=\"thought_balloon\" data-aliases=\"\" data-emoji=\"thought_balloon\" data-unicode-name=\"1F4AD\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F44E\" title=\"thumbsdown\" data-aliases=\":-1:\" data-emoji=\"thumbsdown\" data-unicode-name=\"1F44E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F44D\" title=\"thumbsup\" data-aliases=\":+1:\" data-emoji=\"thumbsup\" data-unicode-name=\"1F44D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F62B\" title=\"tired_face\" data-aliases=\"\" data-emoji=\"tired_face\" data-unicode-name=\"1F62B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F445\" title=\"tongue\" data-aliases=\"\" data-emoji=\"tongue\" data-unicode-name=\"1F445\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F3A9\" title=\"tophat\" data-aliases=\"\" data-emoji=\"tophat\" data-unicode-name=\"1F3A9\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F624\" title=\"triumph\" data-aliases=\"\" data-emoji=\"triumph\" data-unicode-name=\"1F624\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F495\" title=\"two_hearts\" data-aliases=\"\" data-emoji=\"two_hearts\" data-unicode-name=\"1F495\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <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>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <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>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F612\" title=\"unamused\" data-aliases=\"\" data-emoji=\"unamused\" data-unicode-name=\"1F612\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-270C\" title=\"v\" data-aliases=\"\" data-emoji=\"v\" data-unicode-name=\"270C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F6B6\" title=\"walking\" data-aliases=\"\" data-emoji=\"walking\" data-unicode-name=\"1F6B6\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F44B\" title=\"wave\" data-aliases=\"\" data-emoji=\"wave\" data-unicode-name=\"1F44B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F629\" title=\"weary\" data-aliases=\"\" data-emoji=\"weary\" data-unicode-name=\"1F629\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F609\" title=\"wink\" data-aliases=\"\" data-emoji=\"wink\" data-unicode-name=\"1F609\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F469\" title=\"woman\" data-aliases=\"\" data-emoji=\"woman\" data-unicode-name=\"1F469\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F45A\" title=\"womans_clothes\" data-aliases=\"\" data-emoji=\"womans_clothes\" data-unicode-name=\"1F45A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F452\" title=\"womans_hat\" data-aliases=\"\" data-emoji=\"womans_hat\" data-unicode-name=\"1F452\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F61F\" title=\"worried\" data-aliases=\"\" data-emoji=\"worried\" data-unicode-name=\"1F61F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F49B\" title=\"yellow_heart\" data-aliases=\"\" data-emoji=\"yellow_heart\" data-unicode-name=\"1F49B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F60B\" title=\"yum\" data-aliases=\"\" data-emoji=\"yum\" data-unicode-name=\"1F60B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4A4\" title=\"zzz\" data-aliases=\"\" data-emoji=\"zzz\" data-unicode-name=\"1F4A4\"></div>\n </button>\n </li>\n </ul>\n </div>\n</div>";
+
+}).call(this);
diff --git a/spec/javascripts/fixtures/gl_dropdown.html.haml b/spec/javascripts/fixtures/gl_dropdown.html.haml
new file mode 100644
index 00000000000..a20390c08ee
--- /dev/null
+++ b/spec/javascripts/fixtures/gl_dropdown.html.haml
@@ -0,0 +1,16 @@
+%div
+ .dropdown.inline
+ %button#js-project-dropdown.dropdown-menu-toggle{type: 'button', data: {toggle: 'dropdown'}}
+ Projects
+ %i.fa.fa-chevron-down.dropdown-toggle-caret.js-projects-dropdown-toggle
+ .dropdown-menu.dropdown-select.dropdown-menu-selectable
+ .dropdown-title
+ %span Go to project
+ %button.dropdown-title-button.dropdown-menu-close{aria: {label: 'Close'}}
+ %i.fa.fa-times.dropdown-menu-close-icon
+ .dropdown-input
+ %input.dropdown-input-field{type: 'search', placeholder: 'Filter results'}
+ %i.fa.fa-search.dropdown-input-search
+ .dropdown-content
+ .dropdown-loading
+ %i.fa.fa-spinner.fa-spin
diff --git a/spec/javascripts/fixtures/issue_sidebar_label.html.haml b/spec/javascripts/fixtures/issue_sidebar_label.html.haml
new file mode 100644
index 00000000000..397bdc85c67
--- /dev/null
+++ b/spec/javascripts/fixtures/issue_sidebar_label.html.haml
@@ -0,0 +1,16 @@
+.block.labels
+ .sidebar-collapsed-icon.js-sidebar-labels-tooltip
+ .title.hide-collapsed
+ %a.edit-link.pull-right{ href: "#" }
+ Edit
+ .selectbox.hide-collapsed{ style: "display: none;" }
+ .dropdown
+ %button.dropdown-menu-toggle.js-label-select.js-multiselect{ type: "button", data: { ability_name: "issue", field_name: "issue[label_names][]", issue_update: "/root/test/issues/2.json", labels: "/root/test/labels.json", project_id: "12", show_any: "true", show_no: "true", toggle: "dropdown" } }
+ %span.dropdown-toggle-text
+ Label
+ %i.fa.fa-chevron-down
+ .dropdown-menu.dropdown-select.dropdown-menu-paging.dropdown-menu-labels.dropdown-menu-selectable
+ .dropdown-page-one
+ .dropdown-content
+ .dropdown-loading
+ %i.fa.fa-spinner.fa-spin
diff --git a/spec/javascripts/fixtures/issues_show.html.haml b/spec/javascripts/fixtures/issues_show.html.haml
index 470cabeafbb..06c2ab1e823 100644
--- a/spec/javascripts/fixtures/issues_show.html.haml
+++ b/spec/javascripts/fixtures/issues_show.html.haml
@@ -1,7 +1,7 @@
:css
.hidden { display: none !important; }
-.flash-container
+.flash-container.flash-container-page
.flash-alert
.flash-notice
diff --git a/spec/javascripts/fixtures/projects.json b/spec/javascripts/fixtures/projects.json
index 84e8d0ba1e4..4919d77e5a4 100644
--- a/spec/javascripts/fixtures/projects.json
+++ b/spec/javascripts/fixtures/projects.json
@@ -1 +1 @@
-[{"id":9,"description":"","default_branch":null,"tag_list":[],"public":true,"archived":false,"visibility_level":20,"ssh_url_to_repo":"phil@localhost:root/test.git","http_url_to_repo":"http://localhost:3000/root/test.git","web_url":"http://localhost:3000/root/test","owner":{"name":"Administrator","username":"root","id":1,"state":"active","avatar_url":"http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon","web_url":"http://localhost:3000/u/root"},"name":"test","name_with_namespace":"Administrator / test","path":"test","path_with_namespace":"root/test","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"builds_enabled":true,"snippets_enabled":false,"created_at":"2016-01-14T19:08:05.364Z","last_activity_at":"2016-01-14T19:08:07.418Z","shared_runners_enabled":true,"creator_id":1,"namespace":{"id":1,"name":"root","path":"root","owner_id":1,"created_at":"2016-01-13T20:19:44.439Z","updated_at":"2016-01-13T20:19:44.439Z","description":"","avatar":null},"avatar_url":null,"star_count":0,"forks_count":0,"open_issues_count":0,"permissions":{"project_access":null,"group_access":null}},{"id":8,"description":"Voluptatem quae nulla eius numquam ullam voluptatibus quia modi.","default_branch":"master","tag_list":[],"public":false,"archived":false,"visibility_level":0,"ssh_url_to_repo":"phil@localhost:h5bp/html5-boilerplate.git","http_url_to_repo":"http://localhost:3000/h5bp/html5-boilerplate.git","web_url":"http://localhost:3000/h5bp/html5-boilerplate","name":"Html5 Boilerplate","name_with_namespace":"H5bp / Html5 Boilerplate","path":"html5-boilerplate","path_with_namespace":"h5bp/html5-boilerplate","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"builds_enabled":true,"snippets_enabled":false,"created_at":"2016-01-13T20:19:57.525Z","last_activity_at":"2016-01-13T20:27:57.280Z","shared_runners_enabled":true,"creator_id":1,"namespace":{"id":5,"name":"H5bp","path":"h5bp","owner_id":null,"created_at":"2016-01-13T20:19:57.239Z","updated_at":"2016-01-13T20:19:57.239Z","description":"Tempore accusantium possimus aut libero.","avatar":{"url":null}},"avatar_url":null,"star_count":0,"forks_count":0,"open_issues_count":5,"permissions":{"project_access":{"access_level":10,"notification_level":3},"group_access":{"access_level":50,"notification_level":3}}},{"id":7,"description":"Modi odio mollitia dolorem qui.","default_branch":"master","tag_list":[],"public":false,"archived":false,"visibility_level":0,"ssh_url_to_repo":"phil@localhost:twitter/typeahead-js.git","http_url_to_repo":"http://localhost:3000/twitter/typeahead-js.git","web_url":"http://localhost:3000/twitter/typeahead-js","name":"Typeahead.Js","name_with_namespace":"Twitter / Typeahead.Js","path":"typeahead-js","path_with_namespace":"twitter/typeahead-js","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"builds_enabled":true,"snippets_enabled":false,"created_at":"2016-01-13T20:19:56.212Z","last_activity_at":"2016-01-13T20:27:51.496Z","shared_runners_enabled":true,"creator_id":1,"namespace":{"id":4,"name":"Twitter","path":"twitter","owner_id":null,"created_at":"2016-01-13T20:19:54.480Z","updated_at":"2016-01-13T20:19:54.480Z","description":"Id voluptatem ipsa maiores omnis repudiandae et et.","avatar":{"url":null}},"avatar_url":null,"star_count":0,"forks_count":0,"open_issues_count":4,"permissions":{"project_access":null,"group_access":{"access_level":10,"notification_level":3}}},{"id":6,"description":"Omnis asperiores ipsa et beatae quidem necessitatibus quia.","default_branch":"master","tag_list":[],"public":true,"archived":false,"visibility_level":20,"ssh_url_to_repo":"phil@localhost:twitter/flight.git","http_url_to_repo":"http://localhost:3000/twitter/flight.git","web_url":"http://localhost:3000/twitter/flight","name":"Flight","name_with_namespace":"Twitter / Flight","path":"flight","path_with_namespace":"twitter/flight","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"builds_enabled":true,"snippets_enabled":false,"created_at":"2016-01-13T20:19:54.754Z","last_activity_at":"2016-01-13T20:27:50.502Z","shared_runners_enabled":true,"creator_id":1,"namespace":{"id":4,"name":"Twitter","path":"twitter","owner_id":null,"created_at":"2016-01-13T20:19:54.480Z","updated_at":"2016-01-13T20:19:54.480Z","description":"Id voluptatem ipsa maiores omnis repudiandae et et.","avatar":{"url":null}},"avatar_url":null,"star_count":0,"forks_count":0,"open_issues_count":4,"permissions":{"project_access":null,"group_access":{"access_level":10,"notification_level":3}}},{"id":5,"description":"Voluptatem commodi voluptate placeat architecto beatae illum dolores fugiat.","default_branch":"master","tag_list":[],"public":false,"archived":false,"visibility_level":0,"ssh_url_to_repo":"phil@localhost:gitlab-org/gitlab-test.git","http_url_to_repo":"http://localhost:3000/gitlab-org/gitlab-test.git","web_url":"http://localhost:3000/gitlab-org/gitlab-test","name":"Gitlab Test","name_with_namespace":"Gitlab Org / Gitlab Test","path":"gitlab-test","path_with_namespace":"gitlab-org/gitlab-test","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"builds_enabled":true,"snippets_enabled":false,"created_at":"2016-01-13T20:19:53.202Z","last_activity_at":"2016-01-13T20:27:41.626Z","shared_runners_enabled":true,"creator_id":1,"namespace":{"id":3,"name":"Gitlab Org","path":"gitlab-org","owner_id":null,"created_at":"2016-01-13T20:19:48.851Z","updated_at":"2016-01-13T20:19:48.851Z","description":"Magni mollitia quod quidem soluta nesciunt impedit.","avatar":{"url":null}},"avatar_url":null,"star_count":0,"forks_count":0,"open_issues_count":5,"permissions":{"project_access":null,"group_access":{"access_level":50,"notification_level":3}}},{"id":4,"description":"Aut molestias quas est ut aperiam officia quod libero.","default_branch":"master","tag_list":[],"public":true,"archived":false,"visibility_level":20,"ssh_url_to_repo":"phil@localhost:gitlab-org/gitlab-shell.git","http_url_to_repo":"http://localhost:3000/gitlab-org/gitlab-shell.git","web_url":"http://localhost:3000/gitlab-org/gitlab-shell","name":"Gitlab Shell","name_with_namespace":"Gitlab Org / Gitlab Shell","path":"gitlab-shell","path_with_namespace":"gitlab-org/gitlab-shell","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"builds_enabled":true,"snippets_enabled":false,"created_at":"2016-01-13T20:19:51.882Z","last_activity_at":"2016-01-13T20:27:35.678Z","shared_runners_enabled":true,"creator_id":1,"namespace":{"id":3,"name":"Gitlab Org","path":"gitlab-org","owner_id":null,"created_at":"2016-01-13T20:19:48.851Z","updated_at":"2016-01-13T20:19:48.851Z","description":"Magni mollitia quod quidem soluta nesciunt impedit.","avatar":{"url":null}},"avatar_url":null,"star_count":0,"forks_count":0,"open_issues_count":5,"permissions":{"project_access":{"access_level":20,"notification_level":3},"group_access":{"access_level":50,"notification_level":3}}},{"id":3,"description":"Excepturi molestiae quia repellendus omnis est illo illum eligendi.","default_branch":"master","tag_list":[],"public":true,"archived":false,"visibility_level":20,"ssh_url_to_repo":"phil@localhost:gitlab-org/gitlab-ci.git","http_url_to_repo":"http://localhost:3000/gitlab-org/gitlab-ci.git","web_url":"http://localhost:3000/gitlab-org/gitlab-ci","name":"Gitlab Ci","name_with_namespace":"Gitlab Org / Gitlab Ci","path":"gitlab-ci","path_with_namespace":"gitlab-org/gitlab-ci","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"builds_enabled":true,"snippets_enabled":false,"created_at":"2016-01-13T20:19:50.346Z","last_activity_at":"2016-01-13T20:27:30.115Z","shared_runners_enabled":true,"creator_id":1,"namespace":{"id":3,"name":"Gitlab Org","path":"gitlab-org","owner_id":null,"created_at":"2016-01-13T20:19:48.851Z","updated_at":"2016-01-13T20:19:48.851Z","description":"Magni mollitia quod quidem soluta nesciunt impedit.","avatar":{"url":null}},"avatar_url":null,"star_count":0,"forks_count":0,"open_issues_count":3,"permissions":{"project_access":null,"group_access":{"access_level":50,"notification_level":3}}},{"id":2,"description":"Adipisci quaerat dignissimos enim sed ipsam dolorem quia.","default_branch":"master","tag_list":[],"public":false,"archived":false,"visibility_level":10,"ssh_url_to_repo":"phil@localhost:gitlab-org/gitlab-ce.git","http_url_to_repo":"http://localhost:3000/gitlab-org/gitlab-ce.git","web_url":"http://localhost:3000/gitlab-org/gitlab-ce","name":"Gitlab Ce","name_with_namespace":"Gitlab Org / Gitlab Ce","path":"gitlab-ce","path_with_namespace":"gitlab-org/gitlab-ce","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"builds_enabled":true,"snippets_enabled":false,"created_at":"2016-01-13T20:19:49.065Z","last_activity_at":"2016-01-13T20:26:58.454Z","shared_runners_enabled":true,"creator_id":1,"namespace":{"id":3,"name":"Gitlab Org","path":"gitlab-org","owner_id":null,"created_at":"2016-01-13T20:19:48.851Z","updated_at":"2016-01-13T20:19:48.851Z","description":"Magni mollitia quod quidem soluta nesciunt impedit.","avatar":{"url":null}},"avatar_url":null,"star_count":0,"forks_count":0,"open_issues_count":5,"permissions":{"project_access":{"access_level":30,"notification_level":3},"group_access":{"access_level":50,"notification_level":3}}},{"id":1,"description":"Vel voluptatem maxime saepe ex quia.","default_branch":"master","tag_list":[],"public":false,"archived":false,"visibility_level":0,"ssh_url_to_repo":"phil@localhost:documentcloud/underscore.git","http_url_to_repo":"http://localhost:3000/documentcloud/underscore.git","web_url":"http://localhost:3000/documentcloud/underscore","name":"Underscore","name_with_namespace":"Documentcloud / Underscore","path":"underscore","path_with_namespace":"documentcloud/underscore","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"builds_enabled":true,"snippets_enabled":false,"created_at":"2016-01-13T20:19:45.862Z","last_activity_at":"2016-01-13T20:25:03.106Z","shared_runners_enabled":true,"creator_id":1,"namespace":{"id":2,"name":"Documentcloud","path":"documentcloud","owner_id":null,"created_at":"2016-01-13T20:19:44.464Z","updated_at":"2016-01-13T20:19:44.464Z","description":"Aut impedit perferendis fuga et ipsa repellat cupiditate et.","avatar":{"url":null}},"avatar_url":null,"star_count":0,"forks_count":0,"open_issues_count":5,"permissions":{"project_access":null,"group_access":{"access_level":50,"notification_level":3}}}]
+[{"id":9,"description":"","default_branch":null,"tag_list":[],"public":true,"archived":false,"visibility_level":20,"ssh_url_to_repo":"phil@localhost:root/test.git","http_url_to_repo":"http://localhost:3000/root/test.git","web_url":"http://localhost:3000/root/test","owner":{"name":"Administrator","username":"root","id":1,"state":"active","avatar_url":"http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon","web_url":"http://localhost:3000/u/root"},"name":"test","name_with_namespace":"Administrator / test","path":"test","path_with_namespace":"root/test","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"builds_enabled":true,"snippets_enabled":false,"created_at":"2016-01-14T19:08:05.364Z","last_activity_at":"2016-01-14T19:08:07.418Z","shared_runners_enabled":true,"creator_id":1,"namespace":{"id":1,"name":"root","path":"root","owner_id":1,"created_at":"2016-01-13T20:19:44.439Z","updated_at":"2016-01-13T20:19:44.439Z","description":"","avatar":null},"avatar_url":null,"star_count":0,"forks_count":0,"only_allow_merge_if_build_succeeds":false,"open_issues_count":0,"permissions":{"project_access":null,"group_access":null}},{"id":8,"description":"Voluptatem quae nulla eius numquam ullam voluptatibus quia modi.","default_branch":"master","tag_list":[],"public":false,"archived":false,"visibility_level":0,"ssh_url_to_repo":"phil@localhost:h5bp/html5-boilerplate.git","http_url_to_repo":"http://localhost:3000/h5bp/html5-boilerplate.git","web_url":"http://localhost:3000/h5bp/html5-boilerplate","name":"Html5 Boilerplate","name_with_namespace":"H5bp / Html5 Boilerplate","path":"html5-boilerplate","path_with_namespace":"h5bp/html5-boilerplate","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"builds_enabled":true,"snippets_enabled":false,"created_at":"2016-01-13T20:19:57.525Z","last_activity_at":"2016-01-13T20:27:57.280Z","shared_runners_enabled":true,"creator_id":1,"namespace":{"id":5,"name":"H5bp","path":"h5bp","owner_id":null,"created_at":"2016-01-13T20:19:57.239Z","updated_at":"2016-01-13T20:19:57.239Z","description":"Tempore accusantium possimus aut libero.","avatar":{"url":null}},"avatar_url":null,"star_count":0,"forks_count":0,"only_allow_merge_if_build_succeeds":false,"open_issues_count":5,"permissions":{"project_access":{"access_level":10,"notification_level":3},"group_access":{"access_level":50,"notification_level":3}}},{"id":7,"description":"Modi odio mollitia dolorem qui.","default_branch":"master","tag_list":[],"public":false,"archived":false,"visibility_level":0,"ssh_url_to_repo":"phil@localhost:twitter/typeahead-js.git","http_url_to_repo":"http://localhost:3000/twitter/typeahead-js.git","web_url":"http://localhost:3000/twitter/typeahead-js","name":"Typeahead.Js","name_with_namespace":"Twitter / Typeahead.Js","path":"typeahead-js","path_with_namespace":"twitter/typeahead-js","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"builds_enabled":true,"snippets_enabled":false,"created_at":"2016-01-13T20:19:56.212Z","last_activity_at":"2016-01-13T20:27:51.496Z","shared_runners_enabled":true,"creator_id":1,"namespace":{"id":4,"name":"Twitter","path":"twitter","owner_id":null,"created_at":"2016-01-13T20:19:54.480Z","updated_at":"2016-01-13T20:19:54.480Z","description":"Id voluptatem ipsa maiores omnis repudiandae et et.","avatar":{"url":null}},"avatar_url":null,"star_count":0,"forks_count":0,"only_allow_merge_if_build_succeeds":true,"open_issues_count":4,"permissions":{"project_access":null,"group_access":{"access_level":10,"notification_level":3}}},{"id":6,"description":"Omnis asperiores ipsa et beatae quidem necessitatibus quia.","default_branch":"master","tag_list":[],"public":true,"archived":false,"visibility_level":20,"ssh_url_to_repo":"phil@localhost:twitter/flight.git","http_url_to_repo":"http://localhost:3000/twitter/flight.git","web_url":"http://localhost:3000/twitter/flight","name":"Flight","name_with_namespace":"Twitter / Flight","path":"flight","path_with_namespace":"twitter/flight","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"builds_enabled":true,"snippets_enabled":false,"created_at":"2016-01-13T20:19:54.754Z","last_activity_at":"2016-01-13T20:27:50.502Z","shared_runners_enabled":true,"creator_id":1,"namespace":{"id":4,"name":"Twitter","path":"twitter","owner_id":null,"created_at":"2016-01-13T20:19:54.480Z","updated_at":"2016-01-13T20:19:54.480Z","description":"Id voluptatem ipsa maiores omnis repudiandae et et.","avatar":{"url":null}},"avatar_url":null,"star_count":0,"forks_count":0,"only_allow_merge_if_build_succeeds":true,"open_issues_count":4,"permissions":{"project_access":null,"group_access":{"access_level":10,"notification_level":3}}},{"id":5,"description":"Voluptatem commodi voluptate placeat architecto beatae illum dolores fugiat.","default_branch":"master","tag_list":[],"public":false,"archived":false,"visibility_level":0,"ssh_url_to_repo":"phil@localhost:gitlab-org/gitlab-test.git","http_url_to_repo":"http://localhost:3000/gitlab-org/gitlab-test.git","web_url":"http://localhost:3000/gitlab-org/gitlab-test","name":"Gitlab Test","name_with_namespace":"Gitlab Org / Gitlab Test","path":"gitlab-test","path_with_namespace":"gitlab-org/gitlab-test","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"builds_enabled":true,"snippets_enabled":false,"created_at":"2016-01-13T20:19:53.202Z","last_activity_at":"2016-01-13T20:27:41.626Z","shared_runners_enabled":true,"creator_id":1,"namespace":{"id":3,"name":"Gitlab Org","path":"gitlab-org","owner_id":null,"created_at":"2016-01-13T20:19:48.851Z","updated_at":"2016-01-13T20:19:48.851Z","description":"Magni mollitia quod quidem soluta nesciunt impedit.","avatar":{"url":null}},"avatar_url":null,"star_count":0,"forks_count":0,"only_allow_merge_if_build_succeeds":false,"open_issues_count":5,"permissions":{"project_access":null,"group_access":{"access_level":50,"notification_level":3}}},{"id":4,"description":"Aut molestias quas est ut aperiam officia quod libero.","default_branch":"master","tag_list":[],"public":true,"archived":false,"visibility_level":20,"ssh_url_to_repo":"phil@localhost:gitlab-org/gitlab-shell.git","http_url_to_repo":"http://localhost:3000/gitlab-org/gitlab-shell.git","web_url":"http://localhost:3000/gitlab-org/gitlab-shell","name":"Gitlab Shell","name_with_namespace":"Gitlab Org / Gitlab Shell","path":"gitlab-shell","path_with_namespace":"gitlab-org/gitlab-shell","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"builds_enabled":true,"snippets_enabled":false,"created_at":"2016-01-13T20:19:51.882Z","last_activity_at":"2016-01-13T20:27:35.678Z","shared_runners_enabled":true,"creator_id":1,"namespace":{"id":3,"name":"Gitlab Org","path":"gitlab-org","owner_id":null,"created_at":"2016-01-13T20:19:48.851Z","updated_at":"2016-01-13T20:19:48.851Z","description":"Magni mollitia quod quidem soluta nesciunt impedit.","avatar":{"url":null}},"avatar_url":null,"star_count":0,"forks_count":0,"only_allow_merge_if_build_succeeds":false,"open_issues_count":5,"permissions":{"project_access":{"access_level":20,"notification_level":3},"group_access":{"access_level":50,"notification_level":3}}},{"id":3,"description":"Excepturi molestiae quia repellendus omnis est illo illum eligendi.","default_branch":"master","tag_list":[],"public":true,"archived":false,"visibility_level":20,"ssh_url_to_repo":"phil@localhost:gitlab-org/gitlab-ci.git","http_url_to_repo":"http://localhost:3000/gitlab-org/gitlab-ci.git","web_url":"http://localhost:3000/gitlab-org/gitlab-ci","name":"Gitlab Ci","name_with_namespace":"Gitlab Org / Gitlab Ci","path":"gitlab-ci","path_with_namespace":"gitlab-org/gitlab-ci","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"builds_enabled":true,"snippets_enabled":false,"created_at":"2016-01-13T20:19:50.346Z","last_activity_at":"2016-01-13T20:27:30.115Z","shared_runners_enabled":true,"creator_id":1,"namespace":{"id":3,"name":"Gitlab Org","path":"gitlab-org","owner_id":null,"created_at":"2016-01-13T20:19:48.851Z","updated_at":"2016-01-13T20:19:48.851Z","description":"Magni mollitia quod quidem soluta nesciunt impedit.","avatar":{"url":null}},"avatar_url":null,"star_count":0,"forks_count":0,"only_allow_merge_if_build_succeeds":false,"open_issues_count":3,"permissions":{"project_access":null,"group_access":{"access_level":50,"notification_level":3}}},{"id":2,"description":"Adipisci quaerat dignissimos enim sed ipsam dolorem quia.","default_branch":"master","tag_list":[],"public":false,"archived":false,"visibility_level":10,"ssh_url_to_repo":"phil@localhost:gitlab-org/gitlab-ce.git","http_url_to_repo":"http://localhost:3000/gitlab-org/gitlab-ce.git","web_url":"http://localhost:3000/gitlab-org/gitlab-ce","name":"Gitlab Ce","name_with_namespace":"Gitlab Org / Gitlab Ce","path":"gitlab-ce","path_with_namespace":"gitlab-org/gitlab-ce","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"builds_enabled":true,"snippets_enabled":false,"created_at":"2016-01-13T20:19:49.065Z","last_activity_at":"2016-01-13T20:26:58.454Z","shared_runners_enabled":true,"creator_id":1,"namespace":{"id":3,"name":"Gitlab Org","path":"gitlab-org","owner_id":null,"created_at":"2016-01-13T20:19:48.851Z","updated_at":"2016-01-13T20:19:48.851Z","description":"Magni mollitia quod quidem soluta nesciunt impedit.","avatar":{"url":null}},"avatar_url":null,"star_count":0,"forks_count":0,"only_allow_merge_if_build_succeeds":false,"open_issues_count":5,"permissions":{"project_access":{"access_level":30,"notification_level":3},"group_access":{"access_level":50,"notification_level":3}}},{"id":1,"description":"Vel voluptatem maxime saepe ex quia.","default_branch":"master","tag_list":[],"public":false,"archived":false,"visibility_level":0,"ssh_url_to_repo":"phil@localhost:documentcloud/underscore.git","http_url_to_repo":"http://localhost:3000/documentcloud/underscore.git","web_url":"http://localhost:3000/documentcloud/underscore","name":"Underscore","name_with_namespace":"Documentcloud / Underscore","path":"underscore","path_with_namespace":"documentcloud/underscore","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"builds_enabled":true,"snippets_enabled":false,"created_at":"2016-01-13T20:19:45.862Z","last_activity_at":"2016-01-13T20:25:03.106Z","shared_runners_enabled":true,"creator_id":1,"namespace":{"id":2,"name":"Documentcloud","path":"documentcloud","owner_id":null,"created_at":"2016-01-13T20:19:44.464Z","updated_at":"2016-01-13T20:19:44.464Z","description":"Aut impedit perferendis fuga et ipsa repellat cupiditate et.","avatar":{"url":null}},"avatar_url":null,"star_count":0,"forks_count":0,"only_allow_merge_if_build_succeeds":false,"open_issues_count":5,"permissions":{"project_access":null,"group_access":{"access_level":50,"notification_level":3}}}]
diff --git a/spec/javascripts/gl_dropdown_spec.js.es6 b/spec/javascripts/gl_dropdown_spec.js.es6
new file mode 100644
index 00000000000..b529ea6458d
--- /dev/null
+++ b/spec/javascripts/gl_dropdown_spec.js.es6
@@ -0,0 +1,119 @@
+/*= require jquery */
+/*= require gl_dropdown */
+/*= require turbolinks */
+/*= require lib/utils/common_utils */
+/*= require lib/utils/type_utility */
+
+(() => {
+ const NON_SELECTABLE_CLASSES = '.divider, .separator, .dropdown-header, .dropdown-menu-empty-link';
+ const ITEM_SELECTOR = `.dropdown-content li:not(${NON_SELECTABLE_CLASSES})`;
+ const FOCUSED_ITEM_SELECTOR = `${ITEM_SELECTOR} a.is-focused`;
+
+ const ARROW_KEYS = {
+ DOWN: 40,
+ UP: 38,
+ ENTER: 13,
+ ESC: 27
+ };
+
+ let navigateWithKeys = function navigateWithKeys(direction, steps, cb, i) {
+ i = i || 0;
+ if (!i) direction = direction.toUpperCase();
+ $('body').trigger({
+ type: 'keydown',
+ which: ARROW_KEYS[direction],
+ keyCode: ARROW_KEYS[direction]
+ });
+ i++;
+ if (i <= steps) {
+ navigateWithKeys(direction, steps, cb, i);
+ } else {
+ cb();
+ }
+ };
+
+ describe('Dropdown', function describeDropdown() {
+ fixture.preload('gl_dropdown.html');
+ fixture.preload('projects.json');
+
+ beforeEach(() => {
+ fixture.load('gl_dropdown.html');
+ this.dropdownContainerElement = $('.dropdown.inline');
+ this.dropdownMenuElement = $('.dropdown-menu', this.dropdownContainerElement);
+ this.projectsData = fixture.load('projects.json')[0];
+ this.dropdownButtonElement = $('#js-project-dropdown', this.dropdownContainerElement).glDropdown({
+ selectable: true,
+ data: this.projectsData,
+ text: (project) => {
+ (project.name_with_namespace || project.name);
+ },
+ id: (project) => {
+ project.id;
+ }
+ });
+ });
+
+ afterEach(() => {
+ $('body').unbind('keydown');
+ this.dropdownContainerElement.unbind('keyup');
+ });
+
+ it('should open on click', () => {
+ expect(this.dropdownContainerElement).not.toHaveClass('open');
+ this.dropdownButtonElement.click();
+ expect(this.dropdownContainerElement).toHaveClass('open');
+ });
+
+ describe('that is open', () => {
+ beforeEach(() => {
+ this.dropdownButtonElement.click();
+ });
+
+ it('should select a following item on DOWN keypress', () => {
+ expect($(FOCUSED_ITEM_SELECTOR, this.dropdownMenuElement).length).toBe(0);
+ let randomIndex = (Math.floor(Math.random() * (this.projectsData.length - 1)) + 0);
+ navigateWithKeys('down', randomIndex, () => {
+ expect($(FOCUSED_ITEM_SELECTOR, this.dropdownMenuElement).length).toBe(1);
+ expect($(`${ITEM_SELECTOR}:eq(${randomIndex}) a`, this.dropdownMenuElement)).toHaveClass('is-focused');
+ });
+ });
+
+ it('should select a previous item on UP keypress', () => {
+ expect($(FOCUSED_ITEM_SELECTOR, this.dropdownMenuElement).length).toBe(0);
+ navigateWithKeys('down', (this.projectsData.length - 1), () => {
+ expect($(FOCUSED_ITEM_SELECTOR, this.dropdownMenuElement).length).toBe(1);
+ let randomIndex = (Math.floor(Math.random() * (this.projectsData.length - 2)) + 0);
+ navigateWithKeys('up', randomIndex, () => {
+ expect($(FOCUSED_ITEM_SELECTOR, this.dropdownMenuElement).length).toBe(1);
+ expect($(`${ITEM_SELECTOR}:eq(${((this.projectsData.length - 2) - randomIndex)}) a`, this.dropdownMenuElement)).toHaveClass('is-focused');
+ });
+ });
+ });
+
+ it('should click the selected item on ENTER keypress', () => {
+ expect(this.dropdownContainerElement).toHaveClass('open')
+ let randomIndex = Math.floor(Math.random() * (this.projectsData.length - 1)) + 0
+ navigateWithKeys('down', randomIndex, () => {
+ spyOn(Turbolinks, 'visit').and.stub();
+ navigateWithKeys('enter', null, () => {
+ expect(this.dropdownContainerElement).not.toHaveClass('open');
+ let link = $(`${ITEM_SELECTOR}:eq(${randomIndex}) a`, this.dropdownMenuElement);
+ expect(link).toHaveClass('is-active');
+ let linkedLocation = link.attr('href');
+ if (linkedLocation && linkedLocation !== '#') expect(Turbolinks.visit).toHaveBeenCalledWith(linkedLocation);
+ });
+ });
+ });
+
+ it('should close on ESC keypress', () => {
+ expect(this.dropdownContainerElement).toHaveClass('open');
+ this.dropdownContainerElement.trigger({
+ type: 'keyup',
+ which: ARROW_KEYS.ESC,
+ keyCode: ARROW_KEYS.ESC
+ });
+ expect(this.dropdownContainerElement).not.toHaveClass('open');
+ });
+ });
+ });
+})();
diff --git a/spec/javascripts/issue_spec.js b/spec/javascripts/issue_spec.js
new file mode 100644
index 00000000000..dc6231ebb38
--- /dev/null
+++ b/spec/javascripts/issue_spec.js
@@ -0,0 +1,121 @@
+
+/*= require lib/utils/text_utility */
+
+
+/*= require issue */
+
+(function() {
+ describe('Issue', function() {
+ return describe('task lists', function() {
+ fixture.preload('issues_show.html');
+ beforeEach(function() {
+ fixture.load('issues_show.html');
+ return this.issue = new Issue();
+ });
+ it('modifies the Markdown field', function() {
+ spyOn(jQuery, 'ajax').and.stub();
+ $('input[type=checkbox]').attr('checked', true).trigger('change');
+ return expect($('.js-task-list-field').val()).toBe('- [x] Task List Item');
+ });
+ return it('submits an ajax request on tasklist:changed', function() {
+ spyOn(jQuery, 'ajax').and.callFake(function(req) {
+ expect(req.type).toBe('PATCH');
+ expect(req.url).toBe('/foo');
+ return expect(req.data.issue.description).not.toBe(null);
+ });
+ return $('.js-task-list-field').trigger('tasklist:changed');
+ });
+ });
+ });
+
+ describe('reopen/close issue', function() {
+ fixture.preload('issues_show.html');
+ beforeEach(function() {
+ fixture.load('issues_show.html');
+ return this.issue = new Issue();
+ });
+ it('closes an issue', function() {
+ var $btnClose, $btnReopen;
+ spyOn(jQuery, 'ajax').and.callFake(function(req) {
+ expect(req.type).toBe('PUT');
+ expect(req.url).toBe('http://gitlab.com/issues/6/close');
+ return req.success({
+ id: 34
+ });
+ });
+ $btnClose = $('a.btn-close');
+ $btnReopen = $('a.btn-reopen');
+ expect($btnReopen).toBeHidden();
+ expect($btnClose.text()).toBe('Close');
+ expect(typeof $btnClose.prop('disabled')).toBe('undefined');
+ $btnClose.trigger('click');
+ expect($btnReopen).toBeVisible();
+ expect($btnClose).toBeHidden();
+ expect($('div.status-box-closed')).toBeVisible();
+ return expect($('div.status-box-open')).toBeHidden();
+ });
+ it('fails to close an issue with success:false', function() {
+ var $btnClose, $btnReopen;
+ spyOn(jQuery, 'ajax').and.callFake(function(req) {
+ expect(req.type).toBe('PUT');
+ expect(req.url).toBe('http://goesnowhere.nothing/whereami');
+ return req.success({
+ saved: false
+ });
+ });
+ $btnClose = $('a.btn-close');
+ $btnReopen = $('a.btn-reopen');
+ $btnClose.attr('href', 'http://goesnowhere.nothing/whereami');
+ expect($btnReopen).toBeHidden();
+ expect($btnClose.text()).toBe('Close');
+ expect(typeof $btnClose.prop('disabled')).toBe('undefined');
+ $btnClose.trigger('click');
+ expect($btnReopen).toBeHidden();
+ expect($btnClose).toBeVisible();
+ expect($('div.status-box-closed')).toBeHidden();
+ expect($('div.status-box-open')).toBeVisible();
+ expect($('div.flash-alert')).toBeVisible();
+ return expect($('div.flash-alert').text()).toBe('Unable to update this issue at this time.');
+ });
+ it('fails to closes an issue with HTTP error', function() {
+ var $btnClose, $btnReopen;
+ spyOn(jQuery, 'ajax').and.callFake(function(req) {
+ expect(req.type).toBe('PUT');
+ expect(req.url).toBe('http://goesnowhere.nothing/whereami');
+ return req.error();
+ });
+ $btnClose = $('a.btn-close');
+ $btnReopen = $('a.btn-reopen');
+ $btnClose.attr('href', 'http://goesnowhere.nothing/whereami');
+ expect($btnReopen).toBeHidden();
+ expect($btnClose.text()).toBe('Close');
+ expect(typeof $btnClose.prop('disabled')).toBe('undefined');
+ $btnClose.trigger('click');
+ expect($btnReopen).toBeHidden();
+ expect($btnClose).toBeVisible();
+ expect($('div.status-box-closed')).toBeHidden();
+ expect($('div.status-box-open')).toBeVisible();
+ expect($('div.flash-alert')).toBeVisible();
+ return expect($('div.flash-alert').text()).toBe('Unable to update this issue at this time.');
+ });
+ return it('reopens an issue', function() {
+ var $btnClose, $btnReopen;
+ spyOn(jQuery, 'ajax').and.callFake(function(req) {
+ expect(req.type).toBe('PUT');
+ expect(req.url).toBe('http://gitlab.com/issues/6/reopen');
+ return req.success({
+ id: 34
+ });
+ });
+ $btnClose = $('a.btn-close');
+ $btnReopen = $('a.btn-reopen');
+ expect($btnReopen.text()).toBe('Reopen');
+ $btnReopen.trigger('click');
+ expect($btnReopen).toBeHidden();
+ expect($btnClose).toBeVisible();
+ expect($('div.status-box-open')).toBeVisible();
+ return expect($('div.status-box-closed')).toBeHidden();
+ });
+ });
+
+}).call(this);
diff --git a/spec/javascripts/issue_spec.js.coffee b/spec/javascripts/issue_spec.js.coffee
deleted file mode 100644
index d84d80f266b..00000000000
--- a/spec/javascripts/issue_spec.js.coffee
+++ /dev/null
@@ -1,109 +0,0 @@
-#= require lib/utils/text_utility
-#= require issue
-
-describe 'Issue', ->
- describe 'task lists', ->
- fixture.preload('issues_show.html')
-
- beforeEach ->
- fixture.load('issues_show.html')
- @issue = new Issue()
-
- it 'modifies the Markdown field', ->
- spyOn(jQuery, 'ajax').and.stub()
- $('input[type=checkbox]').attr('checked', true).trigger('change')
- expect($('.js-task-list-field').val()).toBe('- [x] Task List Item')
-
- it 'submits an ajax request on tasklist:changed', ->
- spyOn(jQuery, 'ajax').and.callFake (req) ->
- expect(req.type).toBe('PATCH')
- expect(req.url).toBe('/foo')
- expect(req.data.issue.description).not.toBe(null)
-
- $('.js-task-list-field').trigger('tasklist:changed')
-describe 'reopen/close issue', ->
- fixture.preload('issues_show.html')
- beforeEach ->
- fixture.load('issues_show.html')
- @issue = new Issue()
- it 'closes an issue', ->
- spyOn(jQuery, 'ajax').and.callFake (req) ->
- expect(req.type).toBe('PUT')
- expect(req.url).toBe('http://gitlab.com/issues/6/close')
- req.success id: 34
-
- $btnClose = $('a.btn-close')
- $btnReopen = $('a.btn-reopen')
- expect($btnReopen).toBeHidden()
- expect($btnClose.text()).toBe('Close')
- expect(typeof $btnClose.prop('disabled')).toBe('undefined')
-
- $btnClose.trigger('click')
-
- expect($btnReopen).toBeVisible()
- expect($btnClose).toBeHidden()
- expect($('div.status-box-closed')).toBeVisible()
- expect($('div.status-box-open')).toBeHidden()
-
- it 'fails to close an issue with success:false', ->
-
- spyOn(jQuery, 'ajax').and.callFake (req) ->
- expect(req.type).toBe('PUT')
- expect(req.url).toBe('http://goesnowhere.nothing/whereami')
- req.success saved: false
-
- $btnClose = $('a.btn-close')
- $btnReopen = $('a.btn-reopen')
- $btnClose.attr('href','http://goesnowhere.nothing/whereami')
- expect($btnReopen).toBeHidden()
- expect($btnClose.text()).toBe('Close')
- expect(typeof $btnClose.prop('disabled')).toBe('undefined')
-
- $btnClose.trigger('click')
-
- expect($btnReopen).toBeHidden()
- expect($btnClose).toBeVisible()
- expect($('div.status-box-closed')).toBeHidden()
- expect($('div.status-box-open')).toBeVisible()
- expect($('div.flash-alert')).toBeVisible()
- expect($('div.flash-alert').text()).toBe('Unable to update this issue at this time.')
-
- it 'fails to closes an issue with HTTP error', ->
-
- spyOn(jQuery, 'ajax').and.callFake (req) ->
- expect(req.type).toBe('PUT')
- expect(req.url).toBe('http://goesnowhere.nothing/whereami')
- req.error()
-
- $btnClose = $('a.btn-close')
- $btnReopen = $('a.btn-reopen')
- $btnClose.attr('href','http://goesnowhere.nothing/whereami')
- expect($btnReopen).toBeHidden()
- expect($btnClose.text()).toBe('Close')
- expect(typeof $btnClose.prop('disabled')).toBe('undefined')
-
- $btnClose.trigger('click')
-
- expect($btnReopen).toBeHidden()
- expect($btnClose).toBeVisible()
- expect($('div.status-box-closed')).toBeHidden()
- expect($('div.status-box-open')).toBeVisible()
- expect($('div.flash-alert')).toBeVisible()
- expect($('div.flash-alert').text()).toBe('Unable to update this issue at this time.')
-
- it 'reopens an issue', ->
- spyOn(jQuery, 'ajax').and.callFake (req) ->
- expect(req.type).toBe('PUT')
- expect(req.url).toBe('http://gitlab.com/issues/6/reopen')
- req.success id: 34
-
- $btnClose = $('a.btn-close')
- $btnReopen = $('a.btn-reopen')
- expect($btnReopen.text()).toBe('Reopen')
-
- $btnReopen.trigger('click')
-
- expect($btnReopen).toBeHidden()
- expect($btnClose).toBeVisible()
- expect($('div.status-box-open')).toBeVisible()
- expect($('div.status-box-closed')).toBeHidden()
diff --git a/spec/javascripts/labels_issue_sidebar_spec.js.es6 b/spec/javascripts/labels_issue_sidebar_spec.js.es6
new file mode 100644
index 00000000000..840c7b6d015
--- /dev/null
+++ b/spec/javascripts/labels_issue_sidebar_spec.js.es6
@@ -0,0 +1,89 @@
+//= require lib/utils/type_utility
+//= require jquery
+//= require bootstrap
+//= require gl_dropdown
+//= require select2
+//= require jquery.nicescroll
+//= require api
+//= require create_label
+//= require issuable_context
+//= require users_select
+//= require labels_select
+
+(() => {
+ let saveLabelCount = 0;
+ describe('Issue dropdown sidebar', () => {
+ fixture.preload('issue_sidebar_label.html');
+
+ beforeEach(() => {
+ fixture.load('issue_sidebar_label.html');
+ new IssuableContext('{"id":1,"name":"Administrator","username":"root"}');
+ new LabelsSelect();
+
+ spyOn(jQuery, 'ajax').and.callFake((req) => {
+ const d = $.Deferred();
+ let LABELS_DATA = []
+
+ if (req.url === '/root/test/labels.json') {
+ for (let i = 0; i < 10; i++) {
+ LABELS_DATA.push({id: i, title: `test ${i}`, color: '#5CB85C'});
+ }
+ } else if (req.url === '/root/test/issues/2.json') {
+ let tmp = []
+ for (let i = 0; i < saveLabelCount; i++) {
+ tmp.push({id: i, title: `test ${i}`, color: '#5CB85C'});
+ }
+ LABELS_DATA = {labels: tmp};
+ }
+
+ d.resolve(LABELS_DATA);
+ return d.promise();
+ });
+ });
+
+ it('changes collapsed tooltip when changing labels when less than 5', (done) => {
+ saveLabelCount = 5;
+ $('.edit-link').get(0).click();
+
+ setTimeout(() => {
+ expect($('.dropdown-content a').length).toBe(10);
+
+ $('.dropdow-content a').each((i, $link) => {
+ if (i < 5) {
+ $link.get(0).click();
+ }
+ });
+
+ $('.edit-link').get(0).click();
+
+ setTimeout(() => {
+ expect($('.sidebar-collapsed-icon').attr('data-original-title')).toBe('test 0, test 1, test 2, test 3, test 4');
+ done();
+ }, 0);
+ }, 0);
+ });
+
+ it('changes collapsed tooltip when changing labels when more than 5', (done) => {
+ saveLabelCount = 6;
+ $('.edit-link').get(0).click();
+
+ setTimeout(() => {
+ expect($('.dropdown-content a').length).toBe(10);
+
+ $('.dropdow-content a').each((i, $link) => {
+ if (i < 5) {
+ $link.get(0).click();
+ }
+ });
+
+ $('.edit-link').get(0).click();
+
+ setTimeout(() => {
+ expect($('.sidebar-collapsed-icon').attr('data-original-title')).toBe('test 0, test 1, test 2, test 3, test 4, and 1 more');
+ done();
+ }, 0);
+ }, 0);
+ });
+ });
+})();
+
diff --git a/spec/javascripts/line_highlighter_spec.js b/spec/javascripts/line_highlighter_spec.js
new file mode 100644
index 00000000000..e2789571607
--- /dev/null
+++ b/spec/javascripts/line_highlighter_spec.js
@@ -0,0 +1,229 @@
+
+/*= require line_highlighter */
+
+(function() {
+ describe('LineHighlighter', function() {
+ var clickLine;
+ fixture.preload('line_highlighter.html');
+ clickLine = function(number, eventData) {
+ var e;
+ if (eventData == null) {
+ eventData = {};
+ }
+ if ($.isEmptyObject(eventData)) {
+ return $("#L" + number).mousedown().click();
+ } else {
+ e = $.Event('mousedown', eventData);
+ return $("#L" + number).trigger(e).click();
+ }
+ };
+ beforeEach(function() {
+ fixture.load('line_highlighter.html');
+ this["class"] = new LineHighlighter();
+ this.css = this["class"].highlightClass;
+ return this.spies = {
+ __setLocationHash__: spyOn(this["class"], '__setLocationHash__').and.callFake(function() {})
+ };
+ });
+ describe('behavior', function() {
+ it('highlights one line given in the URL hash', function() {
+ new LineHighlighter('#L13');
+ return expect($('#LC13')).toHaveClass(this.css);
+ });
+ it('highlights a range of lines given in the URL hash', function() {
+ var i, line, results;
+ new LineHighlighter('#L5-25');
+ expect($("." + this.css).length).toBe(21);
+ results = [];
+ for (line = i = 5; i <= 25; line = ++i) {
+ results.push(expect($("#LC" + line)).toHaveClass(this.css));
+ }
+ return results;
+ });
+ it('scrolls to the first highlighted line on initial load', function() {
+ var spy;
+ spy = spyOn($, 'scrollTo');
+ new LineHighlighter('#L5-25');
+ return expect(spy).toHaveBeenCalledWith('#L5', jasmine.anything());
+ });
+ it('discards click events', function() {
+ var spy;
+ spy = spyOnEvent('a[data-line-number]', 'click');
+ clickLine(13);
+ return expect(spy).toHaveBeenPrevented();
+ });
+ return it('handles garbage input from the hash', function() {
+ var func;
+ func = function() {
+ return new LineHighlighter('#blob-content-holder');
+ };
+ return expect(func).not.toThrow();
+ });
+ });
+ describe('#clickHandler', function() {
+ it('discards the mousedown event', function() {
+ var spy;
+ spy = spyOnEvent('a[data-line-number]', 'mousedown');
+ clickLine(13);
+ return expect(spy).toHaveBeenPrevented();
+ });
+ it('handles clicking on a child icon element', function() {
+ var spy;
+ spy = spyOn(this["class"], 'setHash').and.callThrough();
+ $('#L13 i').mousedown().click();
+ expect(spy).toHaveBeenCalledWith(13);
+ return expect($('#LC13')).toHaveClass(this.css);
+ });
+ describe('without shiftKey', function() {
+ it('highlights one line when clicked', function() {
+ clickLine(13);
+ return expect($('#LC13')).toHaveClass(this.css);
+ });
+ it('unhighlights previously highlighted lines', function() {
+ clickLine(13);
+ clickLine(20);
+ expect($('#LC13')).not.toHaveClass(this.css);
+ return expect($('#LC20')).toHaveClass(this.css);
+ });
+ return it('sets the hash', function() {
+ var spy;
+ spy = spyOn(this["class"], 'setHash').and.callThrough();
+ clickLine(13);
+ return expect(spy).toHaveBeenCalledWith(13);
+ });
+ });
+ return describe('with shiftKey', function() {
+ it('sets the hash', function() {
+ var spy;
+ spy = spyOn(this["class"], 'setHash').and.callThrough();
+ clickLine(13);
+ clickLine(20, {
+ shiftKey: true
+ });
+ expect(spy).toHaveBeenCalledWith(13);
+ return expect(spy).toHaveBeenCalledWith(13, 20);
+ });
+ describe('without existing highlight', function() {
+ it('highlights the clicked line', function() {
+ clickLine(13, {
+ shiftKey: true
+ });
+ expect($('#LC13')).toHaveClass(this.css);
+ return expect($("." + this.css).length).toBe(1);
+ });
+ return it('sets the hash', function() {
+ var spy;
+ spy = spyOn(this["class"], 'setHash');
+ clickLine(13, {
+ shiftKey: true
+ });
+ return expect(spy).toHaveBeenCalledWith(13);
+ });
+ });
+ describe('with existing single-line highlight', function() {
+ it('uses existing line as last line when target is lesser', function() {
+ var i, line, results;
+ clickLine(20);
+ clickLine(15, {
+ shiftKey: true
+ });
+ expect($("." + this.css).length).toBe(6);
+ results = [];
+ for (line = i = 15; i <= 20; line = ++i) {
+ results.push(expect($("#LC" + line)).toHaveClass(this.css));
+ }
+ return results;
+ });
+ return it('uses existing line as first line when target is greater', function() {
+ var i, line, results;
+ clickLine(5);
+ clickLine(10, {
+ shiftKey: true
+ });
+ expect($("." + this.css).length).toBe(6);
+ results = [];
+ for (line = i = 5; i <= 10; line = ++i) {
+ results.push(expect($("#LC" + line)).toHaveClass(this.css));
+ }
+ return results;
+ });
+ });
+ return describe('with existing multi-line highlight', function() {
+ beforeEach(function() {
+ clickLine(10, {
+ shiftKey: true
+ });
+ return clickLine(13, {
+ shiftKey: true
+ });
+ });
+ it('uses target as first line when it is less than existing first line', function() {
+ var i, line, results;
+ clickLine(5, {
+ shiftKey: true
+ });
+ expect($("." + this.css).length).toBe(6);
+ results = [];
+ for (line = i = 5; i <= 10; line = ++i) {
+ results.push(expect($("#LC" + line)).toHaveClass(this.css));
+ }
+ return results;
+ });
+ return it('uses target as last line when it is greater than existing first line', function() {
+ var i, line, results;
+ clickLine(15, {
+ shiftKey: true
+ });
+ expect($("." + this.css).length).toBe(6);
+ results = [];
+ for (line = i = 10; i <= 15; line = ++i) {
+ results.push(expect($("#LC" + line)).toHaveClass(this.css));
+ }
+ return results;
+ });
+ });
+ });
+ });
+ describe('#hashToRange', function() {
+ beforeEach(function() {
+ return this.subject = this["class"].hashToRange;
+ });
+ it('extracts a single line number from the hash', function() {
+ return expect(this.subject('#L5')).toEqual([5, null]);
+ });
+ it('extracts a range of line numbers from the hash', function() {
+ return expect(this.subject('#L5-15')).toEqual([5, 15]);
+ });
+ return it('returns [null, null] when the hash is not a line number', function() {
+ return expect(this.subject('#foo')).toEqual([null, null]);
+ });
+ });
+ describe('#highlightLine', function() {
+ beforeEach(function() {
+ return this.subject = this["class"].highlightLine;
+ });
+ it('highlights the specified line', function() {
+ this.subject(13);
+ return expect($('#LC13')).toHaveClass(this.css);
+ });
+ return it('accepts a String-based number', function() {
+ this.subject('13');
+ return expect($('#LC13')).toHaveClass(this.css);
+ });
+ });
+ return describe('#setHash', function() {
+ beforeEach(function() {
+ return this.subject = this["class"].setHash;
+ });
+ it('sets the location hash for a single line', function() {
+ this.subject(5);
+ return expect(this.spies.__setLocationHash__).toHaveBeenCalledWith('#L5');
+ });
+ return it('sets the location hash for a range', function() {
+ this.subject(5, 15);
+ return expect(this.spies.__setLocationHash__).toHaveBeenCalledWith('#L5-15');
+ });
+ });
+ });
+
+}).call(this);
diff --git a/spec/javascripts/line_highlighter_spec.js.coffee b/spec/javascripts/line_highlighter_spec.js.coffee
deleted file mode 100644
index a073f21e7bc..00000000000
--- a/spec/javascripts/line_highlighter_spec.js.coffee
+++ /dev/null
@@ -1,158 +0,0 @@
-#= require line_highlighter
-
-describe 'LineHighlighter', ->
- fixture.preload('line_highlighter.html')
-
- clickLine = (number, eventData = {}) ->
- if $.isEmptyObject(eventData)
- $("#L#{number}").mousedown().click()
- else
- e = $.Event 'mousedown', eventData
- $("#L#{number}").trigger(e).click()
-
- beforeEach ->
- fixture.load('line_highlighter.html')
- @class = new LineHighlighter()
- @css = @class.highlightClass
- @spies = {
- __setLocationHash__: spyOn(@class, '__setLocationHash__').and.callFake ->
- }
-
- describe 'behavior', ->
- it 'highlights one line given in the URL hash', ->
- new LineHighlighter('#L13')
- expect($('#LC13')).toHaveClass(@css)
-
- it 'highlights a range of lines given in the URL hash', ->
- new LineHighlighter('#L5-25')
- expect($(".#{@css}").length).toBe(21)
- expect($("#LC#{line}")).toHaveClass(@css) for line in [5..25]
-
- it 'scrolls to the first highlighted line on initial load', ->
- spy = spyOn($, 'scrollTo')
- new LineHighlighter('#L5-25')
- expect(spy).toHaveBeenCalledWith('#L5', jasmine.anything())
-
- it 'discards click events', ->
- spy = spyOnEvent('a[data-line-number]', 'click')
- clickLine(13)
- expect(spy).toHaveBeenPrevented()
-
- it 'handles garbage input from the hash', ->
- func = -> new LineHighlighter('#blob-content-holder')
- expect(func).not.toThrow()
-
- describe '#clickHandler', ->
- it 'discards the mousedown event', ->
- spy = spyOnEvent('a[data-line-number]', 'mousedown')
- clickLine(13)
- expect(spy).toHaveBeenPrevented()
-
- it 'handles clicking on a child icon element', ->
- spy = spyOn(@class, 'setHash').and.callThrough()
-
- $('#L13 i').mousedown().click()
-
- expect(spy).toHaveBeenCalledWith(13)
- expect($('#LC13')).toHaveClass(@css)
-
- describe 'without shiftKey', ->
- it 'highlights one line when clicked', ->
- clickLine(13)
- expect($('#LC13')).toHaveClass(@css)
-
- it 'unhighlights previously highlighted lines', ->
- clickLine(13)
- clickLine(20)
-
- expect($('#LC13')).not.toHaveClass(@css)
- expect($('#LC20')).toHaveClass(@css)
-
- it 'sets the hash', ->
- spy = spyOn(@class, 'setHash').and.callThrough()
- clickLine(13)
- expect(spy).toHaveBeenCalledWith(13)
-
- describe 'with shiftKey', ->
- it 'sets the hash', ->
- spy = spyOn(@class, 'setHash').and.callThrough()
- clickLine(13)
- clickLine(20, shiftKey: true)
- expect(spy).toHaveBeenCalledWith(13)
- expect(spy).toHaveBeenCalledWith(13, 20)
-
- describe 'without existing highlight', ->
- it 'highlights the clicked line', ->
- clickLine(13, shiftKey: true)
- expect($('#LC13')).toHaveClass(@css)
- expect($(".#{@css}").length).toBe(1)
-
- it 'sets the hash', ->
- spy = spyOn(@class, 'setHash')
- clickLine(13, shiftKey: true)
- expect(spy).toHaveBeenCalledWith(13)
-
- describe 'with existing single-line highlight', ->
- it 'uses existing line as last line when target is lesser', ->
- clickLine(20)
- clickLine(15, shiftKey: true)
- expect($(".#{@css}").length).toBe(6)
- expect($("#LC#{line}")).toHaveClass(@css) for line in [15..20]
-
- it 'uses existing line as first line when target is greater', ->
- clickLine(5)
- clickLine(10, shiftKey: true)
- expect($(".#{@css}").length).toBe(6)
- expect($("#LC#{line}")).toHaveClass(@css) for line in [5..10]
-
- describe 'with existing multi-line highlight', ->
- beforeEach ->
- clickLine(10, shiftKey: true)
- clickLine(13, shiftKey: true)
-
- it 'uses target as first line when it is less than existing first line', ->
- clickLine(5, shiftKey: true)
- expect($(".#{@css}").length).toBe(6)
- expect($("#LC#{line}")).toHaveClass(@css) for line in [5..10]
-
- it 'uses target as last line when it is greater than existing first line', ->
- clickLine(15, shiftKey: true)
- expect($(".#{@css}").length).toBe(6)
- expect($("#LC#{line}")).toHaveClass(@css) for line in [10..15]
-
- describe '#hashToRange', ->
- beforeEach ->
- @subject = @class.hashToRange
-
- it 'extracts a single line number from the hash', ->
- expect(@subject('#L5')).toEqual([5, null])
-
- it 'extracts a range of line numbers from the hash', ->
- expect(@subject('#L5-15')).toEqual([5, 15])
-
- it 'returns [null, null] when the hash is not a line number', ->
- expect(@subject('#foo')).toEqual([null, null])
-
- describe '#highlightLine', ->
- beforeEach ->
- @subject = @class.highlightLine
-
- it 'highlights the specified line', ->
- @subject(13)
- expect($('#LC13')).toHaveClass(@css)
-
- it 'accepts a String-based number', ->
- @subject('13')
- expect($('#LC13')).toHaveClass(@css)
-
- describe '#setHash', ->
- beforeEach ->
- @subject = @class.setHash
-
- it 'sets the location hash for a single line', ->
- @subject(5)
- expect(@spies.__setLocationHash__).toHaveBeenCalledWith('#L5')
-
- it 'sets the location hash for a range', ->
- @subject(5, 15)
- expect(@spies.__setLocationHash__).toHaveBeenCalledWith('#L5-15')
diff --git a/spec/javascripts/merge_request_spec.js b/spec/javascripts/merge_request_spec.js
new file mode 100644
index 00000000000..61830d267a9
--- /dev/null
+++ b/spec/javascripts/merge_request_spec.js
@@ -0,0 +1,28 @@
+
+/*= require merge_request */
+
+(function() {
+ describe('MergeRequest', function() {
+ return describe('task lists', function() {
+ fixture.preload('merge_requests_show.html');
+ beforeEach(function() {
+ fixture.load('merge_requests_show.html');
+ return this.merge = new MergeRequest();
+ });
+ it('modifies the Markdown field', function() {
+ spyOn(jQuery, 'ajax').and.stub();
+ $('input[type=checkbox]').attr('checked', true).trigger('change');
+ return expect($('.js-task-list-field').val()).toBe('- [x] Task List Item');
+ });
+ return it('submits an ajax request on tasklist:changed', function() {
+ spyOn(jQuery, 'ajax').and.callFake(function(req) {
+ expect(req.type).toBe('PATCH');
+ expect(req.url).toBe('/foo');
+ return expect(req.data.merge_request.description).not.toBe(null);
+ });
+ return $('.js-task-list-field').trigger('tasklist:changed');
+ });
+ });
+ });
+
+}).call(this);
diff --git a/spec/javascripts/merge_request_spec.js.coffee b/spec/javascripts/merge_request_spec.js.coffee
deleted file mode 100644
index 3cb67d51c85..00000000000
--- a/spec/javascripts/merge_request_spec.js.coffee
+++ /dev/null
@@ -1,23 +0,0 @@
-#= require merge_request
-
-describe 'MergeRequest', ->
- describe 'task lists', ->
- fixture.preload('merge_requests_show.html')
-
- beforeEach ->
- fixture.load('merge_requests_show.html')
- @merge = new MergeRequest()
-
- it 'modifies the Markdown field', ->
- spyOn(jQuery, 'ajax').and.stub()
-
- $('input[type=checkbox]').attr('checked', true).trigger('change')
- expect($('.js-task-list-field').val()).toBe('- [x] Task List Item')
-
- it 'submits an ajax request on tasklist:changed', ->
- spyOn(jQuery, 'ajax').and.callFake (req) ->
- expect(req.type).toBe('PATCH')
- expect(req.url).toBe('/foo')
- expect(req.data.merge_request.description).not.toBe(null)
-
- $('.js-task-list-field').trigger('tasklist:changed')
diff --git a/spec/javascripts/merge_request_tabs_spec.js b/spec/javascripts/merge_request_tabs_spec.js
new file mode 100644
index 00000000000..395032a7416
--- /dev/null
+++ b/spec/javascripts/merge_request_tabs_spec.js
@@ -0,0 +1,106 @@
+
+/*= require merge_request_tabs */
+
+(function() {
+ describe('MergeRequestTabs', function() {
+ var stubLocation;
+ stubLocation = function(stubs) {
+ var defaults;
+ defaults = {
+ pathname: '',
+ search: '',
+ hash: ''
+ };
+ return $.extend(defaults, stubs);
+ };
+ fixture.preload('merge_request_tabs.html');
+ beforeEach(function() {
+ this["class"] = new MergeRequestTabs();
+ return this.spies = {
+ ajax: spyOn($, 'ajax').and.callFake(function() {}),
+ history: spyOn(history, 'replaceState').and.callFake(function() {})
+ };
+ });
+ describe('#activateTab', function() {
+ beforeEach(function() {
+ fixture.load('merge_request_tabs.html');
+ return this.subject = this["class"].activateTab;
+ });
+ it('shows the first tab when action is show', function() {
+ this.subject('show');
+ return expect($('#notes')).toHaveClass('active');
+ });
+ it('shows the notes tab when action is notes', function() {
+ this.subject('notes');
+ return expect($('#notes')).toHaveClass('active');
+ });
+ it('shows the commits tab when action is commits', function() {
+ this.subject('commits');
+ return expect($('#commits')).toHaveClass('active');
+ });
+ return it('shows the diffs tab when action is diffs', function() {
+ this.subject('diffs');
+ return expect($('#diffs')).toHaveClass('active');
+ });
+ });
+ return describe('#setCurrentAction', function() {
+ beforeEach(function() {
+ return this.subject = this["class"].setCurrentAction;
+ });
+ it('changes from commits', function() {
+ this["class"]._location = stubLocation({
+ pathname: '/foo/bar/merge_requests/1/commits'
+ });
+ expect(this.subject('notes')).toBe('/foo/bar/merge_requests/1');
+ return expect(this.subject('diffs')).toBe('/foo/bar/merge_requests/1/diffs');
+ });
+ it('changes from diffs', function() {
+ this["class"]._location = stubLocation({
+ pathname: '/foo/bar/merge_requests/1/diffs'
+ });
+ expect(this.subject('notes')).toBe('/foo/bar/merge_requests/1');
+ return expect(this.subject('commits')).toBe('/foo/bar/merge_requests/1/commits');
+ });
+ it('changes from diffs.html', function() {
+ this["class"]._location = stubLocation({
+ pathname: '/foo/bar/merge_requests/1/diffs.html'
+ });
+ expect(this.subject('notes')).toBe('/foo/bar/merge_requests/1');
+ return expect(this.subject('commits')).toBe('/foo/bar/merge_requests/1/commits');
+ });
+ it('changes from notes', function() {
+ this["class"]._location = stubLocation({
+ pathname: '/foo/bar/merge_requests/1'
+ });
+ expect(this.subject('diffs')).toBe('/foo/bar/merge_requests/1/diffs');
+ return expect(this.subject('commits')).toBe('/foo/bar/merge_requests/1/commits');
+ });
+ it('includes search parameters and hash string', function() {
+ this["class"]._location = stubLocation({
+ pathname: '/foo/bar/merge_requests/1/diffs',
+ search: '?view=parallel',
+ hash: '#L15-35'
+ });
+ return expect(this.subject('show')).toBe('/foo/bar/merge_requests/1?view=parallel#L15-35');
+ });
+ it('replaces the current history state', function() {
+ var new_state;
+ this["class"]._location = stubLocation({
+ pathname: '/foo/bar/merge_requests/1'
+ });
+ new_state = this.subject('commits');
+ return expect(this.spies.history).toHaveBeenCalledWith({
+ turbolinks: true,
+ url: new_state
+ }, document.title, new_state);
+ });
+ return it('treats "show" like "notes"', function() {
+ this["class"]._location = stubLocation({
+ pathname: '/foo/bar/merge_requests/1/commits'
+ });
+ return expect(this.subject('show')).toBe('/foo/bar/merge_requests/1');
+ });
+ });
+ });
+
+}).call(this);
diff --git a/spec/javascripts/merge_request_tabs_spec.js.coffee b/spec/javascripts/merge_request_tabs_spec.js.coffee
deleted file mode 100644
index a0cfba455ea..00000000000
--- a/spec/javascripts/merge_request_tabs_spec.js.coffee
+++ /dev/null
@@ -1,88 +0,0 @@
-#= require merge_request_tabs
-
-describe 'MergeRequestTabs', ->
- stubLocation = (stubs) ->
- defaults = {pathname: '', search: '', hash: ''}
- $.extend(defaults, stubs)
-
- fixture.preload('merge_request_tabs.html')
-
- beforeEach ->
- @class = new MergeRequestTabs()
- @spies = {
- ajax: spyOn($, 'ajax').and.callFake ->
- history: spyOn(history, 'replaceState').and.callFake ->
- }
-
- describe '#activateTab', ->
- beforeEach ->
- fixture.load('merge_request_tabs.html')
- @subject = @class.activateTab
-
- it 'shows the first tab when action is show', ->
- @subject('show')
- expect($('#notes')).toHaveClass('active')
-
- it 'shows the notes tab when action is notes', ->
- @subject('notes')
- expect($('#notes')).toHaveClass('active')
-
- it 'shows the commits tab when action is commits', ->
- @subject('commits')
- expect($('#commits')).toHaveClass('active')
-
- it 'shows the diffs tab when action is diffs', ->
- @subject('diffs')
- expect($('#diffs')).toHaveClass('active')
-
- describe '#setCurrentAction', ->
- beforeEach ->
- @subject = @class.setCurrentAction
-
- it 'changes from commits', ->
- @class._location = stubLocation(pathname: '/foo/bar/merge_requests/1/commits')
-
- expect(@subject('notes')).toBe('/foo/bar/merge_requests/1')
- expect(@subject('diffs')).toBe('/foo/bar/merge_requests/1/diffs')
-
- it 'changes from diffs', ->
- @class._location = stubLocation(pathname: '/foo/bar/merge_requests/1/diffs')
-
- expect(@subject('notes')).toBe('/foo/bar/merge_requests/1')
- expect(@subject('commits')).toBe('/foo/bar/merge_requests/1/commits')
-
- it 'changes from diffs.html', ->
- @class._location = stubLocation(pathname: '/foo/bar/merge_requests/1/diffs.html')
-
- expect(@subject('notes')).toBe('/foo/bar/merge_requests/1')
- expect(@subject('commits')).toBe('/foo/bar/merge_requests/1/commits')
-
- it 'changes from notes', ->
- @class._location = stubLocation(pathname: '/foo/bar/merge_requests/1')
-
- expect(@subject('diffs')).toBe('/foo/bar/merge_requests/1/diffs')
- expect(@subject('commits')).toBe('/foo/bar/merge_requests/1/commits')
-
- it 'includes search parameters and hash string', ->
- @class._location = stubLocation({
- pathname: '/foo/bar/merge_requests/1/diffs'
- search: '?view=parallel'
- hash: '#L15-35'
- })
-
- expect(@subject('show')).toBe('/foo/bar/merge_requests/1?view=parallel#L15-35')
-
- it 'replaces the current history state', ->
- @class._location = stubLocation(pathname: '/foo/bar/merge_requests/1')
- new_state = @subject('commits')
-
- expect(@spies.history).toHaveBeenCalledWith(
- {turbolinks: true, url: new_state},
- document.title,
- new_state
- )
-
- it 'treats "show" like "notes"', ->
- @class._location = stubLocation(pathname: '/foo/bar/merge_requests/1/commits')
-
- expect(@subject('show')).toBe('/foo/bar/merge_requests/1')
diff --git a/spec/javascripts/merge_request_widget_spec.js b/spec/javascripts/merge_request_widget_spec.js
new file mode 100644
index 00000000000..17b32914ec3
--- /dev/null
+++ b/spec/javascripts/merge_request_widget_spec.js
@@ -0,0 +1,74 @@
+
+/*= require merge_request_widget */
+
+(function() {
+ describe('MergeRequestWidget', function() {
+ beforeEach(function() {
+ window.notifyPermissions = function() {};
+ window.notify = function() {};
+ this.opts = {
+ ci_status_url: "http://sampledomain.local/ci/getstatus",
+ ci_status: "",
+ ci_message: {
+ normal: "Build {{status}} for \"{{title}}\"",
+ preparing: "{{status}} build for \"{{title}}\""
+ },
+ ci_title: {
+ preparing: "{{status}} build",
+ normal: "Build {{status}}"
+ },
+ gitlab_icon: "gitlab_logo.png",
+ builds_path: "http://sampledomain.local/sampleBuildsPath"
+ };
+ this["class"] = new MergeRequestWidget(this.opts);
+ return this.ciStatusData = {
+ "title": "Sample MR title",
+ "sha": "12a34bc5",
+ "status": "success",
+ "coverage": 98
+ };
+ });
+ return describe('getCIStatus', function() {
+ beforeEach(function() {
+ return spyOn(jQuery, 'getJSON').and.callFake((function(_this) {
+ return function(req, cb) {
+ return cb(_this.ciStatusData);
+ };
+ })(this));
+ });
+ it('should call showCIStatus even if a notification should not be displayed', function() {
+ var spy;
+ spy = spyOn(this["class"], 'showCIStatus').and.stub();
+ this["class"].getCIStatus(false);
+ return expect(spy).toHaveBeenCalledWith(this.ciStatusData.status);
+ });
+ it('should call showCIStatus when a notification should be displayed', function() {
+ var spy;
+ spy = spyOn(this["class"], 'showCIStatus').and.stub();
+ this["class"].getCIStatus(true);
+ return expect(spy).toHaveBeenCalledWith(this.ciStatusData.status);
+ });
+ it('should call showCICoverage when the coverage rate is set', function() {
+ var spy;
+ spy = spyOn(this["class"], 'showCICoverage').and.stub();
+ this["class"].getCIStatus(false);
+ return expect(spy).toHaveBeenCalledWith(this.ciStatusData.coverage);
+ });
+ it('should not call showCICoverage when the coverage rate is not set', function() {
+ var spy;
+ this.ciStatusData.coverage = null;
+ spy = spyOn(this["class"], 'showCICoverage').and.stub();
+ this["class"].getCIStatus(false);
+ return expect(spy).not.toHaveBeenCalled();
+ });
+ return it('should not display a notification on the first check after the widget has been created', function() {
+ var spy;
+ spy = spyOn(window, 'notify');
+ this["class"] = new MergeRequestWidget(this.opts);
+ this["class"].getCIStatus(true);
+ return expect(spy).not.toHaveBeenCalled();
+ });
+ });
+ });
+
+}).call(this);
diff --git a/spec/javascripts/merge_request_widget_spec.js.coffee b/spec/javascripts/merge_request_widget_spec.js.coffee
deleted file mode 100644
index 92b7eeb1116..00000000000
--- a/spec/javascripts/merge_request_widget_spec.js.coffee
+++ /dev/null
@@ -1,55 +0,0 @@
-#= require merge_request_widget
-
-describe 'MergeRequestWidget', ->
-
- beforeEach ->
- window.notifyPermissions = () ->
- window.notify = () ->
- @opts = {
- ci_status_url:"http://sampledomain.local/ci/getstatus",
- ci_status:"",
- ci_message: {
- normal: "Build {{status}} for \"{{title}}\"",
- preparing: "{{status}} build for \"{{title}}\""
- },
- ci_title: {
- preparing: "{{status}} build",
- normal: "Build {{status}}"
- },
- gitlab_icon:"gitlab_logo.png",
- builds_path:"http://sampledomain.local/sampleBuildsPath"
- }
- @class = new MergeRequestWidget(@opts)
- @ciStatusData = {"title":"Sample MR title","sha":"12a34bc5","status":"success","coverage":98}
-
- describe 'getCIStatus', ->
- beforeEach ->
- spyOn(jQuery, 'getJSON').and.callFake (req, cb) =>
- cb(@ciStatusData)
-
- it 'should call showCIStatus even if a notification should not be displayed', ->
- spy = spyOn(@class, 'showCIStatus').and.stub()
- @class.getCIStatus(false)
- expect(spy).toHaveBeenCalledWith(@ciStatusData.status)
-
- it 'should call showCIStatus when a notification should be displayed', ->
- spy = spyOn(@class, 'showCIStatus').and.stub()
- @class.getCIStatus(true)
- expect(spy).toHaveBeenCalledWith(@ciStatusData.status)
-
- it 'should call showCICoverage when the coverage rate is set', ->
- spy = spyOn(@class, 'showCICoverage').and.stub()
- @class.getCIStatus(false)
- expect(spy).toHaveBeenCalledWith(@ciStatusData.coverage)
-
- it 'should not call showCICoverage when the coverage rate is not set', ->
- @ciStatusData.coverage = null
- spy = spyOn(@class, 'showCICoverage').and.stub()
- @class.getCIStatus(false)
- expect(spy).not.toHaveBeenCalled()
-
- it 'should not display a notification on the first check after the widget has been created', ->
- spy = spyOn(window, 'notify')
- @class = new MergeRequestWidget(@opts)
- @class.getCIStatus(true)
- expect(spy).not.toHaveBeenCalled()
diff --git a/spec/javascripts/new_branch_spec.js b/spec/javascripts/new_branch_spec.js
new file mode 100644
index 00000000000..25d3f5b6c04
--- /dev/null
+++ b/spec/javascripts/new_branch_spec.js
@@ -0,0 +1,170 @@
+
+/*= require jquery-ui/autocomplete */
+
+
+/*= require new_branch_form */
+
+(function() {
+ describe('Branch', function() {
+ return describe('create a new branch', function() {
+ var expectToHaveError, fillNameWith;
+ fixture.preload('new_branch.html');
+ fillNameWith = function(value) {
+ return $('.js-branch-name').val(value).trigger('blur');
+ };
+ expectToHaveError = function(error) {
+ return expect($('.js-branch-name-error span').text()).toEqual(error);
+ };
+ beforeEach(function() {
+ fixture.load('new_branch.html');
+ $('form').on('submit', function(e) {
+ return e.preventDefault();
+ });
+ return this.form = new NewBranchForm($('.js-create-branch-form'), []);
+ });
+ it("can't start with a dot", function() {
+ fillNameWith('.foo');
+ return expectToHaveError("can't start with '.'");
+ });
+ it("can't start with a slash", function() {
+ fillNameWith('/foo');
+ return expectToHaveError("can't start with '/'");
+ });
+ it("can't have two consecutive dots", function() {
+ fillNameWith('foo..bar');
+ return expectToHaveError("can't contain '..'");
+ });
+ it("can't have spaces anywhere", function() {
+ fillNameWith(' foo');
+ expectToHaveError("can't contain spaces");
+ fillNameWith('foo bar');
+ expectToHaveError("can't contain spaces");
+ fillNameWith('foo ');
+ return expectToHaveError("can't contain spaces");
+ });
+ it("can't have ~ anywhere", function() {
+ fillNameWith('~foo');
+ expectToHaveError("can't contain '~'");
+ fillNameWith('foo~bar');
+ expectToHaveError("can't contain '~'");
+ fillNameWith('foo~');
+ return expectToHaveError("can't contain '~'");
+ });
+ it("can't have tilde anwhere", function() {
+ fillNameWith('~foo');
+ expectToHaveError("can't contain '~'");
+ fillNameWith('foo~bar');
+ expectToHaveError("can't contain '~'");
+ fillNameWith('foo~');
+ return expectToHaveError("can't contain '~'");
+ });
+ it("can't have caret anywhere", function() {
+ fillNameWith('^foo');
+ expectToHaveError("can't contain '^'");
+ fillNameWith('foo^bar');
+ expectToHaveError("can't contain '^'");
+ fillNameWith('foo^');
+ return expectToHaveError("can't contain '^'");
+ });
+ it("can't have : anywhere", function() {
+ fillNameWith(':foo');
+ expectToHaveError("can't contain ':'");
+ fillNameWith('foo:bar');
+ expectToHaveError("can't contain ':'");
+ fillNameWith(':foo');
+ return expectToHaveError("can't contain ':'");
+ });
+ it("can't have question mark anywhere", function() {
+ fillNameWith('?foo');
+ expectToHaveError("can't contain '?'");
+ fillNameWith('foo?bar');
+ expectToHaveError("can't contain '?'");
+ fillNameWith('foo?');
+ return expectToHaveError("can't contain '?'");
+ });
+ it("can't have asterisk anywhere", function() {
+ fillNameWith('*foo');
+ expectToHaveError("can't contain '*'");
+ fillNameWith('foo*bar');
+ expectToHaveError("can't contain '*'");
+ fillNameWith('foo*');
+ return expectToHaveError("can't contain '*'");
+ });
+ it("can't have open bracket anywhere", function() {
+ fillNameWith('[foo');
+ expectToHaveError("can't contain '['");
+ fillNameWith('foo[bar');
+ expectToHaveError("can't contain '['");
+ fillNameWith('foo[');
+ return expectToHaveError("can't contain '['");
+ });
+ it("can't have a backslash anywhere", function() {
+ fillNameWith('\\foo');
+ expectToHaveError("can't contain '\\'");
+ fillNameWith('foo\\bar');
+ expectToHaveError("can't contain '\\'");
+ fillNameWith('foo\\');
+ return expectToHaveError("can't contain '\\'");
+ });
+ it("can't contain a sequence @{ anywhere", function() {
+ fillNameWith('@{foo');
+ expectToHaveError("can't contain '@{'");
+ fillNameWith('foo@{bar');
+ expectToHaveError("can't contain '@{'");
+ fillNameWith('foo@{');
+ return expectToHaveError("can't contain '@{'");
+ });
+ it("can't have consecutive slashes", function() {
+ fillNameWith('foo//bar');
+ return expectToHaveError("can't contain consecutive slashes");
+ });
+ it("can't end with a slash", function() {
+ fillNameWith('foo/');
+ return expectToHaveError("can't end in '/'");
+ });
+ it("can't end with a dot", function() {
+ fillNameWith('foo.');
+ return expectToHaveError("can't end in '.'");
+ });
+ it("can't end with .lock", function() {
+ fillNameWith('foo.lock');
+ return expectToHaveError("can't end in '.lock'");
+ });
+ it("can't be the single character @", function() {
+ fillNameWith('@');
+ return expectToHaveError("can't be '@'");
+ });
+ it("concatenates all error messages", function() {
+ fillNameWith('/foo bar?~.');
+ return expectToHaveError("can't start with '/', can't contain spaces, '?', '~', can't end in '.'");
+ });
+ it("doesn't duplicate error messages", function() {
+ fillNameWith('?foo?bar?zoo?');
+ return expectToHaveError("can't contain '?'");
+ });
+ it("removes the error message when is a valid name", function() {
+ fillNameWith('foo?bar');
+ expect($('.js-branch-name-error span').length).toEqual(1);
+ fillNameWith('foobar');
+ return expect($('.js-branch-name-error span').length).toEqual(0);
+ });
+ it("can have dashes anywhere", function() {
+ fillNameWith('-foo-bar-zoo-');
+ return expect($('.js-branch-name-error span').length).toEqual(0);
+ });
+ it("can have underscores anywhere", function() {
+ fillNameWith('_foo_bar_zoo_');
+ return expect($('.js-branch-name-error span').length).toEqual(0);
+ });
+ it("can have numbers anywhere", function() {
+ fillNameWith('1foo2bar3zoo4');
+ return expect($('.js-branch-name-error span').length).toEqual(0);
+ });
+ return it("can be only letters", function() {
+ fillNameWith('foo');
+ return expect($('.js-branch-name-error span').length).toEqual(0);
+ });
+ });
+ });
+
+}).call(this);
diff --git a/spec/javascripts/new_branch_spec.js.coffee b/spec/javascripts/new_branch_spec.js.coffee
deleted file mode 100644
index ce773793817..00000000000
--- a/spec/javascripts/new_branch_spec.js.coffee
+++ /dev/null
@@ -1,160 +0,0 @@
-#= require jquery-ui/autocomplete
-#= require new_branch_form
-
-describe 'Branch', ->
- describe 'create a new branch', ->
- fixture.preload('new_branch.html')
-
- fillNameWith = (value) ->
- $('.js-branch-name').val(value).trigger('blur')
-
- expectToHaveError = (error) ->
- expect($('.js-branch-name-error span').text()).toEqual(error)
-
- beforeEach ->
- fixture.load('new_branch.html')
- $('form').on 'submit', (e) -> e.preventDefault()
-
- @form = new NewBranchForm($('.js-create-branch-form'), [])
-
- it "can't start with a dot", ->
- fillNameWith '.foo'
- expectToHaveError "can't start with '.'"
-
- it "can't start with a slash", ->
- fillNameWith '/foo'
- expectToHaveError "can't start with '/'"
-
- it "can't have two consecutive dots", ->
- fillNameWith 'foo..bar'
- expectToHaveError "can't contain '..'"
-
- it "can't have spaces anywhere", ->
- fillNameWith ' foo'
- expectToHaveError "can't contain spaces"
- fillNameWith 'foo bar'
- expectToHaveError "can't contain spaces"
- fillNameWith 'foo '
- expectToHaveError "can't contain spaces"
-
- it "can't have ~ anywhere", ->
- fillNameWith '~foo'
- expectToHaveError "can't contain '~'"
- fillNameWith 'foo~bar'
- expectToHaveError "can't contain '~'"
- fillNameWith 'foo~'
- expectToHaveError "can't contain '~'"
-
- it "can't have tilde anwhere", ->
- fillNameWith '~foo'
- expectToHaveError "can't contain '~'"
- fillNameWith 'foo~bar'
- expectToHaveError "can't contain '~'"
- fillNameWith 'foo~'
- expectToHaveError "can't contain '~'"
-
- it "can't have caret anywhere", ->
- fillNameWith '^foo'
- expectToHaveError "can't contain '^'"
- fillNameWith 'foo^bar'
- expectToHaveError "can't contain '^'"
- fillNameWith 'foo^'
- expectToHaveError "can't contain '^'"
-
- it "can't have : anywhere", ->
- fillNameWith ':foo'
- expectToHaveError "can't contain ':'"
- fillNameWith 'foo:bar'
- expectToHaveError "can't contain ':'"
- fillNameWith ':foo'
- expectToHaveError "can't contain ':'"
-
- it "can't have question mark anywhere", ->
- fillNameWith '?foo'
- expectToHaveError "can't contain '?'"
- fillNameWith 'foo?bar'
- expectToHaveError "can't contain '?'"
- fillNameWith 'foo?'
- expectToHaveError "can't contain '?'"
-
- it "can't have asterisk anywhere", ->
- fillNameWith '*foo'
- expectToHaveError "can't contain '*'"
- fillNameWith 'foo*bar'
- expectToHaveError "can't contain '*'"
- fillNameWith 'foo*'
- expectToHaveError "can't contain '*'"
-
- it "can't have open bracket anywhere", ->
- fillNameWith '[foo'
- expectToHaveError "can't contain '['"
- fillNameWith 'foo[bar'
- expectToHaveError "can't contain '['"
- fillNameWith 'foo['
- expectToHaveError "can't contain '['"
-
- it "can't have a backslash anywhere", ->
- fillNameWith '\\foo'
- expectToHaveError "can't contain '\\'"
- fillNameWith 'foo\\bar'
- expectToHaveError "can't contain '\\'"
- fillNameWith 'foo\\'
- expectToHaveError "can't contain '\\'"
-
- it "can't contain a sequence @{ anywhere", ->
- fillNameWith '@{foo'
- expectToHaveError "can't contain '@{'"
- fillNameWith 'foo@{bar'
- expectToHaveError "can't contain '@{'"
- fillNameWith 'foo@{'
- expectToHaveError "can't contain '@{'"
-
- it "can't have consecutive slashes", ->
- fillNameWith 'foo//bar'
- expectToHaveError "can't contain consecutive slashes"
-
- it "can't end with a slash", ->
- fillNameWith 'foo/'
- expectToHaveError "can't end in '/'"
-
- it "can't end with a dot", ->
- fillNameWith 'foo.'
- expectToHaveError "can't end in '.'"
-
- it "can't end with .lock", ->
- fillNameWith 'foo.lock'
- expectToHaveError "can't end in '.lock'"
-
- it "can't be the single character @", ->
- fillNameWith '@'
- expectToHaveError "can't be '@'"
-
- it "concatenates all error messages", ->
- fillNameWith '/foo bar?~.'
- expectToHaveError "can't start with '/', can't contain spaces, '?', '~', can't end in '.'"
-
- it "doesn't duplicate error messages", ->
- fillNameWith '?foo?bar?zoo?'
- expectToHaveError "can't contain '?'"
-
- it "removes the error message when is a valid name", ->
- fillNameWith 'foo?bar'
- expect($('.js-branch-name-error span').length).toEqual(1)
- fillNameWith 'foobar'
- expect($('.js-branch-name-error span').length).toEqual(0)
-
- it "can have dashes anywhere", ->
- fillNameWith '-foo-bar-zoo-'
- expect($('.js-branch-name-error span').length).toEqual(0)
-
- it "can have underscores anywhere", ->
- fillNameWith '_foo_bar_zoo_'
- expect($('.js-branch-name-error span').length).toEqual(0)
-
- it "can have numbers anywhere", ->
- fillNameWith '1foo2bar3zoo4'
- expect($('.js-branch-name-error span').length).toEqual(0)
-
- it "can be only letters", ->
- fillNameWith 'foo'
- expect($('.js-branch-name-error span').length).toEqual(0)
diff --git a/spec/javascripts/notes_spec.js b/spec/javascripts/notes_spec.js
new file mode 100644
index 00000000000..14dc6bfdfde
--- /dev/null
+++ b/spec/javascripts/notes_spec.js
@@ -0,0 +1,41 @@
+
+/*= require notes */
+
+
+/*= require gl_form */
+
+(function() {
+ window.gon || (window.gon = {});
+
+ window.disableButtonIfEmptyField = function() {
+ return null;
+ };
+
+ describe('Notes', function() {
+ return describe('task lists', function() {
+ fixture.preload('issue_note.html');
+ beforeEach(function() {
+ fixture.load('issue_note.html');
+ $('form').on('submit', function(e) {
+ return e.preventDefault();
+ });
+ return this.notes = new Notes();
+ });
+ it('modifies the Markdown field', function() {
+ $('input[type=checkbox]').attr('checked', true).trigger('change');
+ return expect($('.js-task-list-field').val()).toBe('- [x] Task List Item');
+ });
+ return it('submits the form on tasklist:changed', function() {
+ var submitted;
+ submitted = false;
+ $('form').on('submit', function(e) {
+ submitted = true;
+ return e.preventDefault();
+ });
+ $('.js-task-list-field').trigger('tasklist:changed');
+ return expect(submitted).toBe(true);
+ });
+ });
+ });
+
+}).call(this);
diff --git a/spec/javascripts/notes_spec.js.coffee b/spec/javascripts/notes_spec.js.coffee
deleted file mode 100644
index 3a3c8d63e82..00000000000
--- a/spec/javascripts/notes_spec.js.coffee
+++ /dev/null
@@ -1,26 +0,0 @@
-#= require notes
-#= require gl_form
-
-window.gon or= {}
-window.disableButtonIfEmptyField = -> null
-
-describe 'Notes', ->
- describe 'task lists', ->
- fixture.preload('issue_note.html')
-
- beforeEach ->
- fixture.load('issue_note.html')
- $('form').on 'submit', (e) -> e.preventDefault()
-
- @notes = new Notes()
-
- it 'modifies the Markdown field', ->
- $('input[type=checkbox]').attr('checked', true).trigger('change')
- expect($('.js-task-list-field').val()).toBe('- [x] Task List Item')
-
- it 'submits the form on tasklist:changed', ->
- submitted = false
- $('form').on 'submit', (e) -> submitted = true; e.preventDefault()
-
- $('.js-task-list-field').trigger('tasklist:changed')
- expect(submitted).toBe(true)
diff --git a/spec/javascripts/project_title_spec.js b/spec/javascripts/project_title_spec.js
new file mode 100644
index 00000000000..ffe49828492
--- /dev/null
+++ b/spec/javascripts/project_title_spec.js
@@ -0,0 +1,60 @@
+
+/*= require bootstrap */
+
+
+/*= require select2 */
+
+
+/*= require lib/utils/type_utility */
+
+
+/*= require gl_dropdown */
+
+
+/*= require api */
+
+
+/*= require project_select */
+
+
+/*= require project */
+
+(function() {
+ window.gon || (window.gon = {});
+
+ window.gon.api_version = 'v3';
+
+ describe('Project Title', function() {
+ fixture.preload('project_title.html');
+ fixture.preload('projects.json');
+ beforeEach(function() {
+ fixture.load('project_title.html');
+ return this.project = new Project();
+ });
+ return describe('project list', function() {
+ beforeEach((function(_this) {
+ return function() {
+ _this.projects_data = fixture.load('projects.json')[0];
+ return spyOn(jQuery, 'ajax').and.callFake(function(req) {
+ var d;
+ expect(req.url).toBe('/api/v3/projects.json?simple=true');
+ d = $.Deferred();
+ d.resolve(_this.projects_data);
+ return d.promise();
+ });
+ };
+ })(this));
+ it('to show on toggle click', (function(_this) {
+ return function() {
+ $('.js-projects-dropdown-toggle').click();
+ return expect($('.header-content').hasClass('open')).toBe(true);
+ };
+ })(this));
+ return it('hide dropdown', function() {
+ $(".dropdown-menu-close-icon").click();
+ return expect($('.header-content').hasClass('open')).toBe(false);
+ });
+ });
+ });
+
+}).call(this);
diff --git a/spec/javascripts/project_title_spec.js.coffee b/spec/javascripts/project_title_spec.js.coffee
deleted file mode 100644
index f0d26fb5446..00000000000
--- a/spec/javascripts/project_title_spec.js.coffee
+++ /dev/null
@@ -1,37 +0,0 @@
-#= require bootstrap
-#= require select2
-#= require lib/utils/type_utility
-#= require gl_dropdown
-#= require api
-#= require project_select
-#= require project
-
-window.gon or= {}
-window.gon.api_version = 'v3'
-
-describe 'Project Title', ->
- fixture.preload('project_title.html')
- fixture.preload('projects.json')
-
- beforeEach ->
- fixture.load('project_title.html')
- @project = new Project()
-
- describe 'project list', ->
- beforeEach =>
- @projects_data = fixture.load('projects.json')[0]
-
- spyOn(jQuery, 'ajax').and.callFake (req) =>
- expect(req.url).toBe('/api/v3/projects.json')
- d = $.Deferred()
- d.resolve @projects_data
- d.promise()
-
- it 'to show on toggle click', =>
- $('.js-projects-dropdown-toggle').click()
- expect($('.header-content').hasClass('open')).toBe(true)
-
- it 'hide dropdown', ->
- $(".dropdown-menu-close-icon").click()
-
- expect($('.header-content').hasClass('open')).toBe(false)
diff --git a/spec/javascripts/right_sidebar_spec.js b/spec/javascripts/right_sidebar_spec.js
new file mode 100644
index 00000000000..38b3b2653ec
--- /dev/null
+++ b/spec/javascripts/right_sidebar_spec.js
@@ -0,0 +1,70 @@
+
+/*= require right_sidebar */
+
+
+/*= require jquery */
+
+
+/*= require jquery.cookie */
+
+(function() {
+ var $aside, $icon, $labelsIcon, $page, $toggle, assertSidebarState;
+
+ this.sidebar = null;
+
+ $aside = null;
+
+ $toggle = null;
+
+ $icon = null;
+
+ $page = null;
+
+ $labelsIcon = null;
+
+ assertSidebarState = function(state) {
+ var shouldBeCollapsed, shouldBeExpanded;
+ shouldBeExpanded = state === 'expanded';
+ shouldBeCollapsed = state === '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);
+ return expect($icon.hasClass('fa-angle-double-left')).toBe(shouldBeCollapsed);
+ };
+
+ describe('RightSidebar', function() {
+ fixture.preload('right_sidebar.html');
+ beforeEach(function() {
+ fixture.load('right_sidebar.html');
+ this.sidebar = new Sidebar;
+ $aside = $('.right-sidebar');
+ $page = $('.page-with-sidebar');
+ $icon = $aside.find('i');
+ $toggle = $aside.find('.js-sidebar-toggle');
+ return $labelsIcon = $aside.find('.sidebar-collapsed-icon');
+ });
+ it('should expand the sidebar when arrow is clicked', function() {
+ $toggle.click();
+ return assertSidebarState('expanded');
+ });
+ it('should collapse the sidebar when arrow is clicked', function() {
+ $toggle.click();
+ assertSidebarState('expanded');
+ $toggle.click();
+ return assertSidebarState('collapsed');
+ });
+ it('should float over the page and when sidebar icons clicked', function() {
+ $labelsIcon.click();
+ return assertSidebarState('expanded');
+ });
+ return it('should collapse when the icon arrow clicked while it is floating on page', function() {
+ $labelsIcon.click();
+ assertSidebarState('expanded');
+ $toggle.click();
+ return assertSidebarState('collapsed');
+ });
+ });
+
+}).call(this);
diff --git a/spec/javascripts/right_sidebar_spec.js.coffee b/spec/javascripts/right_sidebar_spec.js.coffee
deleted file mode 100644
index 2075cacdb67..00000000000
--- a/spec/javascripts/right_sidebar_spec.js.coffee
+++ /dev/null
@@ -1,69 +0,0 @@
-#= 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/search_autocomplete_spec.js b/spec/javascripts/search_autocomplete_spec.js
new file mode 100644
index 00000000000..324f5152780
--- /dev/null
+++ b/spec/javascripts/search_autocomplete_spec.js
@@ -0,0 +1,159 @@
+
+/*= require gl_dropdown */
+
+
+/*= require search_autocomplete */
+
+
+/*= require jquery */
+
+
+/*= require lib/utils/common_utils */
+
+
+/*= require lib/utils/type_utility */
+
+
+/*= require fuzzaldrin-plus */
+
+(function() {
+ var addBodyAttributes, assertLinks, dashboardIssuesPath, dashboardMRsPath, groupIssuesPath, groupMRsPath, groupName, mockDashboardOptions, mockGroupOptions, mockProjectOptions, projectIssuesPath, projectMRsPath, projectName, userId, widget;
+
+ widget = null;
+
+ userId = 1;
+
+ window.gon || (window.gon = {});
+
+ window.gon.current_user_id = userId;
+
+ dashboardIssuesPath = '/dashboard/issues';
+
+ dashboardMRsPath = '/dashboard/merge_requests';
+
+ projectIssuesPath = '/gitlab-org/gitlab-ce/issues';
+
+ projectMRsPath = '/gitlab-org/gitlab-ce/merge_requests';
+
+ groupIssuesPath = '/groups/gitlab-org/issues';
+
+ groupMRsPath = '/groups/gitlab-org/merge_requests';
+
+ projectName = 'GitLab Community Edition';
+
+ groupName = 'Gitlab Org';
+
+ addBodyAttributes = function(section) {
+ var $body;
+ if (section == null) {
+ section = 'dashboard';
+ }
+ $body = $('body');
+ $body.removeAttr('data-page');
+ $body.removeAttr('data-project');
+ $body.removeAttr('data-group');
+ switch (section) {
+ case 'dashboard':
+ return $body.data('page', 'root:index');
+ case 'group':
+ $body.data('page', 'groups:show');
+ return $body.data('group', 'gitlab-org');
+ case 'project':
+ $body.data('page', 'projects:show');
+ return $body.data('project', 'gitlab-ce');
+ }
+ };
+
+ mockDashboardOptions = function() {
+ window.gl || (window.gl = {});
+ return window.gl.dashboardOptions = {
+ issuesPath: dashboardIssuesPath,
+ mrPath: dashboardMRsPath
+ };
+ };
+
+ mockProjectOptions = function() {
+ window.gl || (window.gl = {});
+ return window.gl.projectOptions = {
+ 'gitlab-ce': {
+ issuesPath: projectIssuesPath,
+ mrPath: projectMRsPath,
+ projectName: projectName
+ }
+ };
+ };
+
+ mockGroupOptions = function() {
+ window.gl || (window.gl = {});
+ return window.gl.groupOptions = {
+ 'gitlab-org': {
+ issuesPath: groupIssuesPath,
+ mrPath: groupMRsPath,
+ projectName: groupName
+ }
+ };
+ };
+
+ assertLinks = function(list, issuesPath, mrsPath) {
+ var a1, a2, a3, a4, issuesAssignedToMeLink, issuesIHaveCreatedLink, mrsAssignedToMeLink, mrsIHaveCreatedLink;
+ issuesAssignedToMeLink = issuesPath + "/?assignee_id=" + userId;
+ issuesIHaveCreatedLink = issuesPath + "/?author_id=" + userId;
+ mrsAssignedToMeLink = mrsPath + "/?assignee_id=" + userId;
+ mrsIHaveCreatedLink = mrsPath + "/?author_id=" + userId;
+ a1 = "a[href='" + issuesAssignedToMeLink + "']";
+ a2 = "a[href='" + issuesIHaveCreatedLink + "']";
+ a3 = "a[href='" + mrsAssignedToMeLink + "']";
+ a4 = "a[href='" + mrsIHaveCreatedLink + "']";
+ expect(list.find(a1).length).toBe(1);
+ expect(list.find(a1).text()).toBe('Issues assigned to me');
+ expect(list.find(a2).length).toBe(1);
+ expect(list.find(a2).text()).toBe("Issues I've created");
+ expect(list.find(a3).length).toBe(1);
+ expect(list.find(a3).text()).toBe('Merge requests assigned to me');
+ expect(list.find(a4).length).toBe(1);
+ return expect(list.find(a4).text()).toBe("Merge requests I've created");
+ };
+
+ describe('Search autocomplete dropdown', function() {
+ fixture.preload('search_autocomplete.html');
+ beforeEach(function() {
+ fixture.load('search_autocomplete.html');
+ return widget = new SearchAutocomplete;
+ });
+ it('should show Dashboard specific dropdown menu', function() {
+ var list;
+ addBodyAttributes();
+ mockDashboardOptions();
+ widget.searchInput.focus();
+ list = widget.wrap.find('.dropdown-menu').find('ul');
+ return assertLinks(list, dashboardIssuesPath, dashboardMRsPath);
+ });
+ it('should show Group specific dropdown menu', function() {
+ var list;
+ addBodyAttributes('group');
+ mockGroupOptions();
+ widget.searchInput.focus();
+ list = widget.wrap.find('.dropdown-menu').find('ul');
+ return assertLinks(list, groupIssuesPath, groupMRsPath);
+ });
+ it('should show Project specific dropdown menu', function() {
+ var list;
+ addBodyAttributes('project');
+ mockProjectOptions();
+ widget.searchInput.focus();
+ list = widget.wrap.find('.dropdown-menu').find('ul');
+ return assertLinks(list, projectIssuesPath, projectMRsPath);
+ });
+ return it('should not show category related menu if there is text in the input', function() {
+ var link, list;
+ addBodyAttributes('project');
+ mockProjectOptions();
+ widget.searchInput.val('help');
+ widget.searchInput.focus();
+ list = widget.wrap.find('.dropdown-menu').find('ul');
+ link = "a[href='" + projectIssuesPath + "/?assignee_id=" + userId + "']";
+ return expect(list.find(link).length).toBe(0);
+ });
+ });
+
+}).call(this);
diff --git a/spec/javascripts/search_autocomplete_spec.js.coffee b/spec/javascripts/search_autocomplete_spec.js.coffee
deleted file mode 100644
index 1c1faca3333..00000000000
--- a/spec/javascripts/search_autocomplete_spec.js.coffee
+++ /dev/null
@@ -1,149 +0,0 @@
-#= require gl_dropdown
-#= require search_autocomplete
-#= require jquery
-#= require lib/utils/common_utils
-#= require lib/utils/type_utility
-#= require fuzzaldrin-plus
-
-
-widget = null
-userId = 1
-window.gon or= {}
-window.gon.current_user_id = userId
-
-dashboardIssuesPath = '/dashboard/issues'
-dashboardMRsPath = '/dashboard/merge_requests'
-projectIssuesPath = '/gitlab-org/gitlab-ce/issues'
-projectMRsPath = '/gitlab-org/gitlab-ce/merge_requests'
-groupIssuesPath = '/groups/gitlab-org/issues'
-groupMRsPath = '/groups/gitlab-org/merge_requests'
-projectName = 'GitLab Community Edition'
-groupName = 'Gitlab Org'
-
-
-# Add required attributes to body before starting the test.
-# section would be dashboard|group|project
-addBodyAttributes = (section = 'dashboard') ->
-
- $body = $ 'body'
-
- $body.removeAttr 'data-page'
- $body.removeAttr 'data-project'
- $body.removeAttr 'data-group'
-
- switch section
- when 'dashboard'
- $body.data 'page', 'root:index'
- when 'group'
- $body.data 'page', 'groups:show'
- $body.data 'group', 'gitlab-org'
- when 'project'
- $body.data 'page', 'projects:show'
- $body.data 'project', 'gitlab-ce'
-
-
-# Mock `gl` object in window for dashboard specific page. App code will need it.
-mockDashboardOptions = ->
-
- window.gl or= {}
- window.gl.dashboardOptions =
- issuesPath: dashboardIssuesPath
- mrPath : dashboardMRsPath
-
-
-# Mock `gl` object in window for project specific page. App code will need it.
-mockProjectOptions = ->
-
- window.gl or= {}
- window.gl.projectOptions =
- 'gitlab-ce' :
- issuesPath : projectIssuesPath
- mrPath : projectMRsPath
- projectName : projectName
-
-
-mockGroupOptions = ->
-
- window.gl or= {}
- window.gl.groupOptions =
- 'gitlab-org' :
- issuesPath : groupIssuesPath
- mrPath : groupMRsPath
- projectName : groupName
-
-
-assertLinks = (list, issuesPath, mrsPath) ->
-
- issuesAssignedToMeLink = "#{issuesPath}/?assignee_id=#{userId}"
- issuesIHaveCreatedLink = "#{issuesPath}/?author_id=#{userId}"
- mrsAssignedToMeLink = "#{mrsPath}/?assignee_id=#{userId}"
- mrsIHaveCreatedLink = "#{mrsPath}/?author_id=#{userId}"
-
- a1 = "a[href='#{issuesAssignedToMeLink}']"
- a2 = "a[href='#{issuesIHaveCreatedLink}']"
- a3 = "a[href='#{mrsAssignedToMeLink}']"
- a4 = "a[href='#{mrsIHaveCreatedLink}']"
-
- expect(list.find(a1).length).toBe 1
- expect(list.find(a1).text()).toBe ' Issues assigned to me '
-
- expect(list.find(a2).length).toBe 1
- expect(list.find(a2).text()).toBe " Issues I've created "
-
- expect(list.find(a3).length).toBe 1
- expect(list.find(a3).text()).toBe ' Merge requests assigned to me '
-
- expect(list.find(a4).length).toBe 1
- expect(list.find(a4).text()).toBe " Merge requests I've created "
-
-
-describe 'Search autocomplete dropdown', ->
-
- fixture.preload 'search_autocomplete.html'
-
- beforeEach ->
-
- fixture.load 'search_autocomplete.html'
- widget = new SearchAutocomplete
-
-
- it 'should show Dashboard specific dropdown menu', ->
-
- addBodyAttributes()
- mockDashboardOptions()
- widget.searchInput.focus()
-
- list = widget.wrap.find('.dropdown-menu').find 'ul'
- assertLinks list, dashboardIssuesPath, dashboardMRsPath
-
-
- it 'should show Group specific dropdown menu', ->
-
- addBodyAttributes 'group'
- mockGroupOptions()
- widget.searchInput.focus()
-
- list = widget.wrap.find('.dropdown-menu').find 'ul'
- assertLinks list, groupIssuesPath, groupMRsPath
-
-
- it 'should show Project specific dropdown menu', ->
-
- addBodyAttributes 'project'
- mockProjectOptions()
- widget.searchInput.focus()
-
- list = widget.wrap.find('.dropdown-menu').find 'ul'
- assertLinks list, projectIssuesPath, projectMRsPath
-
-
- it 'should not show category related menu if there is text in the input', ->
-
- addBodyAttributes 'project'
- mockProjectOptions()
- widget.searchInput.val 'help'
- widget.searchInput.focus()
-
- list = widget.wrap.find('.dropdown-menu').find 'ul'
- link = "a[href='#{projectIssuesPath}/?assignee_id=#{userId}']"
- expect(list.find(link).length).toBe 0
diff --git a/spec/javascripts/shortcuts_issuable_spec.js b/spec/javascripts/shortcuts_issuable_spec.js
new file mode 100644
index 00000000000..7b6b55fe545
--- /dev/null
+++ b/spec/javascripts/shortcuts_issuable_spec.js
@@ -0,0 +1,74 @@
+
+/*= require shortcuts_issuable */
+
+(function() {
+ describe('ShortcutsIssuable', function() {
+ fixture.preload('issuable.html');
+ beforeEach(function() {
+ fixture.load('issuable.html');
+ return this.shortcut = new ShortcutsIssuable();
+ });
+ return describe('#replyWithSelectedText', function() {
+ var stubSelection;
+ stubSelection = function(text) {
+ return window.getSelection = function() {
+ return text;
+ };
+ };
+ beforeEach(function() {
+ return this.selector = 'form.js-main-target-form textarea#note_note';
+ });
+ describe('with empty selection', function() {
+ return it('does nothing', function() {
+ stubSelection('');
+ this.shortcut.replyWithSelectedText();
+ return expect($(this.selector).val()).toBe('');
+ });
+ });
+ describe('with any selection', function() {
+ beforeEach(function() {
+ return stubSelection('Selected text.');
+ });
+ it('leaves existing input intact', function() {
+ $(this.selector).val('This text was already here.');
+ expect($(this.selector).val()).toBe('This text was already here.');
+ this.shortcut.replyWithSelectedText();
+ return expect($(this.selector).val()).toBe("This text was already here.\n> Selected text.\n\n");
+ });
+ it('triggers `input`', function() {
+ var triggered;
+ triggered = false;
+ $(this.selector).on('input', function() {
+ return triggered = true;
+ });
+ this.shortcut.replyWithSelectedText();
+ return expect(triggered).toBe(true);
+ });
+ return it('triggers `focus`', function() {
+ var focused;
+ focused = false;
+ $(this.selector).on('focus', function() {
+ return focused = true;
+ });
+ this.shortcut.replyWithSelectedText();
+ return expect(focused).toBe(true);
+ });
+ });
+ describe('with a one-line selection', function() {
+ return it('quotes the selection', function() {
+ stubSelection('This text has been selected.');
+ this.shortcut.replyWithSelectedText();
+ return expect($(this.selector).val()).toBe("> This text has been selected.\n\n");
+ });
+ });
+ return describe('with a multi-line selection', function() {
+ return it('quotes the selected lines as a group', function() {
+ stubSelection("Selected line one.\n\nSelected line two.\nSelected line three.\n");
+ this.shortcut.replyWithSelectedText();
+ return expect($(this.selector).val()).toBe("> Selected line one.\n> Selected line two.\n> Selected line three.\n\n");
+ });
+ });
+ });
+ });
+
+}).call(this);
diff --git a/spec/javascripts/shortcuts_issuable_spec.js.coffee b/spec/javascripts/shortcuts_issuable_spec.js.coffee
deleted file mode 100644
index a01ad7140dd..00000000000
--- a/spec/javascripts/shortcuts_issuable_spec.js.coffee
+++ /dev/null
@@ -1,82 +0,0 @@
-#= require shortcuts_issuable
-
-describe 'ShortcutsIssuable', ->
- fixture.preload('issuable.html')
-
- beforeEach ->
- fixture.load('issuable.html')
- @shortcut = new ShortcutsIssuable()
-
- describe '#replyWithSelectedText', ->
- # Stub window.getSelection to return the provided String.
- stubSelection = (text) ->
- window.getSelection = -> text
-
- beforeEach ->
- @selector = 'form.js-main-target-form textarea#note_note'
-
- describe 'with empty selection', ->
- it 'does nothing', ->
- stubSelection('')
- @shortcut.replyWithSelectedText()
- expect($(@selector).val()).toBe('')
-
- describe 'with any selection', ->
- beforeEach ->
- stubSelection('Selected text.')
-
- it 'leaves existing input intact', ->
- $(@selector).val('This text was already here.')
- expect($(@selector).val()).toBe('This text was already here.')
-
- @shortcut.replyWithSelectedText()
- expect($(@selector).val()).
- toBe("This text was already here.\n> Selected text.\n\n")
-
- it 'triggers `input`', ->
- triggered = false
- $(@selector).on 'input', -> triggered = true
- @shortcut.replyWithSelectedText()
-
- expect(triggered).toBe(true)
-
- it 'triggers `focus`', ->
- focused = false
- $(@selector).on 'focus', -> focused = true
- @shortcut.replyWithSelectedText()
-
- expect(focused).toBe(true)
-
- describe 'with a one-line selection', ->
- it 'quotes the selection', ->
- stubSelection('This text has been selected.')
-
- @shortcut.replyWithSelectedText()
-
- expect($(@selector).val()).
- toBe("> This text has been selected.\n\n")
-
- describe 'with a multi-line selection', ->
- it 'quotes the selected lines as a group', ->
- stubSelection(
- """
- Selected line one.
-
- Selected line two.
- Selected line three.
-
- """
- )
-
- @shortcut.replyWithSelectedText()
-
- expect($(@selector).val()).
- toBe(
- """
- > Selected line one.
- > Selected line two.
- > Selected line three.
-
-
- """
- )
diff --git a/spec/javascripts/spec_helper.coffee b/spec/javascripts/spec_helper.coffee
deleted file mode 100644
index 90b02a6aec5..00000000000
--- a/spec/javascripts/spec_helper.coffee
+++ /dev/null
@@ -1,47 +0,0 @@
-# PhantomJS (Teaspoons default driver) doesn't have support for
-# Function.prototype.bind, which has caused confusion. Use this polyfill to
-# avoid the confusion.
-
-#= require support/bind-poly
-
-# You can require your own javascript files here. By default this will include
-# everything in application, however you may get better load performance if you
-# require the specific files that are being used in the spec that tests them.
-
-#= require jquery
-#= require jquery.turbolinks
-#= require bootstrap
-#= require underscore
-
-# Teaspoon includes some support files, but you can use anything from your own
-# support path too.
-
-# require support/jasmine-jquery-1.7.0
-# require support/jasmine-jquery-2.0.0
-#= require support/jasmine-jquery-2.1.0
-# require support/sinon
-# require support/your-support-file
-
-# Deferring execution
-
-# If you're using CommonJS, RequireJS or some other asynchronous library you can
-# defer execution. Call Teaspoon.execute() after everything has been loaded.
-# Simple example of a timeout:
-
-# Teaspoon.defer = true
-# setTimeout(Teaspoon.execute, 1000)
-
-# Matching files
-
-# By default Teaspoon will look for files that match
-# _spec.{js,js.coffee,.coffee}. Add a filename_spec.js file in your spec path
-# and it'll be included in the default suite automatically. If you want to
-# customize suites, check out the configuration in teaspoon_env.rb
-
-# Manifest
-
-# If you'd rather require your spec files manually (to control order for
-# instance) you can disable the suite matcher in the configuration and use this
-# file as a manifest.
-
-# For more information: http://github.com/modeset/teaspoon
diff --git a/spec/javascripts/spec_helper.js b/spec/javascripts/spec_helper.js
new file mode 100644
index 00000000000..7d91ed0f855
--- /dev/null
+++ b/spec/javascripts/spec_helper.js
@@ -0,0 +1,22 @@
+
+/*= require support/bind-poly */
+
+
+/*= require jquery */
+
+
+/*= require jquery.turbolinks */
+
+
+/*= require bootstrap */
+
+
+/*= require underscore */
+
+
+/*= require support/jasmine-jquery-2.1.0 */
+
+(function() {
+
+
+}).call(this);
diff --git a/spec/javascripts/syntax_highlight_spec.js b/spec/javascripts/syntax_highlight_spec.js
new file mode 100644
index 00000000000..4e5dd1e59bf
--- /dev/null
+++ b/spec/javascripts/syntax_highlight_spec.js
@@ -0,0 +1,44 @@
+
+/*= require syntax_highlight */
+
+(function() {
+ describe('Syntax Highlighter', function() {
+ var stubUserColorScheme;
+ stubUserColorScheme = function(value) {
+ if (window.gon == null) {
+ window.gon = {};
+ }
+ return window.gon.user_color_scheme = value;
+ };
+ describe('on a js-syntax-highlight element', function() {
+ beforeEach(function() {
+ return fixture.set('<div class="js-syntax-highlight"></div>');
+ });
+ return it('applies syntax highlighting', function() {
+ stubUserColorScheme('monokai');
+ $('.js-syntax-highlight').syntaxHighlight();
+ return expect($('.js-syntax-highlight')).toHaveClass('monokai');
+ });
+ });
+ return describe('on a parent element', function() {
+ beforeEach(function() {
+ return fixture.set("<div class=\"parent\">\n <div class=\"js-syntax-highlight\"></div>\n <div class=\"foo\"></div>\n <div class=\"js-syntax-highlight\"></div>\n</div>");
+ });
+ it('applies highlighting to all applicable children', function() {
+ stubUserColorScheme('monokai');
+ $('.parent').syntaxHighlight();
+ expect($('.parent, .foo')).not.toHaveClass('monokai');
+ return expect($('.monokai').length).toBe(2);
+ });
+ return it('prevents an infinite loop when no matches exist', function() {
+ var highlight;
+ fixture.set('<div></div>');
+ highlight = function() {
+ return $('div').syntaxHighlight();
+ };
+ return expect(highlight).not.toThrow();
+ });
+ });
+ });
+
+}).call(this);
diff --git a/spec/javascripts/syntax_highlight_spec.js.coffee b/spec/javascripts/syntax_highlight_spec.js.coffee
deleted file mode 100644
index 6a73b6bf32c..00000000000
--- a/spec/javascripts/syntax_highlight_spec.js.coffee
+++ /dev/null
@@ -1,42 +0,0 @@
-#= require syntax_highlight
-
-describe 'Syntax Highlighter', ->
- stubUserColorScheme = (value) ->
- window.gon ?= {}
- window.gon.user_color_scheme = value
-
- describe 'on a js-syntax-highlight element', ->
- beforeEach ->
- fixture.set('<div class="js-syntax-highlight"></div>')
-
- it 'applies syntax highlighting', ->
- stubUserColorScheme('monokai')
-
- $('.js-syntax-highlight').syntaxHighlight()
-
- expect($('.js-syntax-highlight')).toHaveClass('monokai')
-
- describe 'on a parent element', ->
- beforeEach ->
- fixture.set """
- <div class="parent">
- <div class="js-syntax-highlight"></div>
- <div class="foo"></div>
- <div class="js-syntax-highlight"></div>
- </div>
- """
-
- it 'applies highlighting to all applicable children', ->
- stubUserColorScheme('monokai')
-
- $('.parent').syntaxHighlight()
-
- expect($('.parent, .foo')).not.toHaveClass('monokai')
- expect($('.monokai').length).toBe(2)
-
- it 'prevents an infinite loop when no matches exist', ->
- fixture.set('<div></div>')
-
- highlight = -> $('div').syntaxHighlight()
-
- expect(highlight).not.toThrow()
diff --git a/spec/javascripts/u2f/authenticate_spec.coffee b/spec/javascripts/u2f/authenticate_spec.coffee
deleted file mode 100644
index e8a2892d678..00000000000
--- a/spec/javascripts/u2f/authenticate_spec.coffee
+++ /dev/null
@@ -1,52 +0,0 @@
-#= 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/authenticate_spec.js b/spec/javascripts/u2f/authenticate_spec.js
new file mode 100644
index 00000000000..e008ce956ad
--- /dev/null
+++ b/spec/javascripts/u2f/authenticate_spec.js
@@ -0,0 +1,75 @@
+
+/*= require u2f/authenticate */
+
+
+/*= require u2f/util */
+
+
+/*= require u2f/error */
+
+
+/*= require u2f */
+
+
+/*= require ./mock_u2f_device */
+
+(function() {
+ describe('U2FAuthenticate', function() {
+ fixture.load('u2f/authenticate');
+ beforeEach(function() {
+ this.u2fDevice = new MockU2FDevice;
+ this.container = $("#js-authenticate-u2f");
+ this.component = new U2FAuthenticate(this.container, {
+ sign_requests: []
+ }, "token");
+ return this.component.start();
+ });
+ it('allows authenticating via a U2F device', function() {
+ var authenticatedMessage, deviceResponse, inProgressMessage, setupButton, setupMessage;
+ setupButton = this.container.find("#js-login-u2f-device");
+ setupMessage = this.container.find("p");
+ expect(setupMessage.text()).toContain('Insert your security key');
+ expect(setupButton.text()).toBe('Login Via U2F Device');
+ setupButton.trigger('click');
+ inProgressMessage = this.container.find("p");
+ expect(inProgressMessage.text()).toContain("Trying to communicate with your device");
+ this.u2fDevice.respondToAuthenticateRequest({
+ deviceData: "this is data from the device"
+ });
+ authenticatedMessage = this.container.find("p");
+ deviceResponse = this.container.find('#js-device-response');
+ expect(authenticatedMessage.text()).toContain("Click this button to authenticate with the GitLab server");
+ return expect(deviceResponse.val()).toBe('{"deviceData":"this is data from the device"}');
+ });
+ return describe("errors", function() {
+ it("displays an error message", function() {
+ var errorMessage, setupButton;
+ setupButton = this.container.find("#js-login-u2f-device");
+ setupButton.trigger('click');
+ this.u2fDevice.respondToAuthenticateRequest({
+ errorCode: "error!"
+ });
+ errorMessage = this.container.find("p");
+ return expect(errorMessage.text()).toContain("There was a problem communicating with your device");
+ });
+ return it("allows retrying authentication after an error", function() {
+ var authenticatedMessage, retryButton, setupButton;
+ setupButton = this.container.find("#js-login-u2f-device");
+ setupButton.trigger('click');
+ this.u2fDevice.respondToAuthenticateRequest({
+ errorCode: "error!"
+ });
+ retryButton = this.container.find("#js-u2f-try-again");
+ retryButton.trigger('click');
+ setupButton = this.container.find("#js-login-u2f-device");
+ setupButton.trigger('click');
+ this.u2fDevice.respondToAuthenticateRequest({
+ deviceData: "this is data from the device"
+ });
+ authenticatedMessage = this.container.find("p");
+ return expect(authenticatedMessage.text()).toContain("Click this button to authenticate with the GitLab server");
+ });
+ });
+ });
+
+}).call(this);
diff --git a/spec/javascripts/u2f/mock_u2f_device.js b/spec/javascripts/u2f/mock_u2f_device.js
new file mode 100644
index 00000000000..ca91a716ba3
--- /dev/null
+++ b/spec/javascripts/u2f/mock_u2f_device.js
@@ -0,0 +1,33 @@
+(function() {
+ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
+
+ this.MockU2FDevice = (function() {
+ function MockU2FDevice() {
+ this.respondToAuthenticateRequest = bind(this.respondToAuthenticateRequest, this);
+ this.respondToRegisterRequest = bind(this.respondToRegisterRequest, this);
+ window.u2f || (window.u2f = {});
+ window.u2f.register = (function(_this) {
+ return function(appId, registerRequests, signRequests, callback) {
+ return _this.registerCallback = callback;
+ };
+ })(this);
+ window.u2f.sign = (function(_this) {
+ return function(appId, challenges, signRequests, callback) {
+ return _this.authenticateCallback = callback;
+ };
+ })(this);
+ }
+
+ MockU2FDevice.prototype.respondToRegisterRequest = function(params) {
+ return this.registerCallback(params);
+ };
+
+ MockU2FDevice.prototype.respondToAuthenticateRequest = function(params) {
+ return this.authenticateCallback(params);
+ };
+
+ return MockU2FDevice;
+
+ })();
+
+}).call(this);
diff --git a/spec/javascripts/u2f/mock_u2f_device.js.coffee b/spec/javascripts/u2f/mock_u2f_device.js.coffee
deleted file mode 100644
index 97ed0e83a0e..00000000000
--- a/spec/javascripts/u2f/mock_u2f_device.js.coffee
+++ /dev/null
@@ -1,15 +0,0 @@
-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 b/spec/javascripts/u2f/register_spec.js
new file mode 100644
index 00000000000..21c5266c60e
--- /dev/null
+++ b/spec/javascripts/u2f/register_spec.js
@@ -0,0 +1,81 @@
+
+/*= require u2f/register */
+
+
+/*= require u2f/util */
+
+
+/*= require u2f/error */
+
+
+/*= require u2f */
+
+
+/*= require ./mock_u2f_device */
+
+(function() {
+ describe('U2FRegister', function() {
+ fixture.load('u2f/register');
+ beforeEach(function() {
+ this.u2fDevice = new MockU2FDevice;
+ this.container = $("#js-register-u2f");
+ this.component = new U2FRegister(this.container, $("#js-register-u2f-templates"), {}, "token");
+ return this.component.start();
+ });
+ it('allows registering a U2F device', function() {
+ var deviceResponse, inProgressMessage, registeredMessage, setupButton;
+ setupButton = this.container.find("#js-setup-u2f-device");
+ expect(setupButton.text()).toBe('Setup New U2F Device');
+ setupButton.trigger('click');
+ inProgressMessage = this.container.children("p");
+ expect(inProgressMessage.text()).toContain("Trying to communicate with your device");
+ this.u2fDevice.respondToRegisterRequest({
+ deviceData: "this is data from the device"
+ });
+ registeredMessage = this.container.find('p');
+ deviceResponse = this.container.find('#js-device-response');
+ expect(registeredMessage.text()).toContain("Your device was successfully set up!");
+ return expect(deviceResponse.val()).toBe('{"deviceData":"this is data from the device"}');
+ });
+ return describe("errors", function() {
+ it("doesn't allow the same device to be registered twice (for the same user", function() {
+ var errorMessage, setupButton;
+ setupButton = this.container.find("#js-setup-u2f-device");
+ setupButton.trigger('click');
+ this.u2fDevice.respondToRegisterRequest({
+ errorCode: 4
+ });
+ errorMessage = this.container.find("p");
+ return expect(errorMessage.text()).toContain("already been registered with us");
+ });
+ it("displays an error message for other errors", function() {
+ var errorMessage, setupButton;
+ setupButton = this.container.find("#js-setup-u2f-device");
+ setupButton.trigger('click');
+ this.u2fDevice.respondToRegisterRequest({
+ errorCode: "error!"
+ });
+ errorMessage = this.container.find("p");
+ return expect(errorMessage.text()).toContain("There was a problem communicating with your device");
+ });
+ return it("allows retrying registration after an error", function() {
+ var registeredMessage, retryButton, setupButton;
+ setupButton = this.container.find("#js-setup-u2f-device");
+ setupButton.trigger('click');
+ this.u2fDevice.respondToRegisterRequest({
+ errorCode: "error!"
+ });
+ retryButton = this.container.find("#U2FTryAgain");
+ retryButton.trigger('click');
+ setupButton = this.container.find("#js-setup-u2f-device");
+ setupButton.trigger('click');
+ this.u2fDevice.respondToRegisterRequest({
+ deviceData: "this is data from the device"
+ });
+ registeredMessage = this.container.find("p");
+ return expect(registeredMessage.text()).toContain("Your device was successfully set up!");
+ });
+ });
+ });
+
+}).call(this);
diff --git a/spec/javascripts/u2f/register_spec.js.coffee b/spec/javascripts/u2f/register_spec.js.coffee
deleted file mode 100644
index 0858abeca1a..00000000000
--- a/spec/javascripts/u2f/register_spec.js.coffee
+++ /dev/null
@@ -1,57 +0,0 @@
-#= 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/javascripts/zen_mode_spec.js b/spec/javascripts/zen_mode_spec.js
new file mode 100644
index 00000000000..3d680ec8ea3
--- /dev/null
+++ b/spec/javascripts/zen_mode_spec.js
@@ -0,0 +1,73 @@
+
+/*= require zen_mode */
+
+(function() {
+ var enterZen, escapeKeydown, exitZen;
+
+ describe('ZenMode', function() {
+ fixture.preload('zen_mode.html');
+ beforeEach(function() {
+ fixture.load('zen_mode.html');
+ spyOn(Dropzone, 'forElement').and.callFake(function() {
+ return {
+ enable: function() {
+ return true;
+ }
+ };
+ });
+ this.zen = new ZenMode();
+ return this.zen.scroll_position = 456;
+ });
+ describe('on enter', function() {
+ it('pauses Mousetrap', function() {
+ spyOn(Mousetrap, 'pause');
+ enterZen();
+ return expect(Mousetrap.pause).toHaveBeenCalled();
+ });
+ return it('removes textarea styling', function() {
+ $('textarea').attr('style', 'height: 400px');
+ enterZen();
+ return expect('textarea').not.toHaveAttr('style');
+ });
+ });
+ describe('in use', function() {
+ beforeEach(function() {
+ return enterZen();
+ });
+ return it('exits on Escape', function() {
+ escapeKeydown();
+ return expect($('.zen-backdrop')).not.toHaveClass('fullscreen');
+ });
+ });
+ return describe('on exit', function() {
+ beforeEach(function() {
+ return enterZen();
+ });
+ it('unpauses Mousetrap', function() {
+ spyOn(Mousetrap, 'unpause');
+ exitZen();
+ return expect(Mousetrap.unpause).toHaveBeenCalled();
+ });
+ return it('restores the scroll position', function() {
+ spyOn(this.zen, 'scrollTo');
+ exitZen();
+ return expect(this.zen.scrollTo).toHaveBeenCalled();
+ });
+ });
+ });
+
+ enterZen = function() {
+ return $('a.js-zen-enter').click();
+ };
+
+ exitZen = function() {
+ return $('a.js-zen-leave').click();
+ };
+
+ escapeKeydown = function() {
+ return $('textarea').trigger($.Event('keydown', {
+ keyCode: 27
+ }));
+ };
+
+}).call(this);
diff --git a/spec/javascripts/zen_mode_spec.js.coffee b/spec/javascripts/zen_mode_spec.js.coffee
deleted file mode 100644
index b790fce01ed..00000000000
--- a/spec/javascripts/zen_mode_spec.js.coffee
+++ /dev/null
@@ -1,51 +0,0 @@
-#= require zen_mode
-
-describe 'ZenMode', ->
- fixture.preload('zen_mode.html')
-
- beforeEach ->
- fixture.load('zen_mode.html')
-
- # Stub Dropzone.forElement(...).enable()
- spyOn(Dropzone, 'forElement').and.callFake ->
- enable: -> true
-
- @zen = new ZenMode()
-
- # Set this manually because we can't actually scroll the window
- @zen.scroll_position = 456
-
- describe 'on enter', ->
- it 'pauses Mousetrap', ->
- spyOn(Mousetrap, 'pause')
- enterZen()
- expect(Mousetrap.pause).toHaveBeenCalled()
-
- it 'removes textarea styling', ->
- $('textarea').attr('style', 'height: 400px')
- enterZen()
- expect('textarea').not.toHaveAttr('style')
-
- describe 'in use', ->
- beforeEach -> enterZen()
-
- it 'exits on Escape', ->
- escapeKeydown()
- expect($('.zen-backdrop')).not.toHaveClass('fullscreen')
-
- describe 'on exit', ->
- beforeEach -> enterZen()
-
- it 'unpauses Mousetrap', ->
- spyOn(Mousetrap, 'unpause')
- exitZen()
- expect(Mousetrap.unpause).toHaveBeenCalled()
-
- it 'restores the scroll position', ->
- spyOn(@zen, 'scrollTo')
- exitZen()
- expect(@zen.scrollTo).toHaveBeenCalled()
-
-enterZen = -> $('a.js-zen-enter').click() # Ohmmmmmmm
-exitZen = -> $('a.js-zen-leave').click()
-escapeKeydown = -> $('textarea').trigger($.Event('keydown', {keyCode: 27}))
diff --git a/spec/lib/banzai/filter/autolink_filter_spec.rb b/spec/lib/banzai/filter/autolink_filter_spec.rb
index 84c2ddf444e..dca7f997570 100644
--- a/spec/lib/banzai/filter/autolink_filter_spec.rb
+++ b/spec/lib/banzai/filter/autolink_filter_spec.rb
@@ -15,6 +15,16 @@ describe Banzai::Filter::AutolinkFilter, lib: true do
expect(filter(act).to_html).to eq exp
end
+ context 'when the input contains no links' do
+ it 'does not parse_html back the rinku returned value' do
+ act = HTML::Pipeline.parse('<p>This text contains no links to autolink</p>')
+
+ expect_any_instance_of(described_class).not_to receive(:parse_html)
+
+ filter(act).to_html
+ end
+ end
+
context 'Rinku schemes' do
it 'autolinks http' do
doc = filter("See #{link}")
@@ -58,6 +68,16 @@ describe Banzai::Filter::AutolinkFilter, lib: true do
expect(filter(act).to_html).to eq exp
end
end
+
+ context 'when the input contains link' do
+ it 'does parse_html back the rinku returned value' do
+ act = HTML::Pipeline.parse("<p>See #{link}</p>")
+
+ expect_any_instance_of(described_class).to receive(:parse_html).at_least(:once).and_call_original
+
+ filter(act).to_html
+ end
+ end
end
context 'other schemes' do
diff --git a/spec/lib/banzai/filter/label_reference_filter_spec.rb b/spec/lib/banzai/filter/label_reference_filter_spec.rb
index 9e3d2f5825d..9276a154007 100644
--- a/spec/lib/banzai/filter/label_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/label_reference_filter_spec.rb
@@ -93,8 +93,8 @@ describe Banzai::Filter::LabelReferenceFilter, lib: true do
end
it 'links with adjacent text' do
- doc = reference_filter("Label (#{reference}.)")
- expect(doc.to_html).to match(%r(\(<a.+><span.+>#{label.name}</span></a>\.\)))
+ doc = reference_filter("Label (#{reference}).")
+ expect(doc.to_html).to match(%r(\(<a.+><span.+>#{label.name}</span></a>\)\.))
end
it 'ignores invalid label names' do
@@ -104,8 +104,32 @@ describe Banzai::Filter::LabelReferenceFilter, lib: true do
end
end
+ context 'String-based single-word references that begin with a digit' do
+ let(:label) { create(:label, name: '2fa', project: project) }
+ let(:reference) { "#{Label.reference_prefix}#{label.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_issues_url(project.namespace, project, label_name: label.name)
+ expect(doc.text).to eq 'See 2fa'
+ end
+
+ it 'links with adjacent text' do
+ doc = reference_filter("Label (#{reference}).")
+ expect(doc.to_html).to match(%r(\(<a.+><span.+>#{label.name}</span></a>\)\.))
+ end
+
+ it 'ignores invalid label names' do
+ exp = act = "Label #{Label.reference_prefix}#{label.id}#{label.name.reverse}"
+
+ expect(reference_filter(act).to_html).to eq exp
+ end
+ end
+
context 'String-based single-word references with special characters' do
- let(:label) { create(:label, name: '?gfm&', project: project) }
+ let(:label) { create(:label, name: '?g.fm&', project: project) }
let(:reference) { "#{Label.reference_prefix}#{label.name}" }
it 'links to a valid reference' do
@@ -113,17 +137,17 @@ describe Banzai::Filter::LabelReferenceFilter, lib: true do
expect(doc.css('a').first.attr('href')).to eq urls.
namespace_project_issues_url(project.namespace, project, label_name: label.name)
- expect(doc.text).to eq 'See ?gfm&'
+ expect(doc.text).to eq 'See ?g.fm&'
end
it 'links with adjacent text' do
- doc = reference_filter("Label (#{reference}.)")
- expect(doc.to_html).to match(%r(\(<a.+><span.+>\?gfm&amp;</span></a>\.\)))
+ doc = reference_filter("Label (#{reference}).")
+ expect(doc.to_html).to match(%r(\(<a.+><span.+>\?g\.fm&amp;</span></a>\)\.))
end
it 'ignores invalid label names' do
act = "Label #{Label.reference_prefix}#{label.name.reverse}"
- exp = "Label #{Label.reference_prefix}&amp;mfg?"
+ exp = "Label #{Label.reference_prefix}&amp;mf.g?"
expect(reference_filter(act).to_html).to eq exp
end
@@ -153,8 +177,32 @@ describe Banzai::Filter::LabelReferenceFilter, lib: true do
end
end
+ context 'String-based multi-word references that begin with a digit' do
+ let(:label) { create(:label, name: '2 factor authentication', project: project) }
+ let(:reference) { label.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_issues_url(project.namespace, project, label_name: label.name)
+ expect(doc.text).to eq 'See 2 factor authentication'
+ end
+
+ it 'links with adjacent text' do
+ doc = reference_filter("Label (#{reference}.)")
+ expect(doc.to_html).to match(%r(\(<a.+><span.+>#{label.name}</span></a>\.\)))
+ end
+
+ it 'ignores invalid label names' do
+ exp = act = "Label #{Label.reference_prefix}#{label.id}#{label.name.reverse}"
+
+ expect(reference_filter(act).to_html).to eq exp
+ end
+ end
+
context 'String-based multi-word references with special characters in quotes' do
- let(:label) { create(:label, name: 'gfm & references?', project: project) }
+ let(:label) { create(:label, name: 'g.fm & references?', project: project) }
let(:reference) { label.to_reference(format: :name) }
it 'links to a valid reference' do
@@ -162,22 +210,62 @@ describe Banzai::Filter::LabelReferenceFilter, lib: true do
expect(doc.css('a').first.attr('href')).to eq urls.
namespace_project_issues_url(project.namespace, project, label_name: label.name)
- expect(doc.text).to eq 'See gfm & references?'
+ expect(doc.text).to eq 'See g.fm & references?'
end
it 'links with adjacent text' do
doc = reference_filter("Label (#{reference}.)")
- expect(doc.to_html).to match(%r(\(<a.+><span.+>gfm &amp; references\?</span></a>\.\)))
+ expect(doc.to_html).to match(%r(\(<a.+><span.+>g\.fm &amp; references\?</span></a>\.\)))
end
it 'ignores invalid label names' do
act = %(Label #{Label.reference_prefix}"#{label.name.reverse}")
- exp = %(Label #{Label.reference_prefix}"?secnerefer &amp; mfg\")
+ exp = %(Label #{Label.reference_prefix}"?secnerefer &amp; mf.g\")
expect(reference_filter(act).to_html).to eq exp
end
end
+ describe 'consecutive references' do
+ let(:bug) { create(:label, name: 'bug', project: project) }
+ let(:feature_proposal) { create(:label, name: 'feature proposal', project: project) }
+ let(:technical_debt) { create(:label, name: 'technical debt', project: project) }
+
+ let(:bug_reference) { "#{Label.reference_prefix}#{bug.name}" }
+ let(:feature_proposal_reference) { feature_proposal.to_reference(format: :name) }
+ let(:technical_debt_reference) { technical_debt.to_reference(format: :name) }
+
+ context 'separated with a comma' do
+ let(:references) { "#{bug_reference}, #{feature_proposal_reference}, #{technical_debt_reference}" }
+
+ it 'links to valid references' do
+ doc = reference_filter("See #{references}")
+
+ expect(doc.css('a').map { |a| a.attr('href') }).to match_array([
+ urls.namespace_project_issues_url(project.namespace, project, label_name: bug.name),
+ urls.namespace_project_issues_url(project.namespace, project, label_name: feature_proposal.name),
+ urls.namespace_project_issues_url(project.namespace, project, label_name: technical_debt.name)
+ ])
+ expect(doc.text).to eq 'See bug, feature proposal, technical debt'
+ end
+ end
+
+ context 'separated with a space' do
+ let(:references) { "#{bug_reference} #{feature_proposal_reference} #{technical_debt_reference}" }
+
+ it 'links to valid references' do
+ doc = reference_filter("See #{references}")
+
+ expect(doc.css('a').map { |a| a.attr('href') }).to match_array([
+ urls.namespace_project_issues_url(project.namespace, project, label_name: bug.name),
+ urls.namespace_project_issues_url(project.namespace, project, label_name: feature_proposal.name),
+ urls.namespace_project_issues_url(project.namespace, project, label_name: technical_debt.name)
+ ])
+ expect(doc.text).to eq 'See bug feature proposal technical debt'
+ end
+ end
+ end
+
describe 'edge cases' do
it 'gracefully handles non-references matching the pattern' do
exp = act = '(format nil "~0f" 3.0) ; 3.0'
diff --git a/spec/lib/banzai/filter/relative_link_filter_spec.rb b/spec/lib/banzai/filter/relative_link_filter_spec.rb
index b9e4a4eaf0e..6b58f3e43ee 100644
--- a/spec/lib/banzai/filter/relative_link_filter_spec.rb
+++ b/spec/lib/banzai/filter/relative_link_filter_spec.rb
@@ -1,11 +1,9 @@
-# encoding: UTF-8
-
require 'spec_helper'
describe Banzai::Filter::RelativeLinkFilter, lib: true do
def filter(doc, contexts = {})
contexts.reverse_merge!({
- commit: project.commit,
+ commit: commit,
project: project,
project_wiki: project_wiki,
ref: ref,
@@ -19,6 +17,10 @@ describe Banzai::Filter::RelativeLinkFilter, lib: true do
%(<img src="#{path}" />)
end
+ def video(path)
+ %(<video src="#{path}"></video>)
+ end
+
def link(path)
%(<a href="#{path}">#{path}</a>)
end
@@ -26,6 +28,7 @@ describe Banzai::Filter::RelativeLinkFilter, lib: true do
let(:project) { create(:project) }
let(:project_path) { project.path_with_namespace }
let(:ref) { 'markdown' }
+ let(:commit) { project.commit(ref) }
let(:project_wiki) { nil }
let(:requested_path) { '/' }
@@ -39,6 +42,12 @@ describe Banzai::Filter::RelativeLinkFilter, lib: true do
doc = filter(image('files/images/logo-black.png'))
expect(doc.at_css('img')['src']).to eq 'files/images/logo-black.png'
end
+
+ it 'does not modify any relative URL in video' do
+ doc = filter(video('files/videos/intro.mp4'), commit: project.commit('video'), ref: 'video')
+
+ expect(doc.at_css('video')['src']).to eq 'files/videos/intro.mp4'
+ end
end
shared_examples :relative_to_requested do
@@ -69,13 +78,36 @@ describe Banzai::Filter::RelativeLinkFilter, lib: true do
expect { filter(act) }.not_to raise_error
end
- context 'with a valid repository' do
+ it 'ignores ref if commit is passed' do
+ doc = filter(link('non/existent.file'), commit: project.commit('empty-branch') )
+ expect(doc.at_css('a')['href']).
+ to eq "/#{project_path}/#{ref}/non/existent.file" # non-existent files have no leading blob/raw/tree
+ end
+
+ shared_examples :valid_repository do
+ it 'rebuilds absolute URL for a file in the repo' do
+ doc = filter(link('/doc/api/README.md'))
+ expect(doc.at_css('a')['href']).
+ to eq "/#{project_path}/blob/#{ref}/doc/api/README.md"
+ end
+
+ it 'ignores absolute URLs with two leading slashes' do
+ doc = filter(link('//doc/api/README.md'))
+ expect(doc.at_css('a')['href']).to eq '//doc/api/README.md'
+ end
+
it 'rebuilds relative URL for a file in the repo' do
doc = filter(link('doc/api/README.md'))
expect(doc.at_css('a')['href']).
to eq "/#{project_path}/blob/#{ref}/doc/api/README.md"
end
+ it 'rebuilds relative URL for a file in the repo with leading ./' do
+ doc = filter(link('./doc/api/README.md'))
+ expect(doc.at_css('a')['href']).
+ to eq "/#{project_path}/blob/#{ref}/doc/api/README.md"
+ end
+
it 'rebuilds relative URL for a file in the repo up one directory' do
relative_link = link('../api/README.md')
doc = filter(relative_link, requested_path: 'doc/update/7.14-to-8.0.md')
@@ -113,11 +145,26 @@ describe Banzai::Filter::RelativeLinkFilter, lib: true do
end
it 'rebuilds relative URL for an image in the repo' do
+ doc = filter(image('files/images/logo-black.png'))
+
+ expect(doc.at_css('img')['src']).
+ to eq "/#{project_path}/raw/#{ref}/files/images/logo-black.png"
+ end
+
+ it 'rebuilds relative URL for link to an image in the repo' do
doc = filter(link('files/images/logo-black.png'))
+
expect(doc.at_css('a')['href']).
to eq "/#{project_path}/raw/#{ref}/files/images/logo-black.png"
end
+ it 'rebuilds relative URL for a video in the repo' do
+ doc = filter(video('files/videos/intro.mp4'), commit: project.commit('video'), ref: 'video')
+
+ expect(doc.at_css('video')['src']).
+ to eq "/#{project_path}/raw/video/files/videos/intro.mp4"
+ end
+
it 'does not modify relative URL with an anchor only' do
doc = filter(link('#section-1'))
expect(doc.at_css('a')['href']).to eq '#section-1'
@@ -149,4 +196,13 @@ describe Banzai::Filter::RelativeLinkFilter, lib: true do
include_examples :relative_to_requested
end
end
+
+ context 'with a valid commit' do
+ include_examples :valid_repository
+ end
+
+ context 'with a valid ref' do
+ let(:commit) { nil } # force filter to use ref instead of commit
+ include_examples :valid_repository
+ end
end
diff --git a/spec/lib/banzai/filter/syntax_highlight_filter_spec.rb b/spec/lib/banzai/filter/syntax_highlight_filter_spec.rb
index 407617f3307..b1370bca833 100644
--- a/spec/lib/banzai/filter/syntax_highlight_filter_spec.rb
+++ b/spec/lib/banzai/filter/syntax_highlight_filter_spec.rb
@@ -3,15 +3,35 @@ require 'spec_helper'
describe Banzai::Filter::SyntaxHighlightFilter, lib: true do
include FilterSpecHelper
- it 'highlights valid code blocks' do
- result = filter('<pre><code>def fun end</code>')
- expect(result.to_html).to eq("<pre class=\"code highlight js-syntax-highlight plaintext\"><code>def fun end</code></pre>\n")
+ context "when no language is specified" do
+ it "highlights as plaintext" do
+ result = filter('<pre><code>def fun end</code></pre>')
+ expect(result.to_html).to eq('<pre class="code highlight js-syntax-highlight plaintext"><code>def fun end</code></pre>')
+ end
end
- it 'passes through invalid code blocks' do
- allow_any_instance_of(described_class).to receive(:block_code).and_raise(StandardError)
+ context "when a valid language is specified" do
+ it "highlights as that language" do
+ result = filter('<pre><code class="ruby">def fun end</code></pre>')
+ expect(result.to_html).to eq('<pre class="code highlight js-syntax-highlight ruby"><code><span class="k">def</span> <span class="nf">fun</span> <span class="k">end</span></code></pre>')
+ end
+ end
+
+ context "when an invalid language is specified" do
+ it "highlights as plaintext" do
+ result = filter('<pre><code class="gnuplot">This is a test</code></pre>')
+ expect(result.to_html).to eq('<pre class="code highlight js-syntax-highlight plaintext"><code>This is a test</code></pre>')
+ end
+ end
+
+ context "when Rouge formatting fails" do
+ before do
+ allow_any_instance_of(Rouge::Formatter).to receive(:format).and_raise(StandardError)
+ end
- result = filter('<pre><code>This is a test</code></pre>')
- expect(result.to_html).to eq('<pre>This is a test</pre>')
+ it "highlights as plaintext" do
+ result = filter('<pre><code class="ruby">This is a test</code></pre>')
+ expect(result.to_html).to eq('<pre class="code highlight"><code>This is a test</code></pre>')
+ end
end
end
diff --git a/spec/lib/banzai/filter/table_of_contents_filter_spec.rb b/spec/lib/banzai/filter/table_of_contents_filter_spec.rb
index 6a5d003e87f..356dd01a03a 100644
--- a/spec/lib/banzai/filter/table_of_contents_filter_spec.rb
+++ b/spec/lib/banzai/filter/table_of_contents_filter_spec.rb
@@ -1,5 +1,3 @@
-# encoding: UTF-8
-
require 'spec_helper'
describe Banzai::Filter::TableOfContentsFilter, lib: true do
diff --git a/spec/lib/banzai/filter/upload_link_filter_spec.rb b/spec/lib/banzai/filter/upload_link_filter_spec.rb
index 273d2ed709a..8b76c1d73c9 100644
--- a/spec/lib/banzai/filter/upload_link_filter_spec.rb
+++ b/spec/lib/banzai/filter/upload_link_filter_spec.rb
@@ -1,5 +1,3 @@
-# encoding: UTF-8
-
require 'spec_helper'
describe Banzai::Filter::UploadLinkFilter, lib: true do
diff --git a/spec/lib/banzai/filter/video_link_filter_spec.rb b/spec/lib/banzai/filter/video_link_filter_spec.rb
new file mode 100644
index 00000000000..6ab1be9ccb7
--- /dev/null
+++ b/spec/lib/banzai/filter/video_link_filter_spec.rb
@@ -0,0 +1,50 @@
+require 'spec_helper'
+
+describe Banzai::Filter::VideoLinkFilter, lib: true do
+ def filter(doc, contexts = {})
+ contexts.reverse_merge!({
+ project: project
+ })
+
+ described_class.call(doc, contexts)
+ end
+
+ def link_to_image(path)
+ %(<img src="#{path}" />)
+ end
+
+ let(:project) { create(:project) }
+
+ context 'when the element src has a video extension' do
+ UploaderHelper::VIDEO_EXT.each do |ext|
+ it "replaces the image tag 'path/video.#{ext}' with a video tag" do
+ container = filter(link_to_image("/path/video.#{ext}")).children.first
+
+ expect(container.name).to eq 'div'
+ expect(container['class']).to eq 'video-container'
+
+ video, paragraph = container.children
+
+ expect(video.name).to eq 'video'
+ expect(video['src']).to eq "/path/video.#{ext}"
+
+ expect(paragraph.name).to eq 'p'
+
+ link = paragraph.children.first
+
+ expect(link.name).to eq 'a'
+ expect(link['href']).to eq "/path/video.#{ext}"
+ expect(link['target']).to eq '_blank'
+ end
+ end
+ end
+
+ context 'when the element src is an image' do
+ it 'leaves the document unchanged' do
+ element = filter(link_to_image('/path/my_image.jpg')).children.first
+
+ expect(element.name).to eq 'img'
+ expect(element['src']).to eq '/path/my_image.jpg'
+ end
+ end
+end
diff --git a/spec/lib/banzai/object_renderer_spec.rb b/spec/lib/banzai/object_renderer_spec.rb
index 44256b32bdc..bcdb95250ca 100644
--- a/spec/lib/banzai/object_renderer_spec.rb
+++ b/spec/lib/banzai/object_renderer_spec.rb
@@ -17,6 +17,7 @@ describe Banzai::ObjectRenderer do
and_call_original
expect(object).to receive(:note_html=).with('<p>hello</p>')
+ expect(object).to receive(:user_visible_reference_count=).with(0)
renderer.render([object], :note)
end
@@ -25,9 +26,10 @@ describe Banzai::ObjectRenderer do
describe '#render_objects' do
it 'renders an Array of objects' do
object = double(:object, note: 'hello')
+
renderer = described_class.new(project, user)
- expect(renderer).to receive(:render_attribute).with(object, :note).
+ expect(renderer).to receive(:render_attributes).with([object], :note).
and_call_original
rendered = renderer.render_objects([object], :note)
@@ -38,7 +40,7 @@ describe Banzai::ObjectRenderer do
end
describe '#redact_documents' do
- it 'redacts a set of documents and returns them as an Array of Strings' do
+ it 'redacts a set of documents and returns them as an Array of Hashes' do
doc = Nokogiri::HTML.fragment('<p>hello</p>')
renderer = described_class.new(project, user)
@@ -48,7 +50,9 @@ describe Banzai::ObjectRenderer do
redacted = renderer.redact_documents([doc])
- expect(redacted).to eq(['<p>hello</p>'])
+ expect(redacted.count).to eq(1)
+ expect(redacted.first[:visible_reference_count]).to eq(0)
+ expect(redacted.first[:document].to_html).to eq('<p>hello</p>')
end
end
@@ -85,14 +89,36 @@ describe Banzai::ObjectRenderer do
end
end
- describe '#render_attribute' do
- it 'renders the attribute of an object' do
- object = double(:doc, note: 'hello')
+ describe '#render_attributes' do
+ it 'renders the attribute of a list of objects' do
+ objects = [double(:doc, note: 'hello'), double(:doc, note: 'bye')]
renderer = described_class.new(project, user, pipeline: :note)
- doc = renderer.render_attribute(object, :note)
- expect(doc).to be_an_instance_of(Nokogiri::HTML::DocumentFragment)
- expect(doc.to_html).to eq('<p>hello</p>')
+ expect(Banzai).to receive(:cache_collection_render).
+ with([
+ { text: 'hello', context: renderer.context_for(objects[0], :note) },
+ { text: 'bye', context: renderer.context_for(objects[1], :note) }
+ ]).
+ and_call_original
+
+ docs = renderer.render_attributes(objects, :note)
+
+ expect(docs[0]).to be_an_instance_of(Nokogiri::HTML::DocumentFragment)
+ expect(docs[0].to_html).to eq('<p>hello</p>')
+
+ expect(docs[1]).to be_an_instance_of(Nokogiri::HTML::DocumentFragment)
+ expect(docs[1].to_html).to eq('<p>bye</p>')
+ end
+
+ it 'returns when no objects to render' do
+ objects = []
+ renderer = described_class.new(project, user, pipeline: :note)
+
+ expect(Banzai).to receive(:cache_collection_render).
+ with([]).
+ and_call_original
+
+ expect(renderer.render_attributes(objects, :note)).to eq([])
end
end
diff --git a/spec/lib/banzai/redactor_spec.rb b/spec/lib/banzai/redactor_spec.rb
index 488f465bcda..254657a881d 100644
--- a/spec/lib/banzai/redactor_spec.rb
+++ b/spec/lib/banzai/redactor_spec.rb
@@ -15,11 +15,31 @@ describe Banzai::Redactor do
expect(redactor).to receive(:nodes_visible_to_user).and_return([])
- expect(redactor.redact([doc1, doc2])).to eq([doc1, doc2])
+ redacted_data = redactor.redact([doc1, doc2])
+ expect(redacted_data.map { |data| data[:document] }).to eq([doc1, doc2])
+ expect(redacted_data.map { |data| data[:visible_reference_count] }).to eq([0, 0])
expect(doc1.to_html).to eq('foo')
expect(doc2.to_html).to eq('bar')
end
+
+ it 'does not redact an Array of documents' do
+ doc1_html = '<a class="gfm" data-reference-type="issue">foo</a>'
+ doc1 = Nokogiri::HTML.fragment(doc1_html)
+
+ doc2_html = '<a class="gfm" data-reference-type="issue">bar</a>'
+ doc2 = Nokogiri::HTML.fragment(doc2_html)
+
+ nodes = redactor.document_nodes([doc1, doc2]).map { |x| x[:nodes] }
+ expect(redactor).to receive(:nodes_visible_to_user).and_return(nodes.flatten)
+
+ redacted_data = redactor.redact([doc1, doc2])
+
+ expect(redacted_data.map { |data| data[:document] }).to eq([doc1, doc2])
+ expect(redacted_data.map { |data| data[:visible_reference_count] }).to eq([1, 1])
+ expect(doc1.to_html).to eq(doc1_html)
+ expect(doc2.to_html).to eq(doc2_html)
+ end
end
describe '#redact_nodes' do
@@ -31,7 +51,7 @@ describe Banzai::Redactor do
with([node]).
and_return(Set.new)
- redactor.redact_nodes([node])
+ redactor.redact_document_nodes([{ document: doc, nodes: [node] }])
expect(doc.to_html).to eq('foo')
end
diff --git a/spec/lib/banzai/reference_parser/issue_parser_spec.rb b/spec/lib/banzai/reference_parser/issue_parser_spec.rb
index 514c752546d..85cfe728b6a 100644
--- a/spec/lib/banzai/reference_parser/issue_parser_spec.rb
+++ b/spec/lib/banzai/reference_parser/issue_parser_spec.rb
@@ -16,17 +16,17 @@ describe Banzai::ReferenceParser::IssueParser, lib: true do
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(Ability).to receive(:issues_readable_by_user).
+ with([issue], user).
+ and_return([issue])
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(Ability).to receive(:issues_readable_by_user).
+ with([issue], user).
+ and_return([])
expect(subject.nodes_visible_to_user(user, [link])).to eq([])
end
diff --git a/spec/lib/ci/charts_spec.rb b/spec/lib/ci/charts_spec.rb
index 97f2e97b062..fb6cc398307 100644
--- a/spec/lib/ci/charts_spec.rb
+++ b/spec/lib/ci/charts_spec.rb
@@ -2,21 +2,23 @@ require 'spec_helper'
describe Ci::Charts, lib: true do
context "build_times" do
+ let(:project) { create(:empty_project) }
+ let(:chart) { Ci::Charts::BuildTime.new(project) }
+
+ subject { chart.build_times }
+
before do
- @pipeline = FactoryGirl.create(:ci_pipeline)
- FactoryGirl.create(:ci_build, pipeline: @pipeline)
+ create(:ci_empty_pipeline, project: project, duration: 120)
end
- it 'should return build times in minutes' do
- chart = Ci::Charts::BuildTime.new(@pipeline.project)
- expect(chart.build_times).to eq([2])
+ it 'returns build times in minutes' do
+ is_expected.to contain_exactly(2)
end
- it 'should handle nil build times' do
- create(:ci_pipeline, duration: nil, project: @pipeline.project)
+ it 'handles nil build times' do
+ create(:ci_empty_pipeline, project: project, duration: nil)
- chart = Ci::Charts::BuildTime.new(@pipeline.project)
- expect(chart.build_times).to eq([2, 0])
+ is_expected.to contain_exactly(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 bad439bc489..be51d942af7 100644
--- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
+++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
@@ -19,19 +19,18 @@ module Ci
expect(config_processor.builds_for_stage_and_ref(type, "master").first).to eq({
stage: "test",
stage_idx: 1,
- except: nil,
- name: :rspec,
- only: nil,
+ name: "rspec",
commands: "pwd\nrspec",
tag_list: [],
options: {},
allow_failure: false,
when: "on_success",
environment: nil,
+ yaml_variables: []
})
end
- describe :only do
+ describe 'only' do
it "does not return builds if only has another branch" do
config = YAML.dump({
before_script: ["pwd"],
@@ -163,7 +162,7 @@ module Ci
shared_examples 'raises an error' do
it do
- expect { processor }.to raise_error(GitlabCiYamlProcessor::ValidationError, 'rspec job: only parameter should be an array of strings or regexps')
+ expect { processor }.to raise_error(GitlabCiYamlProcessor::ValidationError, 'jobs:rspec:only config should be an array of strings or regexps')
end
end
@@ -187,7 +186,7 @@ module Ci
end
end
- describe :except do
+ describe 'except' do
it "returns builds if except has another branch" do
config = YAML.dump({
before_script: ["pwd"],
@@ -319,7 +318,7 @@ module Ci
shared_examples 'raises an error' do
it do
- expect { processor }.to raise_error(GitlabCiYamlProcessor::ValidationError, 'rspec job: except parameter should be an array of strings or regexps')
+ expect { processor }.to raise_error(GitlabCiYamlProcessor::ValidationError, 'jobs:rspec:except config should be an array of strings or regexps')
end
end
@@ -432,11 +431,9 @@ module Ci
expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1)
expect(config_processor.builds_for_stage_and_ref("test", "master").first).to eq({
- except: nil,
stage: "test",
stage_idx: 1,
- name: :rspec,
- only: nil,
+ name: "rspec",
commands: "pwd\nrspec",
tag_list: [],
options: {
@@ -446,6 +443,7 @@ module Ci
allow_failure: false,
when: "on_success",
environment: nil,
+ yaml_variables: []
})
end
@@ -461,11 +459,9 @@ module Ci
expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1)
expect(config_processor.builds_for_stage_and_ref("test", "master").first).to eq({
- except: nil,
stage: "test",
stage_idx: 1,
- name: :rspec,
- only: nil,
+ name: "rspec",
commands: "pwd\nrspec",
tag_list: [],
options: {
@@ -475,101 +471,122 @@ module Ci
allow_failure: false,
when: "on_success",
environment: nil,
+ yaml_variables: []
})
end
end
describe 'Variables' do
- context 'when global variables are defined' do
- it 'returns global variables' do
- variables = {
- VAR1: 'value1',
- VAR2: 'value2',
- }
+ let(:config_processor) { GitlabCiYamlProcessor.new(YAML.dump(config), path) }
- config = YAML.dump({
+ subject { config_processor.builds.first[:yaml_variables] }
+
+ context 'when global variables are defined' do
+ let(:variables) do
+ { VAR1: 'value1', VAR2: 'value2' }
+ end
+ let(:config) do
+ {
variables: variables,
before_script: ['pwd'],
rspec: { script: 'rspec' }
- })
+ }
+ end
- config_processor = GitlabCiYamlProcessor.new(config, path)
+ it 'returns global variables' do
+ expect(subject).to contain_exactly(
+ { key: :VAR1, value: 'value1', public: true },
+ { key: :VAR2, value: 'value2', public: true }
+ )
+ end
+ end
- expect(config_processor.global_variables).to eq(variables)
+ context 'when job and global variables are defined' do
+ let(:global_variables) do
+ { VAR1: 'global1', VAR3: 'global3' }
+ end
+ let(:job_variables) do
+ { VAR1: 'value1', VAR2: 'value2' }
+ end
+ let(:config) do
+ {
+ before_script: ['pwd'],
+ variables: global_variables,
+ rspec: { script: 'rspec', variables: job_variables }
+ }
+ end
+
+ it 'returns all unique variables' do
+ expect(subject).to contain_exactly(
+ { key: :VAR3, value: 'global3', public: true },
+ { key: :VAR1, value: 'value1', public: true },
+ { key: :VAR2, value: 'value2', public: true }
+ )
end
end
context 'when job variables are defined' do
- context 'when syntax is correct' do
- it 'returns job variables' do
- variables = {
- KEY1: 'value1',
- SOME_KEY_2: 'value2'
- }
-
- config = YAML.dump(
- { before_script: ['pwd'],
- rspec: {
- variables: variables,
- script: 'rspec' }
- })
+ let(:config) do
+ {
+ before_script: ['pwd'],
+ rspec: { script: 'rspec', variables: variables }
+ }
+ end
- config_processor = GitlabCiYamlProcessor.new(config, path)
+ context 'when syntax is correct' do
+ let(:variables) do
+ { VAR1: 'value1', VAR2: 'value2' }
+ end
- expect(config_processor.job_variables(:rspec)).to eq variables
+ it 'returns job variables' do
+ expect(subject).to contain_exactly(
+ { key: :VAR1, value: 'value1', public: true },
+ { key: :VAR2, value: 'value2', public: true }
+ )
end
end
context 'when syntax is incorrect' do
context 'when variables defined but invalid' do
- it 'raises error' do
- variables = [:KEY1, 'value1', :KEY2, 'value2']
-
- config = YAML.dump(
- { before_script: ['pwd'],
- rspec: {
- variables: variables,
- script: 'rspec' }
- })
+ let(:variables) do
+ [ :VAR1, 'value1', :VAR2, 'value2' ]
+ end
- expect { GitlabCiYamlProcessor.new(config, path) }
+ it 'raises error' do
+ expect { subject }
.to raise_error(GitlabCiYamlProcessor::ValidationError,
- /job: variables should be a map/)
+ /jobs:rspec:variables config should be a hash of key value pairs/)
end
end
context 'when variables key defined but value not specified' do
- it 'returns empty array' do
- config = YAML.dump(
- { before_script: ['pwd'],
- rspec: {
- variables: nil,
- script: 'rspec' }
- })
-
- config_processor = GitlabCiYamlProcessor.new(config, path)
+ let(:variables) do
+ nil
+ end
+ it 'returns empty array' do
##
# When variables config is empty, we assume this is a valid
# configuration, see issue #18775
#
- expect(config_processor.job_variables(:rspec))
- .to be_an_instance_of(Array).and be_empty
+ expect(subject).to be_an_instance_of(Array)
+ expect(subject).to be_empty
end
end
end
end
context 'when job variables are not defined' do
- it 'returns empty array' do
- config = YAML.dump({
+ let(:config) do
+ {
before_script: ['pwd'],
rspec: { script: 'rspec' }
- })
-
- config_processor = GitlabCiYamlProcessor.new(config, path)
+ }
+ end
- expect(config_processor.job_variables(:rspec)).to eq []
+ it 'returns empty array' do
+ expect(subject).to be_an_instance_of(Array)
+ expect(subject).to be_empty
end
end
end
@@ -681,11 +698,9 @@ module Ci
expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1)
expect(config_processor.builds_for_stage_and_ref("test", "master").first).to eq({
- except: nil,
stage: "test",
stage_idx: 1,
- name: :rspec,
- only: nil,
+ name: "rspec",
commands: "pwd\nrspec",
tag_list: [],
options: {
@@ -701,6 +716,7 @@ module Ci
when: "on_success",
allow_failure: false,
environment: nil,
+ yaml_variables: []
})
end
@@ -754,7 +770,7 @@ module Ci
let(:environment) { 1 }
it 'raises error' do
- expect { builds }.to raise_error("deploy_to_production job: environment parameter #{Gitlab::Regex.environment_name_regex_message}")
+ expect { builds }.to raise_error("jobs:deploy_to_production environment #{Gitlab::Regex.environment_name_regex_message}")
end
end
@@ -762,7 +778,7 @@ module Ci
let(:environment) { 'production staging' }
it 'raises error' do
- expect { builds }.to raise_error("deploy_to_production job: environment parameter #{Gitlab::Regex.environment_name_regex_message}")
+ expect { builds }.to raise_error("jobs:deploy_to_production environment #{Gitlab::Regex.environment_name_regex_message}")
end
end
end
@@ -819,17 +835,16 @@ module Ci
it "doesn't create jobs that start with dot" do
expect(subject.size).to eq(1)
expect(subject.first).to eq({
- except: nil,
stage: "test",
stage_idx: 1,
- name: :normal_job,
- only: nil,
+ name: "normal_job",
commands: "test",
tag_list: [],
options: {},
when: "on_success",
allow_failure: false,
environment: nil,
+ yaml_variables: []
})
end
end
@@ -865,30 +880,28 @@ module Ci
it "is correctly supported for jobs" do
expect(subject.size).to eq(2)
expect(subject.first).to eq({
- except: nil,
stage: "build",
stage_idx: 0,
- name: :job1,
- only: nil,
+ name: "job1",
commands: "execute-script-for-job",
tag_list: [],
options: {},
when: "on_success",
allow_failure: false,
environment: nil,
+ yaml_variables: []
})
expect(subject.second).to eq({
- except: nil,
stage: "build",
stage_idx: 0,
- name: :job2,
- only: nil,
+ name: "job2",
commands: "execute-script-for-job",
tag_list: [],
options: {},
when: "on_success",
allow_failure: false,
environment: nil,
+ yaml_variables: []
})
end
end
@@ -956,7 +969,7 @@ EOT
config = YAML.dump({ rspec: { script: "test", tags: "mysql" } })
expect do
GitlabCiYamlProcessor.new(config, path)
- end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: tags parameter should be an array of strings")
+ end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec tags should be an array of strings")
end
it "returns errors if before_script parameter is invalid" do
@@ -970,7 +983,7 @@ EOT
config = YAML.dump({ rspec: { script: "test", before_script: [10, "test"] } })
expect do
GitlabCiYamlProcessor.new(config, path)
- end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: before_script should be an array of strings")
+ end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:before_script config should be an array of strings")
end
it "returns errors if after_script parameter is invalid" do
@@ -984,7 +997,7 @@ EOT
config = YAML.dump({ rspec: { script: "test", after_script: [10, "test"] } })
expect do
GitlabCiYamlProcessor.new(config, path)
- end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: after_script should be an array of strings")
+ end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:after_script config should be an array of strings")
end
it "returns errors if image parameter is invalid" do
@@ -998,21 +1011,21 @@ EOT
config = YAML.dump({ '' => { script: "test" } })
expect do
GitlabCiYamlProcessor.new(config, path)
- end.to raise_error(GitlabCiYamlProcessor::ValidationError, "job name should be non-empty string")
+ end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:job name can't be blank")
end
it "returns errors if job name is non-string" do
config = YAML.dump({ 10 => { script: "test" } })
expect do
GitlabCiYamlProcessor.new(config, path)
- end.to raise_error(GitlabCiYamlProcessor::ValidationError, "job name should be non-empty string")
+ end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:10 name should be a symbol")
end
it "returns errors if job image parameter is invalid" do
config = YAML.dump({ rspec: { script: "test", image: ["test"] } })
expect do
GitlabCiYamlProcessor.new(config, path)
- end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: image should be a string")
+ end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:image config should be a string")
end
it "returns errors if services parameter is not an array" do
@@ -1033,49 +1046,56 @@ EOT
config = YAML.dump({ rspec: { script: "test", services: "test" } })
expect do
GitlabCiYamlProcessor.new(config, path)
- end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: services should be an array of strings")
+ end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:services config should be an array of strings")
end
it "returns errors if job services parameter is not an array of strings" do
config = YAML.dump({ rspec: { script: "test", services: [10, "test"] } })
expect do
GitlabCiYamlProcessor.new(config, path)
- end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: services should be an array of strings")
+ end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:services config should be an array of strings")
end
- it "returns errors if there are unknown parameters" do
+ it "returns error if job configuration is invalid" do
config = YAML.dump({ extra: "bundle update" })
expect do
GitlabCiYamlProcessor.new(config, path)
- end.to raise_error(GitlabCiYamlProcessor::ValidationError, "Unknown parameter: extra")
+ end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:extra config should be a hash")
end
it "returns errors if there are unknown parameters that are hashes, but doesn't have a script" do
config = YAML.dump({ extra: { services: "test" } })
expect do
GitlabCiYamlProcessor.new(config, path)
- end.to raise_error(GitlabCiYamlProcessor::ValidationError, "Unknown parameter: extra")
+ end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:extra:services config should be an array of strings")
end
it "returns errors if there are no jobs defined" do
config = YAML.dump({ before_script: ["bundle update"] })
expect do
GitlabCiYamlProcessor.new(config, path)
- end.to raise_error(GitlabCiYamlProcessor::ValidationError, "Please define at least one job")
+ end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs config should contain at least one visible job")
+ end
+
+ it "returns errors if there are no visible jobs defined" do
+ config = YAML.dump({ before_script: ["bundle update"], '.hidden'.to_sym => { script: 'ls' } })
+ expect do
+ GitlabCiYamlProcessor.new(config, path)
+ end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs config should contain at least one visible job")
end
it "returns errors if job allow_failure parameter is not an boolean" do
config = YAML.dump({ rspec: { script: "test", allow_failure: "string" } })
expect do
GitlabCiYamlProcessor.new(config, path)
- end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: allow_failure parameter should be an boolean")
+ end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec allow failure should be a boolean value")
end
it "returns errors if job stage is not a string" do
config = YAML.dump({ rspec: { script: "test", type: 1 } })
expect do
GitlabCiYamlProcessor.new(config, path)
- end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: stage parameter should be build, test, deploy")
+ end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:type config should be a string")
end
it "returns errors if job stage is not a pre-defined stage" do
@@ -1124,49 +1144,49 @@ EOT
config = YAML.dump({ rspec: { script: "test", when: 1 } })
expect do
GitlabCiYamlProcessor.new(config, path)
- end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: when parameter should be on_success, on_failure or always")
+ end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec when should be on_success, on_failure, always or manual")
end
it "returns errors if job artifacts:name is not an a string" do
config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", artifacts: { name: 1 } } })
expect do
GitlabCiYamlProcessor.new(config)
- end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: artifacts:name parameter should be a string")
+ end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:artifacts name 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.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:artifacts when 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.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:artifacts expire in 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.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:artifacts expire in 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
GitlabCiYamlProcessor.new(config)
- end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: artifacts:untracked parameter should be an boolean")
+ end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:artifacts untracked should be a boolean value")
end
it "returns errors if job artifacts:paths is not an array of strings" do
config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", artifacts: { paths: "string" } } })
expect do
GitlabCiYamlProcessor.new(config)
- end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: artifacts:paths parameter should be an array of strings")
+ end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:artifacts paths should be an array of strings")
end
it "returns errors if cache:untracked is not an array of strings" do
@@ -1194,28 +1214,28 @@ EOT
config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", cache: { key: 1 } } })
expect do
GitlabCiYamlProcessor.new(config)
- end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: cache:key parameter should be a string")
+ end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:cache:key config should be a string or symbol")
end
it "returns errors if job cache:untracked is not an array of strings" do
config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", cache: { untracked: "string" } } })
expect do
GitlabCiYamlProcessor.new(config)
- end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: cache:untracked parameter should be an boolean")
+ end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:cache:untracked config should be a boolean value")
end
it "returns errors if job cache:paths is not an array of strings" do
config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", cache: { paths: "string" } } })
expect do
GitlabCiYamlProcessor.new(config)
- end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: cache:paths parameter should be an array of strings")
+ end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:cache:paths config should be an array of strings")
end
it "returns errors if job dependencies is not an array of strings" do
config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", dependencies: "string" } })
expect do
GitlabCiYamlProcessor.new(config)
- end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: dependencies parameter should be an array of strings")
+ end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec dependencies should be an array of strings")
end
end
diff --git a/spec/lib/container_registry/blob_spec.rb b/spec/lib/container_registry/blob_spec.rb
index 4d8cb787dde..bbacdc67ebd 100644
--- a/spec/lib/container_registry/blob_spec.rb
+++ b/spec/lib/container_registry/blob_spec.rb
@@ -9,8 +9,9 @@ describe ContainerRegistry::Blob do
'size' => 1000
}
end
+ let(:token) { 'authorization-token' }
- let(:registry) { ContainerRegistry::Registry.new('http://example.com') }
+ let(:registry) { ContainerRegistry::Registry.new('http://example.com', token: token) }
let(:repository) { registry.repository('group/test') }
let(:blob) { repository.blob(config) }
@@ -58,4 +59,53 @@ describe ContainerRegistry::Blob do
it { is_expected.to be_truthy }
end
+
+ context '#data' do
+ let(:data) { '{"key":"value"}' }
+
+ subject { blob.data }
+
+ context 'when locally stored' do
+ before do
+ stub_request(:get, 'http://example.com/v2/group/test/blobs/sha256:0123456789012345').
+ to_return(
+ status: 200,
+ headers: { 'Content-Type' => 'application/json' },
+ body: data)
+ end
+
+ it { is_expected.to eq(data) }
+ end
+
+ context 'when externally stored' do
+ before do
+ stub_request(:get, 'http://example.com/v2/group/test/blobs/sha256:0123456789012345').
+ with(headers: { 'Authorization' => "bearer #{token}" }).
+ to_return(
+ status: 307,
+ headers: { 'Location' => location })
+ end
+
+ context 'for a valid address' do
+ let(:location) { 'http://external.com/blob/file' }
+
+ before do
+ stub_request(:get, location).
+ with(headers: { 'Authorization' => nil }).
+ to_return(
+ status: 200,
+ headers: { 'Content-Type' => 'application/json' },
+ body: data)
+ end
+
+ it { is_expected.to eq(data) }
+ end
+
+ context 'for invalid file' do
+ let(:location) { 'file:///etc/passwd' }
+
+ it { expect{ subject }.to raise_error(ArgumentError, 'invalid address') }
+ end
+ end
+ end
end
diff --git a/spec/lib/container_registry/tag_spec.rb b/spec/lib/container_registry/tag_spec.rb
index c7324c2bf77..c5e31ae82b6 100644
--- a/spec/lib/container_registry/tag_spec.rb
+++ b/spec/lib/container_registry/tag_spec.rb
@@ -77,24 +77,47 @@ describe ContainerRegistry::Tag do
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
+ shared_examples 'a processable' do
+ context '#config' do
+ subject { tag.config }
- 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 }
+ it { is_expected.not_to be_nil }
+ end
end
- context '#created_at' do
- subject { tag.created_at }
+ context 'when locally stored' 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
+
+ it_behaves_like 'a processable'
+ end
- it { is_expected.not_to be_nil }
+ context 'when externally stored' do
+ before do
+ stub_request(:get, 'http://example.com/v2/group/test/blobs/sha256:d7a513a663c1a6dcdba9ed832ca53c02ac2af0c333322cd6ca92936d1d9917ac').
+ with(headers: { 'Accept' => 'application/octet-stream' }).
+ to_return(
+ status: 307,
+ headers: { 'Location' => 'http://external.com/blob/file' })
+
+ stub_request(:get, 'http://external.com/blob/file').
+ to_return(
+ status: 200,
+ body: File.read(Rails.root + 'spec/fixtures/container_registry/config_blob.json'))
+ end
+
+ it_behaves_like 'a processable'
end
end
end
diff --git a/spec/lib/disable_email_interceptor_spec.rb b/spec/lib/disable_email_interceptor_spec.rb
index 309a88151cf..8f51474476d 100644
--- a/spec/lib/disable_email_interceptor_spec.rb
+++ b/spec/lib/disable_email_interceptor_spec.rb
@@ -5,7 +5,7 @@ describe DisableEmailInterceptor, lib: true do
Mail.register_interceptor(DisableEmailInterceptor)
end
- it 'should not send emails' do
+ it 'does not send emails' do
allow(Gitlab.config.gitlab).to receive(:email_enabled).and_return(false)
expect { deliver_mail }.not_to change(ActionMailer::Base.deliveries, :count)
end
diff --git a/spec/lib/extracts_path_spec.rb b/spec/lib/extracts_path_spec.rb
index 566035c60d0..e10c1f5c547 100644
--- a/spec/lib/extracts_path_spec.rb
+++ b/spec/lib/extracts_path_spec.rb
@@ -25,18 +25,40 @@ describe ExtractsPath, lib: true do
@project = create(:project)
end
- it "log tree path should have no escape sequences" do
+ it "log tree path has no escape sequences" do
assign_ref_vars
expect(@logs_path).to eq("/#{@project.path_with_namespace}/refs/#{ref}/logs_tree/files/ruby/popen.rb")
end
- context 'escaped sequences in ref' do
- let(:ref) { "improve%2Fawesome" }
+ context 'ref contains %20' do
+ let(:ref) { 'foo%20bar' }
- it "id should have no escape sequences" do
+ it 'is not converted to a space in @id' do
+ @project.repository.add_branch(@project.owner, 'foo%20bar', 'master')
+
+ assign_ref_vars
+
+ expect(@id).to start_with('foo%20bar/')
+ end
+ end
+
+ context 'path contains space' do
+ let(:params) { { path: 'with space', ref: '38008cb17ce1466d8fec2dfa6f6ab8dcfe5cf49e' } }
+
+ it 'is not converted to %20 in @path' do
assign_ref_vars
- expect(@ref).to eq('improve/awesome')
- expect(@logs_path).to eq("/#{@project.path_with_namespace}/refs/#{ref}/logs_tree/files/ruby/popen.rb")
+
+ expect(@path).to eq(params[:path])
+ end
+ end
+
+ context 'subclass overrides get_id' do
+ it 'uses ref returned by get_id' do
+ allow_any_instance_of(self.class).to receive(:get_id){ '38008cb17ce1466d8fec2dfa6f6ab8dcfe5cf49e' }
+
+ assign_ref_vars
+
+ expect(@id).to eq(get_id)
end
end
end
diff --git a/spec/lib/gitlab/akismet_helper_spec.rb b/spec/lib/gitlab/akismet_helper_spec.rb
deleted file mode 100644
index 88a71528867..00000000000
--- a/spec/lib/gitlab/akismet_helper_spec.rb
+++ /dev/null
@@ -1,35 +0,0 @@
-require 'spec_helper'
-
-describe Gitlab::AkismetHelper, type: :helper do
- let(:project) { create(:project) }
- let(:user) { create(:user) }
-
- before do
- allow(Gitlab.config.gitlab).to receive(:url).and_return(Settings.send(:build_gitlab_url))
- 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
- it 'returns true for non-member' do
- expect(helper.check_for_spam?(project, user)).to eq(true)
- end
-
- it 'returns false for member' do
- project.team << [user, :guest]
- expect(helper.check_for_spam?(project, user)).to eq(false)
- end
- end
-
- describe '#is_spam?' do
- it 'returns true for spam' do
- environment = {
- 'action_dispatch.remote_ip' => '127.0.0.1',
- 'HTTP_USER_AGENT' => 'Test User Agent'
- }
-
- allow_any_instance_of(::Akismet::Client).to receive(:check).and_return([true, true])
- expect(helper.is_spam?(environment, user, 'Is this spam?')).to eq(true)
- end
- end
-end
diff --git a/spec/lib/gitlab/asciidoc_spec.rb b/spec/lib/gitlab/asciidoc_spec.rb
index 32ca8239845..4aba783dc33 100644
--- a/spec/lib/gitlab/asciidoc_spec.rb
+++ b/spec/lib/gitlab/asciidoc_spec.rb
@@ -8,7 +8,7 @@ module Gitlab
let(:html) { 'H<sub>2</sub>O' }
context "without project" do
- it "should convert the input using Asciidoctor and default options" do
+ it "converts the input using Asciidoctor and default options" do
expected_asciidoc_opts = {
safe: :secure,
backend: :html5,
@@ -24,7 +24,7 @@ module Gitlab
context "with asciidoc_opts" do
let(:asciidoc_opts) { { safe: :safe, attributes: ['foo'] } }
- it "should merge the options with default ones" do
+ it "merges the options with default ones" do
expected_asciidoc_opts = {
safe: :safe,
backend: :html5,
diff --git a/spec/lib/gitlab/auth_spec.rb b/spec/lib/gitlab/auth_spec.rb
index 7bec1367156..b0772cad312 100644
--- a/spec/lib/gitlab/auth_spec.rb
+++ b/spec/lib/gitlab/auth_spec.rb
@@ -51,24 +51,24 @@ describe Gitlab::Auth, lib: true do
let(:username) { 'John' } # username isn't lowercase, test this
let(:password) { 'my-secret' }
- it "should find user by valid login/password" do
+ it "finds user by valid login/password" do
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
+ it 'finds user by valid email/password with case-insensitive email' do
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
+ it 'finds user by valid username/password with case-insensitive username' do
expect(gl_auth.find_with_user_password(username.upcase, password)).to eql user
end
- it "should not find user with invalid password" do
+ it "does not find user with invalid password" do
password = 'wrong'
expect( gl_auth.find_with_user_password(username, password) ).not_to eql user
end
- it "should not find user with invalid login" do
+ it "does not find user with invalid login" do
user = 'wrong'
expect( gl_auth.find_with_user_password(username, password) ).not_to eql user
end
diff --git a/spec/lib/gitlab/badge/build/metadata_spec.rb b/spec/lib/gitlab/badge/build/metadata_spec.rb
new file mode 100644
index 00000000000..d678e522721
--- /dev/null
+++ b/spec/lib/gitlab/badge/build/metadata_spec.rb
@@ -0,0 +1,27 @@
+require 'spec_helper'
+require 'lib/gitlab/badge/shared/metadata'
+
+describe Gitlab::Badge::Build::Metadata do
+ let(:badge) { double(project: create(:project), ref: 'feature') }
+ let(:metadata) { described_class.new(badge) }
+
+ it_behaves_like 'badge metadata'
+
+ describe '#title' do
+ it 'returns build status title' do
+ expect(metadata.title).to eq 'build status'
+ end
+ end
+
+ describe '#image_url' do
+ it 'returns valid url' do
+ expect(metadata.image_url).to include 'badges/feature/build.svg'
+ end
+ end
+
+ describe '#link_url' do
+ it 'returns valid link' do
+ expect(metadata.link_url).to include 'commits/feature'
+ end
+ end
+end
diff --git a/spec/lib/gitlab/badge/build/status_spec.rb b/spec/lib/gitlab/badge/build/status_spec.rb
new file mode 100644
index 00000000000..38eebb2a176
--- /dev/null
+++ b/spec/lib/gitlab/badge/build/status_spec.rb
@@ -0,0 +1,94 @@
+require 'spec_helper'
+
+describe Gitlab::Badge::Build::Status do
+ let(:project) { create(:project) }
+ let(:sha) { project.commit.sha }
+ let(:branch) { 'master' }
+ let(:badge) { described_class.new(project, branch) }
+
+ describe '#entity' do
+ it 'always says build' do
+ expect(badge.entity).to eq 'build'
+ end
+ end
+
+ describe '#template' do
+ it 'returns badge template' do
+ expect(badge.template.key_text).to eq 'build'
+ end
+ end
+
+ describe '#metadata' do
+ it 'returns badge metadata' do
+ expect(badge.metadata.image_url)
+ .to include 'badges/master/build.svg'
+ end
+ end
+
+ context 'build exists' do
+ let!(:build) { create_build(project, sha, branch) }
+
+ context 'build success' do
+ before { build.success! }
+
+ describe '#status' do
+ it 'is successful' do
+ expect(badge.status).to eq 'success'
+ end
+ end
+ end
+
+ context 'build failed' do
+ before { build.drop! }
+
+ describe '#status' do
+ it 'failed' do
+ expect(badge.status).to eq 'failed'
+ end
+ end
+ end
+
+ context 'when outdated pipeline for given ref exists' do
+ before do
+ build.success!
+
+ old_build = create_build(project, '11eeffdd', branch)
+ old_build.drop!
+ end
+
+ it 'does not take outdated pipeline into account' do
+ expect(badge.status).to eq 'success'
+ end
+ end
+
+ context 'when multiple pipelines exist for given sha' do
+ before do
+ build.drop!
+
+ new_build = create_build(project, sha, branch)
+ new_build.success!
+ end
+
+ it 'reports the compound status' do
+ expect(badge.status).to eq 'failed'
+ end
+ end
+ end
+
+ context 'build does not exist' do
+ describe '#status' do
+ it 'is unknown' do
+ expect(badge.status).to eq 'unknown'
+ end
+ end
+ end
+
+ def create_build(project, sha, branch)
+ pipeline = create(:ci_empty_pipeline,
+ project: project,
+ sha: sha,
+ ref: branch)
+
+ create(:ci_build, pipeline: pipeline, stage: 'notify')
+ end
+end
diff --git a/spec/lib/gitlab/badge/build/template_spec.rb b/spec/lib/gitlab/badge/build/template_spec.rb
new file mode 100644
index 00000000000..a7e21fb8bb1
--- /dev/null
+++ b/spec/lib/gitlab/badge/build/template_spec.rb
@@ -0,0 +1,82 @@
+require 'spec_helper'
+
+describe Gitlab::Badge::Build::Template do
+ let(:badge) { double(entity: 'build', status: 'success') }
+ let(:template) { described_class.new(badge) }
+
+ describe '#key_text' do
+ it 'is always says build' do
+ expect(template.key_text).to eq 'build'
+ end
+ end
+
+ describe '#value_text' do
+ it 'is status value' do
+ expect(template.value_text).to eq 'success'
+ end
+ end
+
+ describe 'widths and text anchors' do
+ it 'has fixed width and text anchors' do
+ expect(template.width).to eq 92
+ expect(template.key_width).to eq 38
+ expect(template.value_width).to eq 54
+ expect(template.key_text_anchor).to eq 19
+ expect(template.value_text_anchor).to eq 65
+ end
+ end
+
+ describe '#key_color' do
+ it 'is always the same' do
+ expect(template.key_color).to eq '#555'
+ end
+ end
+
+ describe '#value_color' do
+ context 'when status is success' do
+ it 'has expected color' do
+ expect(template.value_color).to eq '#4c1'
+ end
+ end
+
+ context 'when status is failed' do
+ before do
+ allow(badge).to receive(:status).and_return('failed')
+ end
+
+ it 'has expected color' do
+ expect(template.value_color).to eq '#e05d44'
+ end
+ end
+
+ context 'when status is running' do
+ before do
+ allow(badge).to receive(:status).and_return('running')
+ end
+
+ it 'has expected color' do
+ expect(template.value_color).to eq '#dfb317'
+ end
+ end
+
+ context 'when status is unknown' do
+ before do
+ allow(badge).to receive(:status).and_return('unknown')
+ end
+
+ it 'has expected color' do
+ expect(template.value_color).to eq '#9f9f9f'
+ end
+ end
+
+ context 'when status does not match any known statuses' do
+ before do
+ allow(badge).to receive(:status).and_return('invalid')
+ end
+
+ it 'has expected color' do
+ expect(template.value_color).to eq '#9f9f9f'
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/badge/build_spec.rb b/spec/lib/gitlab/badge/build_spec.rb
deleted file mode 100644
index 2034445a197..00000000000
--- a/spec/lib/gitlab/badge/build_spec.rb
+++ /dev/null
@@ -1,123 +0,0 @@
-require 'spec_helper'
-
-describe Gitlab::Badge::Build do
- let(:project) { create(:project) }
- let(:sha) { project.commit.sha }
- let(:branch) { 'master' }
- let(:badge) { described_class.new(project, branch) }
-
- describe '#type' do
- subject { badge.type }
- it { is_expected.to eq 'image/svg+xml' }
- end
-
- describe '#to_html' do
- let(:html) { Nokogiri::HTML.parse(badge.to_html) }
- let(:a_href) { html.at('a') }
-
- it 'points to link' do
- expect(a_href[:href]).to eq badge.link_url
- end
-
- it 'contains clickable image' do
- expect(a_href.children.first.name).to eq 'img'
- end
- end
-
- describe '#to_markdown' do
- subject { badge.to_markdown }
-
- it { is_expected.to include badge.image_url }
- it { is_expected.to include badge.link_url }
- end
-
- describe '#image_url' do
- subject { badge.image_url }
- it { is_expected.to include "badges/#{branch}/build.svg" }
- end
-
- describe '#link_url' do
- subject { badge.link_url }
- it { is_expected.to include "commits/#{branch}" }
- end
-
- context 'build exists' do
- let!(:build) { create_build(project, sha, branch) }
-
- context 'build success' do
- before { build.success! }
-
- describe '#to_s' do
- subject { badge.to_s }
- it { is_expected.to eq 'build-success' }
- end
-
- describe '#data' do
- let(:data) { badge.data }
-
- it 'contains information about success' do
- expect(status_node(data, 'success')).to be_truthy
- end
- end
- end
-
- context 'build failed' do
- before { build.drop! }
-
- describe '#to_s' do
- subject { badge.to_s }
- it { is_expected.to eq 'build-failed' }
- end
-
- describe '#data' do
- let(:data) { badge.data }
-
- it 'contains information about failure' do
- expect(status_node(data, 'failed')).to be_truthy
- end
- end
- end
- end
-
- context 'build does not exist' do
- describe '#to_s' do
- subject { badge.to_s }
- it { is_expected.to eq 'build-unknown' }
- end
-
- describe '#data' do
- let(:data) { badge.data }
-
- it 'contains infromation about unknown build' do
- expect(status_node(data, 'unknown')).to be_truthy
- end
- 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}")})
- end
-end
diff --git a/spec/lib/gitlab/badge/coverage/metadata_spec.rb b/spec/lib/gitlab/badge/coverage/metadata_spec.rb
new file mode 100644
index 00000000000..74eaf7eaf8b
--- /dev/null
+++ b/spec/lib/gitlab/badge/coverage/metadata_spec.rb
@@ -0,0 +1,30 @@
+require 'spec_helper'
+require 'lib/gitlab/badge/shared/metadata'
+
+describe Gitlab::Badge::Coverage::Metadata do
+ let(:badge) do
+ double(project: create(:project), ref: 'feature', job: 'test')
+ end
+
+ let(:metadata) { described_class.new(badge) }
+
+ it_behaves_like 'badge metadata'
+
+ describe '#title' do
+ it 'returns coverage report title' do
+ expect(metadata.title).to eq 'coverage report'
+ end
+ end
+
+ describe '#image_url' do
+ it 'returns valid url' do
+ expect(metadata.image_url).to include 'badges/feature/coverage.svg'
+ end
+ end
+
+ describe '#link_url' do
+ it 'returns valid link' do
+ expect(metadata.link_url).to include 'commits/feature'
+ end
+ end
+end
diff --git a/spec/lib/gitlab/badge/coverage/report_spec.rb b/spec/lib/gitlab/badge/coverage/report_spec.rb
new file mode 100644
index 00000000000..ab0cce6e091
--- /dev/null
+++ b/spec/lib/gitlab/badge/coverage/report_spec.rb
@@ -0,0 +1,106 @@
+require 'spec_helper'
+
+describe Gitlab::Badge::Coverage::Report do
+ let(:project) { create(:project) }
+ let(:job_name) { nil }
+
+ let(:badge) do
+ described_class.new(project, 'master', job_name)
+ end
+
+ describe '#entity' do
+ it 'describes a coverage' do
+ expect(badge.entity).to eq 'coverage'
+ end
+ end
+
+ describe '#metadata' do
+ it 'returns correct metadata' do
+ expect(badge.metadata.image_url).to include 'coverage.svg'
+ end
+ end
+
+ describe '#template' do
+ it 'returns correct template' do
+ expect(badge.template.key_text).to eq 'coverage'
+ end
+ end
+
+ shared_examples 'unknown coverage report' do
+ context 'particular job specified' do
+ let(:job_name) { '' }
+
+ it 'returns nil' do
+ expect(badge.status).to be_nil
+ end
+ end
+
+ context 'particular job not specified' do
+ let(:job_name) { nil }
+
+ it 'returns nil' do
+ expect(badge.status).to be_nil
+ end
+ end
+ end
+
+ context 'when latest successful pipeline exists' do
+ before do
+ create_pipeline do |pipeline|
+ create(:ci_build, :success, pipeline: pipeline, name: 'first', coverage: 40)
+ create(:ci_build, :success, pipeline: pipeline, coverage: 60)
+ end
+
+ create_pipeline do |pipeline|
+ create(:ci_build, :failed, pipeline: pipeline, coverage: 10)
+ end
+ end
+
+ context 'when particular job specified' do
+ let(:job_name) { 'first' }
+
+ it 'returns coverage for the particular job' do
+ expect(badge.status).to eq 40
+ end
+ end
+
+ context 'when particular job not specified' do
+ let(:job_name) { '' }
+
+ it 'returns arithemetic mean for the pipeline' do
+ expect(badge.status).to eq 50
+ end
+ end
+ end
+
+ context 'when only failed pipeline exists' do
+ before do
+ create_pipeline do |pipeline|
+ create(:ci_build, :failed, pipeline: pipeline, coverage: 10)
+ end
+ end
+
+ it_behaves_like 'unknown coverage report'
+
+ context 'particular job specified' do
+ let(:job_name) { 'nonexistent' }
+
+ it 'retruns nil' do
+ expect(badge.status).to be_nil
+ end
+ end
+ end
+
+ context 'pipeline does not exist' do
+ it_behaves_like 'unknown coverage report'
+ end
+
+ def create_pipeline
+ opts = { project: project, sha: project.commit.id, ref: 'master' }
+
+ create(:ci_pipeline, opts).tap do |pipeline|
+ yield pipeline
+ pipeline.build_updated
+ end
+ end
+end
diff --git a/spec/lib/gitlab/badge/coverage/template_spec.rb b/spec/lib/gitlab/badge/coverage/template_spec.rb
new file mode 100644
index 00000000000..383bae6e087
--- /dev/null
+++ b/spec/lib/gitlab/badge/coverage/template_spec.rb
@@ -0,0 +1,130 @@
+require 'spec_helper'
+
+describe Gitlab::Badge::Coverage::Template do
+ let(:badge) { double(entity: 'coverage', status: 90) }
+ let(:template) { described_class.new(badge) }
+
+ describe '#key_text' do
+ it 'is always says coverage' do
+ expect(template.key_text).to eq 'coverage'
+ end
+ end
+
+ describe '#value_text' do
+ context 'when coverage is known' do
+ it 'returns coverage percentage' do
+ expect(template.value_text).to eq '90%'
+ end
+ end
+
+ context 'when coverage is unknown' do
+ before do
+ allow(badge).to receive(:status).and_return(nil)
+ end
+
+ it 'returns string that says coverage is unknown' do
+ expect(template.value_text).to eq 'unknown'
+ end
+ end
+ end
+
+ describe '#key_width' do
+ it 'has a fixed key width' do
+ expect(template.key_width).to eq 62
+ end
+ end
+
+ describe '#value_width' do
+ context 'when coverage is known' do
+ it 'is narrower when coverage is known' do
+ expect(template.value_width).to eq 36
+ end
+ end
+
+ context 'when coverage is unknown' do
+ before do
+ allow(badge).to receive(:status).and_return(nil)
+ end
+
+ it 'is wider when coverage is unknown to fit text' do
+ expect(template.value_width).to eq 58
+ end
+ end
+ end
+
+ describe '#key_color' do
+ it 'always has the same color' do
+ expect(template.key_color).to eq '#555'
+ end
+ end
+
+ describe '#value_color' do
+ context 'when coverage is good' do
+ before do
+ allow(badge).to receive(:status).and_return(98)
+ end
+
+ it 'is green' do
+ expect(template.value_color).to eq '#4c1'
+ end
+ end
+
+ context 'when coverage is acceptable' do
+ before do
+ allow(badge).to receive(:status).and_return(90)
+ end
+
+ it 'is green-orange' do
+ expect(template.value_color).to eq '#a3c51c'
+ end
+ end
+
+ context 'when coverage is medium' do
+ before do
+ allow(badge).to receive(:status).and_return(75)
+ end
+
+ it 'is orange-yellow' do
+ expect(template.value_color).to eq '#dfb317'
+ end
+ end
+
+ context 'when coverage is low' do
+ before do
+ allow(badge).to receive(:status).and_return(50)
+ end
+
+ it 'is red' do
+ expect(template.value_color).to eq '#e05d44'
+ end
+ end
+
+ context 'when coverage is unknown' do
+ before do
+ allow(badge).to receive(:status).and_return(nil)
+ end
+
+ it 'is grey' do
+ expect(template.value_color).to eq '#9f9f9f'
+ end
+ end
+ end
+
+ describe '#width' do
+ context 'when coverage is known' do
+ it 'returns the key width plus value width' do
+ expect(template.width).to eq 98
+ end
+ end
+
+ context 'when coverage is unknown' do
+ before do
+ allow(badge).to receive(:status).and_return(nil)
+ end
+
+ it 'returns key width plus wider value width' do
+ expect(template.width).to eq 120
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/badge/shared/metadata.rb b/spec/lib/gitlab/badge/shared/metadata.rb
new file mode 100644
index 00000000000..0cf18514251
--- /dev/null
+++ b/spec/lib/gitlab/badge/shared/metadata.rb
@@ -0,0 +1,21 @@
+shared_examples 'badge metadata' do
+ describe '#to_html' do
+ let(:html) { Nokogiri::HTML.parse(metadata.to_html) }
+ let(:a_href) { html.at('a') }
+
+ it 'points to link' do
+ expect(a_href[:href]).to eq metadata.link_url
+ end
+
+ it 'contains clickable image' do
+ expect(a_href.children.first.name).to eq 'img'
+ end
+ end
+
+ describe '#to_markdown' do
+ subject { metadata.to_markdown }
+
+ it { is_expected.to include metadata.image_url }
+ it { is_expected.to include metadata.link_url }
+ end
+end
diff --git a/spec/lib/gitlab/bitbucket_import/client_spec.rb b/spec/lib/gitlab/bitbucket_import/client_spec.rb
index 760d66a1488..7543c29bcc4 100644
--- a/spec/lib/gitlab/bitbucket_import/client_spec.rb
+++ b/spec/lib/gitlab/bitbucket_import/client_spec.rb
@@ -54,12 +54,12 @@ describe Gitlab::BitbucketImport::Client, lib: true do
context 'project import' do
it 'calls .from_project with no errors' do
project = create(:empty_project)
+ project.import_url = "ssh://git@bitbucket.org/test/test.git"
project.create_or_update_import_data(credentials:
{ user: "git",
password: nil,
bb_session: { bitbucket_access_token: "test",
bitbucket_access_token_secret: "test" } })
- project.import_url = "ssh://git@bitbucket.org/test/test.git"
expect { described_class.from_project(project) }.not_to raise_error
end
diff --git a/spec/lib/gitlab/changes_list_spec.rb b/spec/lib/gitlab/changes_list_spec.rb
new file mode 100644
index 00000000000..69d86144e32
--- /dev/null
+++ b/spec/lib/gitlab/changes_list_spec.rb
@@ -0,0 +1,30 @@
+require "spec_helper"
+
+describe Gitlab::ChangesList do
+ let(:valid_changes_string) { "\n000000 570e7b2 refs/heads/my_branch\nd14d6c 6fd24d refs/heads/master" }
+ let(:invalid_changes) { 1 }
+
+ context 'when changes is a valid string' do
+ let(:changes_list) { Gitlab::ChangesList.new(valid_changes_string) }
+
+ it 'splits elements by newline character' do
+ expect(changes_list).to contain_exactly({
+ oldrev: "000000",
+ newrev: "570e7b2",
+ ref: "refs/heads/my_branch"
+ }, {
+ oldrev: "d14d6c",
+ newrev: "6fd24d",
+ ref: "refs/heads/master"
+ })
+ end
+
+ it 'behaves like a list' do
+ expect(changes_list.first).to eq({
+ oldrev: "000000",
+ newrev: "570e7b2",
+ ref: "refs/heads/my_branch"
+ })
+ end
+ end
+end
diff --git a/spec/lib/gitlab/checks/change_access_spec.rb b/spec/lib/gitlab/checks/change_access_spec.rb
new file mode 100644
index 00000000000..39069b49978
--- /dev/null
+++ b/spec/lib/gitlab/checks/change_access_spec.rb
@@ -0,0 +1,99 @@
+require 'spec_helper'
+
+describe Gitlab::Checks::ChangeAccess, lib: true do
+ describe '#exec' do
+ let(:user) { create(:user) }
+ let(:project) { create(:project) }
+ let(:user_access) { Gitlab::UserAccess.new(user, project: project) }
+ let(:changes) do
+ {
+ oldrev: 'be93687618e4b132087f430a4d8fc3a609c9b77c',
+ newrev: '54fcc214b94e78d7a41a9a8fe6d87a5e59500e51',
+ ref: 'refs/heads/master'
+ }
+ end
+
+ subject { described_class.new(changes, project: project, user_access: user_access).exec }
+
+ before { allow(user_access).to receive(:can_do_action?).with(:push_code).and_return(true) }
+
+ context 'without failed checks' do
+ it "doesn't return any error" do
+ expect(subject.status).to be(true)
+ end
+ end
+
+ context 'when the user is not allowed to push code' do
+ it 'returns an error' do
+ expect(user_access).to receive(:can_do_action?).with(:push_code).and_return(false)
+
+ expect(subject.status).to be(false)
+ expect(subject.message).to eq('You are not allowed to push code to this project.')
+ end
+ end
+
+ context 'tags check' do
+ let(:changes) do
+ {
+ oldrev: 'be93687618e4b132087f430a4d8fc3a609c9b77c',
+ newrev: '54fcc214b94e78d7a41a9a8fe6d87a5e59500e51',
+ ref: 'refs/tags/v1.0.0'
+ }
+ end
+
+ it 'returns an error if the user is not allowed to update tags' do
+ expect(user_access).to receive(:can_do_action?).with(:admin_project).and_return(false)
+
+ expect(subject.status).to be(false)
+ expect(subject.message).to eq('You are not allowed to change existing tags on this project.')
+ end
+ end
+
+ context 'protected branches check' do
+ before do
+ allow(project).to receive(:protected_branch?).with('master').and_return(true)
+ end
+
+ it 'returns an error if the user is not allowed to do forced pushes to protected branches' do
+ expect(Gitlab::Checks::ForcePush).to receive(:force_push?).and_return(true)
+ expect(user_access).to receive(:can_do_action?).with(:force_push_code_to_protected_branches).and_return(false)
+
+ expect(subject.status).to be(false)
+ expect(subject.message).to eq('You are not allowed to force push code to a protected branch on this project.')
+ end
+
+ it 'returns an error if the user is not allowed to merge to protected branches' do
+ expect_any_instance_of(Gitlab::Checks::MatchingMergeRequest).to receive(:match?).and_return(true)
+ expect(user_access).to receive(:can_merge_to_branch?).and_return(false)
+ expect(user_access).to receive(:can_push_to_branch?).and_return(false)
+
+ expect(subject.status).to be(false)
+ expect(subject.message).to eq('You are not allowed to merge code into protected branches on this project.')
+ end
+
+ it 'returns an error if the user is not allowed to push to protected branches' do
+ expect(user_access).to receive(:can_push_to_branch?).and_return(false)
+
+ expect(subject.status).to be(false)
+ expect(subject.message).to eq('You are not allowed to push code to protected branches on this project.')
+ end
+
+ context 'branch deletion' do
+ let(:changes) do
+ {
+ oldrev: 'be93687618e4b132087f430a4d8fc3a609c9b77c',
+ newrev: '0000000000000000000000000000000000000000',
+ ref: 'refs/heads/master'
+ }
+ end
+
+ it 'returns an error if the user is not allowed to delete protected branches' do
+ expect(user_access).to receive(:can_do_action?).with(:remove_protected_branches).and_return(false)
+
+ expect(subject.status).to be(false)
+ expect(subject.message).to eq('You are not allowed to delete protected branches from this project.')
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/config/node/artifacts_spec.rb b/spec/lib/gitlab/ci/config/node/artifacts_spec.rb
new file mode 100644
index 00000000000..c09a0a9c793
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/node/artifacts_spec.rb
@@ -0,0 +1,45 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Config::Node::Artifacts do
+ let(:entry) { described_class.new(config) }
+
+ describe 'validation' do
+ context 'when entry config value is correct' do
+ let(:config) { { paths: %w[public/] } }
+
+ describe '#value' do
+ it 'returns artifacs configuration' do
+ expect(entry.value).to eq config
+ 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
+ describe '#errors' do
+ context 'when value of attribute is invalid' do
+ let(:config) { { name: 10 } }
+
+ it 'reports error' do
+ expect(entry.errors)
+ .to include 'artifacts name should be a string'
+ end
+ end
+
+ context 'when there is an unknown key present' do
+ let(:config) { { test: 100 } }
+
+ it 'reports error' do
+ expect(entry.errors)
+ .to include 'artifacts config contains unknown keys: test'
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/config/node/attributable_spec.rb b/spec/lib/gitlab/ci/config/node/attributable_spec.rb
new file mode 100644
index 00000000000..24d9daafd88
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/node/attributable_spec.rb
@@ -0,0 +1,43 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Config::Node::Attributable do
+ let(:node) { Class.new }
+ let(:instance) { node.new }
+
+ before do
+ node.include(described_class)
+
+ node.class_eval do
+ attributes :name, :test
+ end
+ end
+
+ context 'config is a hash' do
+ before do
+ allow(instance)
+ .to receive(:config)
+ .and_return({ name: 'some name', test: 'some test' })
+ end
+
+ it 'returns the value of config' do
+ expect(instance.name).to eq 'some name'
+ expect(instance.test).to eq 'some test'
+ end
+
+ it 'returns no method error for unknown attributes' do
+ expect { instance.unknown }.to raise_error(NoMethodError)
+ end
+ end
+
+ context 'config is not a hash' do
+ before do
+ allow(instance)
+ .to receive(:config)
+ .and_return('some test')
+ end
+
+ it 'returns nil' do
+ expect(instance.test).to be_nil
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/config/node/commands_spec.rb b/spec/lib/gitlab/ci/config/node/commands_spec.rb
new file mode 100644
index 00000000000..e373c40706f
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/node/commands_spec.rb
@@ -0,0 +1,49 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Config::Node::Commands do
+ let(:entry) { described_class.new(config) }
+
+ context 'when entry config value is an array' do
+ let(:config) { ['ls', 'pwd'] }
+
+ describe '#value' do
+ it 'returns array of strings' do
+ expect(entry.value).to eq config
+ end
+ end
+
+ describe '#errors' do
+ it 'does not append errors' do
+ expect(entry.errors).to be_empty
+ end
+ end
+ end
+
+ context 'when entry config value is a string' do
+ let(:config) { 'ls' }
+
+ describe '#value' do
+ it 'returns array with single element' do
+ expect(entry.value).to eq ['ls']
+ end
+ end
+
+ describe '#valid?' do
+ it 'is valid' do
+ expect(entry).to be_valid
+ end
+ end
+ end
+
+ context 'when entry value is not valid' do
+ let(:config) { 1 }
+
+ describe '#errors' do
+ it 'saves errors' do
+ expect(entry.errors)
+ .to include 'commands config should be a ' \
+ 'string or an array of strings'
+ 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
index 91ddef7bfbf..d26185ba585 100644
--- a/spec/lib/gitlab/ci/config/node/factory_spec.rb
+++ b/spec/lib/gitlab/ci/config/node/factory_spec.rb
@@ -2,13 +2,13 @@ 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 }
+ let(:factory) { described_class.new(node) }
+ let(:node) { Gitlab::Ci::Config::Node::Script }
- context 'when setting up a value' do
+ context 'when setting a concrete value' do
it 'creates entry with valid value' do
entry = factory
- .with(value: ['ls', 'pwd'])
+ .value(['ls', 'pwd'])
.create!
expect(entry.value).to eq ['ls', 'pwd']
@@ -17,7 +17,7 @@ describe Gitlab::Ci::Config::Node::Factory do
context 'when setting description' do
it 'creates entry with description' do
entry = factory
- .with(value: ['ls', 'pwd'])
+ .value(['ls', 'pwd'])
.with(description: 'test description')
.create!
@@ -29,7 +29,8 @@ describe Gitlab::Ci::Config::Node::Factory do
context 'when setting key' do
it 'creates entry with custom key' do
entry = factory
- .with(value: ['ls', 'pwd'], key: 'test key')
+ .value(['ls', 'pwd'])
+ .with(key: 'test key')
.create!
expect(entry.key).to eq 'test key'
@@ -37,19 +38,20 @@ describe Gitlab::Ci::Config::Node::Factory do
end
context 'when setting a parent' do
- let(:parent) { Object.new }
+ let(:object) { Object.new }
it 'creates entry with valid parent' do
entry = factory
- .with(value: 'ls', parent: parent)
+ .value('ls')
+ .with(parent: object)
.create!
- expect(entry.parent).to eq parent
+ expect(entry.parent).to eq object
end
end
end
- context 'when not setting up a value' do
+ context 'when not setting a value' do
it 'raises error' do
expect { factory.create! }.to raise_error(
Gitlab::Ci::Config::Node::Factory::InvalidFactory
@@ -60,11 +62,25 @@ describe Gitlab::Ci::Config::Node::Factory do
context 'when creating entry with nil value' do
it 'creates an undefined entry' do
entry = factory
- .with(value: nil)
+ .value(nil)
.create!
expect(entry).to be_an_instance_of Gitlab::Ci::Config::Node::Undefined
end
end
+
+ context 'when passing metadata' do
+ let(:node) { spy('node') }
+
+ it 'passes metadata as a parameter' do
+ factory
+ .value('some value')
+ .metadata(some: 'hash')
+ .create!
+
+ expect(node).to have_received(:new)
+ .with('some value', { some: 'hash' })
+ 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
index c87c9e97bc8..2f87d270b36 100644
--- a/spec/lib/gitlab/ci/config/node/global_spec.rb
+++ b/spec/lib/gitlab/ci/config/node/global_spec.rb
@@ -22,38 +22,40 @@ describe Gitlab::Ci::Config::Node::Global do
variables: { VAR: 'value' },
after_script: ['make clean'],
stages: ['build', 'pages'],
- cache: { key: 'k', untracked: true, paths: ['public/'] } }
+ cache: { key: 'k', untracked: true, paths: ['public/'] },
+ rspec: { script: %w[rspec ls] },
+ spinach: { script: 'spinach' } }
end
describe '#process!' do
before { global.process! }
it 'creates nodes hash' do
- expect(global.nodes).to be_an Array
+ expect(global.descendants).to be_an Array
end
it 'creates node object for each entry' do
- expect(global.nodes.count).to eq 8
+ expect(global.descendants.count).to eq 8
end
it 'creates node object using valid class' do
- expect(global.nodes.first)
+ expect(global.descendants.first)
.to be_an_instance_of Gitlab::Ci::Config::Node::Script
- expect(global.nodes.second)
+ expect(global.descendants.second)
.to be_an_instance_of Gitlab::Ci::Config::Node::Image
end
it 'sets correct description for nodes' do
- expect(global.nodes.first.description)
+ expect(global.descendants.first.description)
.to eq 'Script that will be executed before each job.'
- expect(global.nodes.second.description)
+ expect(global.descendants.second.description)
.to eq 'Docker image that will be used to execute jobs.'
end
- end
- describe '#leaf?' do
- it 'is not leaf' do
- expect(global).not_to be_leaf
+ describe '#leaf?' do
+ it 'is not leaf' do
+ expect(global).not_to be_leaf
+ end
end
end
@@ -63,6 +65,12 @@ describe Gitlab::Ci::Config::Node::Global do
expect(global.before_script).to be nil
end
end
+
+ describe '#leaf?' do
+ it 'is leaf' do
+ expect(global).to be_leaf
+ end
+ end
end
context 'when processed' do
@@ -106,7 +114,10 @@ describe Gitlab::Ci::Config::Node::Global do
end
context 'when deprecated types key defined' do
- let(:hash) { { types: ['test', 'deploy'] } }
+ let(:hash) do
+ { types: ['test', 'deploy'],
+ rspec: { script: 'rspec' } }
+ end
it 'returns array of types as stages' do
expect(global.stages).to eq %w[test deploy]
@@ -120,20 +131,33 @@ describe Gitlab::Ci::Config::Node::Global do
.to eq(key: 'k', untracked: true, paths: ['public/'])
end
end
+
+ describe '#jobs' do
+ it 'returns jobs configuration' do
+ expect(global.jobs).to eq(
+ rspec: { name: :rspec,
+ script: %w[rspec ls],
+ stage: 'test' },
+ spinach: { name: :spinach,
+ script: %w[spinach],
+ stage: 'test' }
+ )
+ end
+ end
end
end
context 'when most of entires not defined' do
- let(:hash) { { cache: { key: 'a' }, rspec: {} } }
+ let(:hash) { { cache: { key: 'a' }, rspec: { script: %w[ls] } } }
before { global.process! }
describe '#nodes' do
it 'instantizes all nodes' do
- expect(global.nodes.count).to eq 8
+ expect(global.descendants.count).to eq 8
end
it 'contains undefined nodes' do
- expect(global.nodes.first)
+ expect(global.descendants.first)
.to be_an_instance_of Gitlab::Ci::Config::Node::Undefined
end
end
@@ -164,7 +188,7 @@ describe Gitlab::Ci::Config::Node::Global do
# details.
#
context 'when entires specified but not defined' do
- let(:hash) { { variables: nil } }
+ let(:hash) { { variables: nil, rspec: { script: 'rspec' } } }
before { global.process! }
describe '#variables' do
@@ -196,10 +220,8 @@ describe Gitlab::Ci::Config::Node::Global do
end
describe '#before_script' do
- it 'raises error' do
- expect { global.before_script }.to raise_error(
- Gitlab::Ci::Config::Node::Entry::InvalidError
- )
+ it 'returns nil' do
+ expect(global.before_script).to be_nil
end
end
end
@@ -220,9 +242,9 @@ describe Gitlab::Ci::Config::Node::Global do
end
end
- describe '#defined?' do
+ describe '#specified?' do
it 'is concrete entry that is defined' do
- expect(global.defined?).to be true
+ expect(global.specified?).to be true
end
end
end
diff --git a/spec/lib/gitlab/ci/config/node/hidden_job_spec.rb b/spec/lib/gitlab/ci/config/node/hidden_job_spec.rb
new file mode 100644
index 00000000000..cc44e2cc054
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/node/hidden_job_spec.rb
@@ -0,0 +1,58 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Config::Node::HiddenJob do
+ let(:entry) { described_class.new(config) }
+
+ describe 'validations' do
+ context 'when entry config value is correct' do
+ let(:config) { { image: 'ruby:2.2' } }
+
+ describe '#value' do
+ it 'returns key value' do
+ expect(entry.value).to eq(image: 'ruby:2.2')
+ 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
+ context 'incorrect config value type' do
+ let(:config) { ['incorrect'] }
+
+ describe '#errors' do
+ it 'saves errors' do
+ expect(entry.errors)
+ .to include 'hidden job config should be a hash'
+ end
+ end
+ end
+
+ context 'when config is empty' do
+ let(:config) { {} }
+
+ describe '#valid' do
+ it 'is invalid' do
+ expect(entry).not_to be_valid
+ end
+ end
+ end
+ end
+ end
+
+ describe '#leaf?' do
+ it 'is a leaf' do
+ expect(entry).to be_leaf
+ end
+ end
+
+ describe '#relevant?' do
+ it 'is not a relevant entry' do
+ expect(entry).not_to be_relevant
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/config/node/job_spec.rb b/spec/lib/gitlab/ci/config/node/job_spec.rb
new file mode 100644
index 00000000000..1484fb60dd8
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/node/job_spec.rb
@@ -0,0 +1,86 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Config::Node::Job do
+ let(:entry) { described_class.new(config, name: :rspec) }
+
+ before { entry.process! }
+
+ describe 'validations' do
+ context 'when entry config value is correct' do
+ let(:config) { { script: 'rspec' } }
+
+ describe '#valid?' do
+ it 'is valid' do
+ expect(entry).to be_valid
+ end
+ end
+
+ context 'when job name is empty' do
+ let(:entry) { described_class.new(config, name: ''.to_sym) }
+
+ it 'reports error' do
+ expect(entry.errors)
+ .to include "job name can't be blank"
+ end
+ end
+ end
+
+ context 'when entry value is not correct' do
+ context 'incorrect config value type' do
+ let(:config) { ['incorrect'] }
+
+ describe '#errors' do
+ it 'reports error about a config type' do
+ expect(entry.errors)
+ .to include 'job config should be a hash'
+ end
+ end
+ end
+
+ context 'when config is empty' do
+ let(:config) { {} }
+
+ describe '#valid' do
+ it 'is invalid' do
+ expect(entry).not_to be_valid
+ end
+ end
+ end
+
+ context 'when unknown keys detected' do
+ let(:config) { { unknown: true } }
+
+ describe '#valid' do
+ it 'is not valid' do
+ expect(entry).not_to be_valid
+ end
+ end
+ end
+ end
+ end
+
+ describe '#value' do
+ context 'when entry is correct' do
+ let(:config) do
+ { before_script: %w[ls pwd],
+ script: 'rspec',
+ after_script: %w[cleanup] }
+ end
+
+ it 'returns correct value' do
+ expect(entry.value)
+ .to eq(name: :rspec,
+ before_script: %w[ls pwd],
+ script: %w[rspec],
+ stage: 'test',
+ after_script: %w[cleanup])
+ end
+ end
+ end
+
+ describe '#relevant?' do
+ it 'is a relevant entry' do
+ expect(entry).to be_relevant
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/config/node/jobs_spec.rb b/spec/lib/gitlab/ci/config/node/jobs_spec.rb
new file mode 100644
index 00000000000..b8d9c70479c
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/node/jobs_spec.rb
@@ -0,0 +1,87 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Config::Node::Jobs do
+ let(:entry) { described_class.new(config) }
+
+ describe 'validations' do
+ before { entry.process! }
+
+ context 'when entry config value is correct' do
+ let(:config) { { rspec: { script: 'rspec' } } }
+
+ describe '#valid?' do
+ it 'is valid' do
+ expect(entry).to be_valid
+ end
+ end
+ end
+
+ context 'when entry value is not correct' do
+ describe '#errors' do
+ context 'incorrect config value type' do
+ let(:config) { ['incorrect'] }
+
+ it 'returns error about incorrect type' do
+ expect(entry.errors)
+ .to include 'jobs config should be a hash'
+ end
+ end
+
+ context 'when job is unspecified' do
+ let(:config) { { rspec: nil } }
+
+ it 'reports error' do
+ expect(entry.errors).to include "rspec config can't be blank"
+ end
+ end
+
+ context 'when no visible jobs present' do
+ let(:config) { { '.hidden'.to_sym => { script: [] } } }
+
+ it 'returns error about no visible jobs defined' do
+ expect(entry.errors)
+ .to include 'jobs config should contain at least one visible job'
+ end
+ end
+ end
+ end
+ end
+
+ context 'when valid job entries processed' do
+ before { entry.process! }
+
+ let(:config) do
+ { rspec: { script: 'rspec' },
+ spinach: { script: 'spinach' },
+ '.hidden'.to_sym => {} }
+ end
+
+ describe '#value' do
+ it 'returns key value' do
+ expect(entry.value).to eq(
+ rspec: { name: :rspec,
+ script: %w[rspec],
+ stage: 'test' },
+ spinach: { name: :spinach,
+ script: %w[spinach],
+ stage: 'test' })
+ end
+ end
+
+ describe '#descendants' do
+ it 'creates valid descendant nodes' do
+ expect(entry.descendants.count).to eq 3
+ expect(entry.descendants.first(2))
+ .to all(be_an_instance_of(Gitlab::Ci::Config::Node::Job))
+ expect(entry.descendants.last)
+ .to be_an_instance_of(Gitlab::Ci::Config::Node::HiddenJob)
+ end
+ end
+
+ describe '#value' do
+ it 'returns value of visible jobs only' do
+ expect(entry.value.keys).to eq [:rspec, :spinach]
+ 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..1ab5478dcfa
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/node/null_spec.rb
@@ -0,0 +1,41 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Config::Node::Null do
+ let(:null) { described_class.new(nil) }
+
+ describe '#leaf?' do
+ it 'is leaf node' do
+ expect(null).to be_leaf
+ end
+ end
+
+ describe '#valid?' do
+ it 'is always valid' do
+ expect(null).to be_valid
+ end
+ end
+
+ describe '#errors' do
+ it 'is does not contain errors' do
+ expect(null.errors).to be_empty
+ end
+ end
+
+ describe '#value' do
+ it 'returns nil' do
+ expect(null.value).to eq nil
+ end
+ end
+
+ describe '#relevant?' do
+ it 'is not relevant' do
+ expect(null.relevant?).to eq false
+ end
+ end
+
+ describe '#specified?' do
+ it 'is not defined' do
+ expect(null.specified?).to eq false
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/config/node/stage_spec.rb b/spec/lib/gitlab/ci/config/node/stage_spec.rb
new file mode 100644
index 00000000000..fb9ec70762a
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/node/stage_spec.rb
@@ -0,0 +1,38 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Config::Node::Stage do
+ let(:stage) { described_class.new(config) }
+
+ describe 'validations' do
+ context 'when stage config value is correct' do
+ let(:config) { 'build' }
+
+ describe '#value' do
+ it 'returns a stage key' do
+ expect(stage.value).to eq config
+ end
+ end
+
+ describe '#valid?' do
+ it 'is valid' do
+ expect(stage).to be_valid
+ end
+ end
+ end
+
+ context 'when value has a wrong type' do
+ let(:config) { { test: true } }
+
+ it 'reports errors about wrong type' do
+ expect(stage.errors)
+ .to include 'stage config should be a string'
+ end
+ end
+ end
+
+ describe '.default' do
+ it 'returns default stage' do
+ expect(described_class.default).to eq 'test'
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/config/node/trigger_spec.rb b/spec/lib/gitlab/ci/config/node/trigger_spec.rb
new file mode 100644
index 00000000000..a4a3e36754e
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/node/trigger_spec.rb
@@ -0,0 +1,56 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Config::Node::Trigger do
+ let(:entry) { described_class.new(config) }
+
+ describe 'validations' do
+ context 'when entry config value is valid' do
+ context 'when config is a branch or tag name' do
+ let(:config) { %w[master feature/branch] }
+
+ describe '#valid?' do
+ it 'is valid' do
+ expect(entry).to be_valid
+ end
+ end
+
+ describe '#value' do
+ it 'returns key value' do
+ expect(entry.value).to eq config
+ end
+ end
+ end
+
+ context 'when config is a regexp' do
+ let(:config) { ['/^issue-.*$/'] }
+
+ describe '#valid?' do
+ it 'is valid' do
+ expect(entry).to be_valid
+ end
+ end
+ end
+
+ context 'when config is a special keyword' do
+ let(:config) { %w[tags triggers branches] }
+
+ describe '#valid?' do
+ it 'is valid' do
+ expect(entry).to be_valid
+ end
+ end
+ end
+ end
+
+ context 'when entry value is not valid' do
+ let(:config) { [1] }
+
+ describe '#errors' do
+ it 'saves errors' do
+ expect(entry.errors)
+ .to include 'trigger config should be an array of strings or regexps'
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/config/node/undefined_spec.rb b/spec/lib/gitlab/ci/config/node/undefined_spec.rb
index 0c6608d906d..2d43e1c1a9d 100644
--- a/spec/lib/gitlab/ci/config/node/undefined_spec.rb
+++ b/spec/lib/gitlab/ci/config/node/undefined_spec.rb
@@ -2,39 +2,31 @@ require 'spec_helper'
describe Gitlab::Ci::Config::Node::Undefined do
let(:undefined) { described_class.new(entry) }
- let(:entry) { Class.new }
-
- describe '#leaf?' do
- it 'is leaf node' do
- expect(undefined).to be_leaf
- end
- end
+ let(:entry) { spy('Entry') }
describe '#valid?' do
- it 'is always valid' do
- expect(undefined).to be_valid
+ it 'delegates method to entry' do
+ expect(undefined.valid).to eq entry
end
end
describe '#errors' do
- it 'is does not contain errors' do
- expect(undefined.errors).to be_empty
+ it 'delegates method to entry' do
+ expect(undefined.errors).to eq entry
end
end
describe '#value' do
- before do
- allow(entry).to receive(:default).and_return('some value')
- end
-
- it 'returns default value for entry' do
- expect(undefined.value).to eq 'some value'
+ it 'delegates method to entry' do
+ expect(undefined.value).to eq entry
end
end
- describe '#undefined?' do
- it 'is not a defined entry' do
- expect(undefined.defined?).to be false
+ describe '#specified?' do
+ it 'is always false' do
+ allow(entry).to receive(:specified?).and_return(true)
+
+ expect(undefined.specified?).to be false
end
end
end
diff --git a/spec/lib/gitlab/ci/config/node/validatable_spec.rb b/spec/lib/gitlab/ci/config/node/validatable_spec.rb
index 10cd01afcd1..64b77fd6e03 100644
--- a/spec/lib/gitlab/ci/config/node/validatable_spec.rb
+++ b/spec/lib/gitlab/ci/config/node/validatable_spec.rb
@@ -23,6 +23,10 @@ describe Gitlab::Ci::Config::Node::Validatable do
.to be Gitlab::Ci::Config::Node::Validator
end
+ it 'returns only one validator to mitigate leaks' do
+ expect { node.validator }.not_to change { node.validator }
+ end
+
context 'when validating node instance' do
let(:node_instance) { node.new }
diff --git a/spec/lib/gitlab/closing_issue_extractor_spec.rb b/spec/lib/gitlab/closing_issue_extractor_spec.rb
index e9b8ce6b5bb..de3f64249a2 100644
--- a/spec/lib/gitlab/closing_issue_extractor_spec.rb
+++ b/spec/lib/gitlab/closing_issue_extractor_spec.rb
@@ -3,10 +3,12 @@ require 'spec_helper'
describe Gitlab::ClosingIssueExtractor, lib: true do
let(:project) { create(:project) }
let(:project2) { create(:project) }
+ let(:forked_project) { Projects::ForkService.new(project, project.creator).execute }
let(:issue) { create(:issue, project: project) }
let(:issue2) { create(:issue, project: project2) }
let(:reference) { issue.to_reference }
let(:cross_reference) { issue2.to_reference(project) }
+ let(:fork_cross_reference) { issue.to_reference(forked_project) }
subject { described_class.new(project, project.creator) }
@@ -278,6 +280,15 @@ describe Gitlab::ClosingIssueExtractor, lib: true do
end
end
+ context "with a cross-project fork reference" do
+ subject { described_class.new(forked_project, forked_project.creator) }
+
+ it do
+ message = "Closes #{fork_cross_reference}"
+ expect(subject.closed_by_message(message)).to be_empty
+ end
+ end
+
context "with an invalid URL" do
it do
message = "Closes https://google.com#{urls.namespace_project_issue_path(issue2.project.namespace, issue2.project, issue2)}"
diff --git a/spec/lib/gitlab/conflict/file_collection_spec.rb b/spec/lib/gitlab/conflict/file_collection_spec.rb
new file mode 100644
index 00000000000..39d892c18c0
--- /dev/null
+++ b/spec/lib/gitlab/conflict/file_collection_spec.rb
@@ -0,0 +1,24 @@
+require 'spec_helper'
+
+describe Gitlab::Conflict::FileCollection, lib: true do
+ let(:merge_request) { create(:merge_request, source_branch: 'conflict-resolvable', target_branch: 'conflict-start') }
+ let(:file_collection) { Gitlab::Conflict::FileCollection.new(merge_request) }
+
+ describe '#files' do
+ it 'returns an array of Conflict::Files' do
+ expect(file_collection.files).to all(be_an_instance_of(Gitlab::Conflict::File))
+ end
+ end
+
+ describe '#default_commit_message' do
+ it 'matches the format of the git CLI commit message' do
+ expect(file_collection.default_commit_message).to eq(<<EOM.chomp)
+Merge branch 'conflict-start' into 'conflict-resolvable'
+
+# Conflicts:
+# files/ruby/popen.rb
+# files/ruby/regex.rb
+EOM
+ end
+ end
+end
diff --git a/spec/lib/gitlab/conflict/file_spec.rb b/spec/lib/gitlab/conflict/file_spec.rb
new file mode 100644
index 00000000000..60020487061
--- /dev/null
+++ b/spec/lib/gitlab/conflict/file_spec.rb
@@ -0,0 +1,261 @@
+require 'spec_helper'
+
+describe Gitlab::Conflict::File, lib: true do
+ let(:project) { create(:project) }
+ let(:repository) { project.repository }
+ let(:rugged) { repository.rugged }
+ let(:their_commit) { rugged.branches['conflict-start'].target }
+ let(:our_commit) { rugged.branches['conflict-resolvable'].target }
+ let(:merge_request) { create(:merge_request, source_branch: 'conflict-resolvable', target_branch: 'conflict-start', source_project: project) }
+ let(:index) { rugged.merge_commits(our_commit, their_commit) }
+ let(:conflict) { index.conflicts.last }
+ let(:merge_file_result) { index.merge_file('files/ruby/regex.rb') }
+ let(:conflict_file) { Gitlab::Conflict::File.new(merge_file_result, conflict, merge_request: merge_request) }
+
+ describe '#resolve_lines' do
+ let(:section_keys) { conflict_file.sections.map { |section| section[:id] }.compact }
+
+ context 'when resolving everything to the same side' do
+ let(:resolution_hash) { section_keys.map { |key| [key, 'head'] }.to_h }
+ let(:resolved_lines) { conflict_file.resolve_lines(resolution_hash) }
+ let(:expected_lines) { conflict_file.lines.reject { |line| line.type == 'old' } }
+
+ it 'has the correct number of lines' do
+ expect(resolved_lines.length).to eq(expected_lines.length)
+ end
+
+ it 'has content matching the chosen lines' do
+ expect(resolved_lines.map(&:text)).to eq(expected_lines.map(&:text))
+ end
+ end
+
+ context 'with mixed resolutions' do
+ let(:resolution_hash) do
+ section_keys.map.with_index { |key, i| [key, i.even? ? 'head' : 'origin'] }.to_h
+ end
+
+ let(:resolved_lines) { conflict_file.resolve_lines(resolution_hash) }
+
+ it 'has the correct number of lines' do
+ file_lines = conflict_file.lines.reject { |line| line.type == 'new' }
+
+ expect(resolved_lines.length).to eq(file_lines.length)
+ end
+
+ it 'returns a file containing only the chosen parts of the resolved sections' do
+ expect(resolved_lines.chunk { |line| line.type || 'both' }.map(&:first)).
+ to eq(['both', 'new', 'both', 'old', 'both', 'new', 'both'])
+ end
+ end
+
+ it 'raises MissingResolution when passed a hash without resolutions for all sections' do
+ empty_hash = section_keys.map { |key| [key, nil] }.to_h
+ invalid_hash = section_keys.map { |key| [key, 'invalid'] }.to_h
+
+ expect { conflict_file.resolve_lines({}) }.
+ to raise_error(Gitlab::Conflict::File::MissingResolution)
+
+ expect { conflict_file.resolve_lines(empty_hash) }.
+ to raise_error(Gitlab::Conflict::File::MissingResolution)
+
+ expect { conflict_file.resolve_lines(invalid_hash) }.
+ to raise_error(Gitlab::Conflict::File::MissingResolution)
+ end
+ end
+
+ describe '#highlight_lines!' do
+ def html_to_text(html)
+ CGI.unescapeHTML(ActionView::Base.full_sanitizer.sanitize(html)).delete("\n")
+ end
+
+ it 'modifies the existing lines' do
+ expect { conflict_file.highlight_lines! }.to change { conflict_file.lines.map(&:instance_variables) }
+ end
+
+ it 'is called implicitly when rich_text is accessed on a line' do
+ expect(conflict_file).to receive(:highlight_lines!).once.and_call_original
+
+ conflict_file.lines.each(&:rich_text)
+ end
+
+ it 'sets the rich_text of the lines matching the text content' do
+ conflict_file.lines.each do |line|
+ expect(line.text).to eq(html_to_text(line.rich_text))
+ end
+ end
+ end
+
+ describe '#sections' do
+ it 'only inserts match lines when there is a gap between sections' do
+ conflict_file.sections.each_with_index do |section, i|
+ previous_line_number = 0
+ current_line_number = section[:lines].map(&:old_line).compact.min
+
+ if i > 0
+ previous_line_number = conflict_file.sections[i - 1][:lines].map(&:old_line).compact.last
+ end
+
+ if current_line_number == previous_line_number + 1
+ expect(section[:lines].first.type).not_to eq('match')
+ else
+ expect(section[:lines].first.type).to eq('match')
+ expect(section[:lines].first.text).to match(/\A@@ -#{current_line_number},\d+ \+\d+,\d+ @@ module Gitlab\Z/)
+ end
+ end
+ end
+
+ it 'sets conflict to false for sections with only unchanged lines' do
+ conflict_file.sections.reject { |section| section[:conflict] }.each do |section|
+ without_match = section[:lines].reject { |line| line.type == 'match' }
+
+ expect(without_match).to all(have_attributes(type: nil))
+ end
+ end
+
+ it 'only includes a maximum of CONTEXT_LINES (plus an optional match line) in context sections' do
+ conflict_file.sections.reject { |section| section[:conflict] }.each do |section|
+ without_match = section[:lines].reject { |line| line.type == 'match' }
+
+ expect(without_match.length).to be <= Gitlab::Conflict::File::CONTEXT_LINES * 2
+ end
+ end
+
+ it 'sets conflict to true for sections with only changed lines' do
+ conflict_file.sections.select { |section| section[:conflict] }.each do |section|
+ section[:lines].each do |line|
+ expect(line.type).to be_in(['new', 'old'])
+ end
+ end
+ end
+
+ it 'adds unique IDs to conflict sections, and not to other sections' do
+ section_ids = []
+
+ conflict_file.sections.each do |section|
+ if section[:conflict]
+ expect(section).to have_key(:id)
+ section_ids << section[:id]
+ else
+ expect(section).not_to have_key(:id)
+ end
+ end
+
+ expect(section_ids.uniq).to eq(section_ids)
+ end
+
+ context 'with an example file' do
+ let(:file) do
+ <<FILE
+ # Ensure there is no match line header here
+ def username_regexp
+ default_regexp
+ end
+
+<<<<<<< files/ruby/regex.rb
+def project_name_regexp
+ /\A[a-zA-Z0-9][a-zA-Z0-9_\-\. ]*\z/
+end
+
+def name_regexp
+ /\A[a-zA-Z0-9_\-\. ]*\z/
+=======
+def project_name_regex
+ %r{\A[a-zA-Z0-9][a-zA-Z0-9_\-\. ]*\z}
+end
+
+def name_regex
+ %r{\A[a-zA-Z0-9_\-\. ]*\z}
+>>>>>>> files/ruby/regex.rb
+end
+
+# Some extra lines
+# To force a match line
+# To be created
+
+def path_regexp
+ default_regexp
+end
+
+<<<<<<< files/ruby/regex.rb
+def archive_formats_regexp
+ /(zip|tar|7z|tar\.gz|tgz|gz|tar\.bz2|tbz|tbz2|tb2|bz2)/
+=======
+def archive_formats_regex
+ %r{(zip|tar|7z|tar\.gz|tgz|gz|tar\.bz2|tbz|tbz2|tb2|bz2)}
+>>>>>>> files/ruby/regex.rb
+end
+
+def git_reference_regexp
+ # Valid git ref regexp, see:
+ # https://www.kernel.org/pub/software/scm/git/docs/git-check-ref-format.html
+ %r{
+ (?!
+ (?# doesn't begins with)
+ \/| (?# rule #6)
+ (?# doesn't contain)
+ .*(?:
+ [\/.]\.| (?# rule #1,3)
+ \/\/| (?# rule #6)
+ @\{| (?# rule #8)
+ \\ (?# rule #9)
+ )
+ )
+ [^\000-\040\177~^:?*\[]+ (?# rule #4-5)
+ (?# doesn't end with)
+ (?<!\.lock) (?# rule #1)
+ (?<![\/.]) (?# rule #6-7)
+ }x
+end
+
+protected
+
+<<<<<<< files/ruby/regex.rb
+def default_regexp
+ /\A[.?]?[a-zA-Z0-9][a-zA-Z0-9_\-\.]*(?<!\.git)\z/
+=======
+def default_regex
+ %r{\A[.?]?[a-zA-Z0-9][a-zA-Z0-9_\-\.]*(?<!\.git)\z}
+>>>>>>> files/ruby/regex.rb
+end
+FILE
+ end
+
+ let(:conflict_file) { Gitlab::Conflict::File.new({ data: file }, conflict, merge_request: merge_request) }
+ let(:sections) { conflict_file.sections }
+
+ it 'sets the correct match line headers' do
+ expect(sections[0][:lines].first).to have_attributes(type: 'match', text: '@@ -3,14 +3,14 @@')
+ expect(sections[3][:lines].first).to have_attributes(type: 'match', text: '@@ -19,26 +19,26 @@ def path_regexp')
+ expect(sections[6][:lines].first).to have_attributes(type: 'match', text: '@@ -47,52 +47,52 @@ end')
+ end
+
+ it 'does not add match lines where they are not needed' do
+ expect(sections[1][:lines].first.type).not_to eq('match')
+ expect(sections[2][:lines].first.type).not_to eq('match')
+ expect(sections[4][:lines].first.type).not_to eq('match')
+ expect(sections[5][:lines].first.type).not_to eq('match')
+ expect(sections[7][:lines].first.type).not_to eq('match')
+ end
+
+ it 'creates context sections of the correct length' do
+ expect(sections[0][:lines].reject(&:type).length).to eq(3)
+ expect(sections[2][:lines].reject(&:type).length).to eq(3)
+ expect(sections[3][:lines].reject(&:type).length).to eq(3)
+ expect(sections[5][:lines].reject(&:type).length).to eq(3)
+ expect(sections[6][:lines].reject(&:type).length).to eq(3)
+ expect(sections[8][:lines].reject(&:type).length).to eq(1)
+ end
+ end
+ end
+
+ describe '#as_json' do
+ it 'includes the blob path for the file' do
+ expect(conflict_file.as_json[:blob_path]).
+ to eq("/#{project.namespace.to_param}/#{merge_request.project.to_param}/blob/#{our_commit.oid}/files/ruby/regex.rb")
+ end
+
+ it 'includes the blob icon for the file' do
+ expect(conflict_file.as_json[:blob_icon]).to eq('file-text-o')
+ end
+ end
+end
diff --git a/spec/lib/gitlab/conflict/parser_spec.rb b/spec/lib/gitlab/conflict/parser_spec.rb
new file mode 100644
index 00000000000..a1d2ca1e272
--- /dev/null
+++ b/spec/lib/gitlab/conflict/parser_spec.rb
@@ -0,0 +1,193 @@
+require 'spec_helper'
+
+describe Gitlab::Conflict::Parser, lib: true do
+ let(:parser) { Gitlab::Conflict::Parser.new }
+
+ describe '#parse' do
+ def parse_text(text)
+ parser.parse(text, our_path: 'README.md', their_path: 'README.md')
+ end
+
+ context 'when the file has valid conflicts' do
+ let(:text) do
+ <<CONFLICT
+module Gitlab
+ module Regexp
+ extend self
+
+ def username_regexp
+ default_regexp
+ end
+
+<<<<<<< files/ruby/regex.rb
+ def project_name_regexp
+ /\A[a-zA-Z0-9][a-zA-Z0-9_\-\. ]*\z/
+ end
+
+ def name_regexp
+ /\A[a-zA-Z0-9_\-\. ]*\z/
+=======
+ def project_name_regex
+ %r{\A[a-zA-Z0-9][a-zA-Z0-9_\-\. ]*\z}
+ end
+
+ def name_regex
+ %r{\A[a-zA-Z0-9_\-\. ]*\z}
+>>>>>>> files/ruby/regex.rb
+ end
+
+ def path_regexp
+ default_regexp
+ end
+
+<<<<<<< files/ruby/regex.rb
+ def archive_formats_regexp
+ /(zip|tar|7z|tar\.gz|tgz|gz|tar\.bz2|tbz|tbz2|tb2|bz2)/
+=======
+ def archive_formats_regex
+ %r{(zip|tar|7z|tar\.gz|tgz|gz|tar\.bz2|tbz|tbz2|tb2|bz2)}
+>>>>>>> files/ruby/regex.rb
+ end
+
+ def git_reference_regexp
+ # Valid git ref regexp, see:
+ # https://www.kernel.org/pub/software/scm/git/docs/git-check-ref-format.html
+ %r{
+ (?!
+ (?# doesn't begins with)
+ \/| (?# rule #6)
+ (?# doesn't contain)
+ .*(?:
+ [\/.]\.| (?# rule #1,3)
+ \/\/| (?# rule #6)
+ @\{| (?# rule #8)
+ \\ (?# rule #9)
+ )
+ )
+ [^\000-\040\177~^:?*\[]+ (?# rule #4-5)
+ (?# doesn't end with)
+ (?<!\.lock) (?# rule #1)
+ (?<![\/.]) (?# rule #6-7)
+ }x
+ end
+
+ protected
+
+<<<<<<< files/ruby/regex.rb
+ def default_regexp
+ /\A[.?]?[a-zA-Z0-9][a-zA-Z0-9_\-\.]*(?<!\.git)\z/
+=======
+ def default_regex
+ %r{\A[.?]?[a-zA-Z0-9][a-zA-Z0-9_\-\.]*(?<!\.git)\z}
+>>>>>>> files/ruby/regex.rb
+ end
+ end
+end
+CONFLICT
+ end
+
+ let(:lines) do
+ parser.parse(text, our_path: 'files/ruby/regex.rb', their_path: 'files/ruby/regex.rb')
+ end
+
+ it 'sets our lines as new lines' do
+ expect(lines[8..13]).to all(have_attributes(type: 'new'))
+ expect(lines[26..27]).to all(have_attributes(type: 'new'))
+ expect(lines[56..57]).to all(have_attributes(type: 'new'))
+ end
+
+ it 'sets their lines as old lines' do
+ expect(lines[14..19]).to all(have_attributes(type: 'old'))
+ expect(lines[28..29]).to all(have_attributes(type: 'old'))
+ expect(lines[58..59]).to all(have_attributes(type: 'old'))
+ end
+
+ it 'sets non-conflicted lines as both' do
+ expect(lines[0..7]).to all(have_attributes(type: nil))
+ expect(lines[20..25]).to all(have_attributes(type: nil))
+ expect(lines[30..55]).to all(have_attributes(type: nil))
+ expect(lines[60..62]).to all(have_attributes(type: nil))
+ end
+
+ it 'sets consecutive line numbers for index, old_pos, and new_pos' do
+ old_line_numbers = lines.select { |line| line.type != 'new' }.map(&:old_pos)
+ new_line_numbers = lines.select { |line| line.type != 'old' }.map(&:new_pos)
+
+ expect(lines.map(&:index)).to eq(0.upto(62).to_a)
+ expect(old_line_numbers).to eq(1.upto(53).to_a)
+ expect(new_line_numbers).to eq(1.upto(53).to_a)
+ end
+ end
+
+ context 'when the file contents include conflict delimiters' do
+ it 'raises UnexpectedDelimiter when there is a non-start delimiter first' do
+ expect { parse_text('=======') }.
+ to raise_error(Gitlab::Conflict::Parser::UnexpectedDelimiter)
+
+ expect { parse_text('>>>>>>> README.md') }.
+ to raise_error(Gitlab::Conflict::Parser::UnexpectedDelimiter)
+
+ expect { parse_text('>>>>>>> some-other-path.md') }.
+ not_to raise_error
+ end
+
+ it 'raises UnexpectedDelimiter when a start delimiter is followed by a non-middle delimiter' do
+ start_text = "<<<<<<< README.md\n"
+ end_text = "\n=======\n>>>>>>> README.md"
+
+ expect { parse_text(start_text + '>>>>>>> README.md' + end_text) }.
+ to raise_error(Gitlab::Conflict::Parser::UnexpectedDelimiter)
+
+ expect { parse_text(start_text + start_text + end_text) }.
+ to raise_error(Gitlab::Conflict::Parser::UnexpectedDelimiter)
+
+ expect { parse_text(start_text + '>>>>>>> some-other-path.md' + end_text) }.
+ not_to raise_error
+ end
+
+ it 'raises UnexpectedDelimiter when a middle delimiter is followed by a non-end delimiter' do
+ start_text = "<<<<<<< README.md\n=======\n"
+ end_text = "\n>>>>>>> README.md"
+
+ expect { parse_text(start_text + '=======' + end_text) }.
+ to raise_error(Gitlab::Conflict::Parser::UnexpectedDelimiter)
+
+ expect { parse_text(start_text + start_text + end_text) }.
+ to raise_error(Gitlab::Conflict::Parser::UnexpectedDelimiter)
+
+ expect { parse_text(start_text + '>>>>>>> some-other-path.md' + end_text) }.
+ not_to raise_error
+ end
+
+ it 'raises MissingEndDelimiter when there is no end delimiter at the end' do
+ start_text = "<<<<<<< README.md\n=======\n"
+
+ expect { parse_text(start_text) }.
+ to raise_error(Gitlab::Conflict::Parser::MissingEndDelimiter)
+
+ expect { parse_text(start_text + '>>>>>>> some-other-path.md') }.
+ to raise_error(Gitlab::Conflict::Parser::MissingEndDelimiter)
+ end
+ end
+
+ context 'other file types' do
+ it 'raises UnmergeableFile when lines is blank, indicating a binary file' do
+ expect { parse_text('') }.
+ to raise_error(Gitlab::Conflict::Parser::UnmergeableFile)
+
+ expect { parse_text(nil) }.
+ to raise_error(Gitlab::Conflict::Parser::UnmergeableFile)
+ end
+
+ it 'raises UnmergeableFile when the file is over 100 KB' do
+ expect { parse_text('a' * 102401) }.
+ to raise_error(Gitlab::Conflict::Parser::UnmergeableFile)
+ end
+
+ it 'raises UnsupportedEncoding when the file contains non-UTF-8 characters' do
+ expect { parse_text("a\xC4\xFC".force_encoding(Encoding::ASCII_8BIT)) }.
+ to raise_error(Gitlab::Conflict::Parser::UnsupportedEncoding)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/build_data_builder_spec.rb b/spec/lib/gitlab/data_builder/build_spec.rb
index 38be9448794..6c71e98066b 100644
--- a/spec/lib/gitlab/build_data_builder_spec.rb
+++ b/spec/lib/gitlab/data_builder/build_spec.rb
@@ -1,11 +1,11 @@
require 'spec_helper'
-describe 'Gitlab::BuildDataBuilder' do
+describe Gitlab::DataBuilder::Build do
let(:build) { create(:ci_build) }
- describe :build do
+ describe '.build' do
let(:data) do
- Gitlab::BuildDataBuilder.build(build)
+ described_class.build(build)
end
it { expect(data).to be_a(Hash) }
diff --git a/spec/lib/gitlab/note_data_builder_spec.rb b/spec/lib/gitlab/data_builder/note_spec.rb
index 3d6bcdfd873..9a4dec91e56 100644
--- a/spec/lib/gitlab/note_data_builder_spec.rb
+++ b/spec/lib/gitlab/data_builder/note_spec.rb
@@ -1,9 +1,9 @@
require 'spec_helper'
-describe 'Gitlab::NoteDataBuilder', lib: true do
+describe Gitlab::DataBuilder::Note, lib: true do
let(:project) { create(:project) }
let(:user) { create(:user) }
- let(:data) { Gitlab::NoteDataBuilder.build(note, user) }
+ let(:data) { described_class.build(note, user) }
let(:fixed_time) { Time.at(1425600000) } # Avoid time precision errors
before(:each) do
diff --git a/spec/lib/gitlab/data_builder/pipeline_spec.rb b/spec/lib/gitlab/data_builder/pipeline_spec.rb
new file mode 100644
index 00000000000..a68f5943a6a
--- /dev/null
+++ b/spec/lib/gitlab/data_builder/pipeline_spec.rb
@@ -0,0 +1,36 @@
+require 'spec_helper'
+
+describe Gitlab::DataBuilder::Pipeline do
+ let(:user) { create(:user) }
+ let(:project) { create(:project) }
+
+ let(:pipeline) do
+ create(:ci_pipeline,
+ project: project,
+ status: 'success',
+ sha: project.commit.sha,
+ ref: project.default_branch)
+ end
+
+ let!(:build) { create(:ci_build, pipeline: pipeline) }
+
+ describe '.build' do
+ let(:data) { described_class.build(pipeline) }
+ let(:attributes) { data[:object_attributes] }
+ let(:build_data) { data[:builds].first }
+ let(:project_data) { data[:project] }
+
+ it { expect(attributes).to be_a(Hash) }
+ it { expect(attributes[:ref]).to eq(pipeline.ref) }
+ it { expect(attributes[:sha]).to eq(pipeline.sha) }
+ it { expect(attributes[:tag]).to eq(pipeline.tag) }
+ it { expect(attributes[:id]).to eq(pipeline.id) }
+ it { expect(attributes[:status]).to eq(pipeline.status) }
+
+ it { expect(build_data).to be_a(Hash) }
+ it { expect(build_data[:id]).to eq(build.id) }
+ it { expect(build_data[:status]).to eq(build.status) }
+
+ it { expect(project_data).to eq(project.hook_attrs(backward: false)) }
+ end
+end
diff --git a/spec/lib/gitlab/push_data_builder_spec.rb b/spec/lib/gitlab/data_builder/push_spec.rb
index 6bd7393aaa7..b73434e8dd7 100644
--- a/spec/lib/gitlab/push_data_builder_spec.rb
+++ b/spec/lib/gitlab/data_builder/push_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Gitlab::PushDataBuilder, lib: true do
+describe Gitlab::DataBuilder::Push, lib: true do
let(:project) { create(:project) }
let(:user) { create(:user) }
diff --git a/spec/lib/gitlab/database/migration_helpers_spec.rb b/spec/lib/gitlab/database/migration_helpers_spec.rb
index 9096ad101b0..4ec3f19e03f 100644
--- a/spec/lib/gitlab/database/migration_helpers_spec.rb
+++ b/spec/lib/gitlab/database/migration_helpers_spec.rb
@@ -13,6 +13,10 @@ describe Gitlab::Database::MigrationHelpers, lib: true do
context 'outside a transaction' do
before do
expect(model).to receive(:transaction_open?).and_return(false)
+
+ unless Gitlab::Database.postgresql?
+ allow_any_instance_of(Gitlab::Database::MigrationHelpers).to receive(:disable_statement_timeout)
+ end
end
context 'using PostgreSQL' do
diff --git a/spec/lib/gitlab/diff/file_spec.rb b/spec/lib/gitlab/diff/file_spec.rb
index 1cb513d5229..0650cb291e5 100644
--- a/spec/lib/gitlab/diff/file_spec.rb
+++ b/spec/lib/gitlab/diff/file_spec.rb
@@ -5,17 +5,17 @@ describe Gitlab::Diff::File, lib: true do
let(:project) { create(:project) }
let(:commit) { project.commit(sample_commit.id) }
- let(:diff) { commit.diffs.first }
+ let(:diff) { commit.raw_diffs.first }
let(:diff_file) { Gitlab::Diff::File.new(diff, diff_refs: commit.diff_refs, repository: project.repository) }
- describe :diff_lines do
+ describe '#diff_lines' do
let(:diff_lines) { diff_file.diff_lines }
it { expect(diff_lines.size).to eq(30) }
it { expect(diff_lines.first).to be_kind_of(Gitlab::Diff::Line) }
end
- describe :mode_changed? do
+ describe '#mode_changed?' do
it { expect(diff_file.mode_changed?).to be_falsey }
end
@@ -32,4 +32,18 @@ describe Gitlab::Diff::File, lib: true do
expect(diff_file.too_large?).to eq(false)
end
end
+
+ describe '#collapsed?' do
+ it 'returns true for a file that is quite big' do
+ expect(diff).to receive(:collapsed?).and_return(true)
+
+ expect(diff_file.collapsed?).to eq(true)
+ end
+
+ it 'returns false for a file that is small enough' do
+ expect(diff).to receive(:collapsed?).and_return(false)
+
+ expect(diff_file.collapsed?).to eq(false)
+ end
+ end
end
diff --git a/spec/lib/gitlab/diff/highlight_spec.rb b/spec/lib/gitlab/diff/highlight_spec.rb
index fb5d50a5c68..1c2ddeed692 100644
--- a/spec/lib/gitlab/diff/highlight_spec.rb
+++ b/spec/lib/gitlab/diff/highlight_spec.rb
@@ -5,7 +5,7 @@ describe Gitlab::Diff::Highlight, lib: true do
let(:project) { create(:project) }
let(:commit) { project.commit(sample_commit.id) }
- let(:diff) { commit.diffs.first }
+ let(:diff) { commit.raw_diffs.first }
let(:diff_file) { Gitlab::Diff::File.new(diff, diff_refs: commit.diff_refs, repository: project.repository) }
describe '#highlight' do
@@ -28,13 +28,13 @@ describe Gitlab::Diff::Highlight, lib: true do
end
it 'highlights and marks removed lines' do
- code = %Q{-<span id="LC9" class="line"> <span class="k">raise</span> <span class="s2">&quot;System commands must be given as an array of strings&quot;</span></span>\n}
+ code = %Q{-<span id="LC9" class="line"> <span class="k">raise</span> <span class="s2">"System commands must be given as an array of strings"</span></span>\n}
expect(subject[4].text).to eq(code)
end
it 'highlights and marks added lines' do
- code = %Q{+<span id="LC9" class="line"> <span class="k">raise</span> <span class="no"><span class='idiff left'>RuntimeError</span></span><span class="p"><span class='idiff'>,</span></span><span class='idiff right'> </span><span class="s2">&quot;System commands must be given as an array of strings&quot;</span></span>\n}
+ code = %Q{+<span id="LC9" class="line"> <span class="k">raise</span> <span class="no"><span class='idiff left'>RuntimeError</span></span><span class="p"><span class='idiff'>,</span></span><span class='idiff right'> </span><span class="s2">"System commands must be given as an array of strings"</span></span>\n}
expect(subject[5].text).to eq(code)
end
diff --git a/spec/lib/gitlab/diff/inline_diff_spec.rb b/spec/lib/gitlab/diff/inline_diff_spec.rb
index 95a993d26cf..8ca3f73509e 100644
--- a/spec/lib/gitlab/diff/inline_diff_spec.rb
+++ b/spec/lib/gitlab/diff/inline_diff_spec.rb
@@ -3,14 +3,19 @@ require 'spec_helper'
describe Gitlab::Diff::InlineDiff, lib: true do
describe '.for_lines' do
let(:diff) do
- <<eos
- class Test
-- def initialize(test = true)
-+ def initialize(test = false)
- @test = test
- end
- end
-eos
+ <<-EOF.strip_heredoc
+ class Test
+ - def initialize(test = true)
+ + def initialize(test = false)
+ @test = test
+ - if true
+ - @foo = "bar"
+ + unless false
+ + @foo = "baz"
+ end
+ end
+ end
+ EOF
end
let(:subject) { described_class.for_lines(diff.lines) }
@@ -20,8 +25,11 @@ eos
expect(subject[1]).to eq([25..27])
expect(subject[2]).to eq([25..28])
expect(subject[3]).to be_nil
- expect(subject[4]).to be_nil
- expect(subject[5]).to be_nil
+ expect(subject[4]).to eq([5..10])
+ expect(subject[5]).to eq([17..17])
+ expect(subject[6]).to eq([5..15])
+ expect(subject[7]).to eq([17..17])
+ expect(subject[8]).to be_nil
end
end
diff --git a/spec/lib/gitlab/diff/line_mapper_spec.rb b/spec/lib/gitlab/diff/line_mapper_spec.rb
index 4e50e03bb7e..4b943fa382d 100644
--- a/spec/lib/gitlab/diff/line_mapper_spec.rb
+++ b/spec/lib/gitlab/diff/line_mapper_spec.rb
@@ -6,7 +6,7 @@ describe Gitlab::Diff::LineMapper, lib: true do
let(:project) { create(:project) }
let(:repository) { project.repository }
let(:commit) { project.commit(sample_commit.id) }
- let(:diffs) { commit.diffs }
+ let(:diffs) { commit.raw_diffs }
let(:diff) { diffs.first }
let(:diff_file) { Gitlab::Diff::File.new(diff, diff_refs: commit.diff_refs, repository: repository) }
subject { described_class.new(diff_file) }
diff --git a/spec/lib/gitlab/diff/parallel_diff_spec.rb b/spec/lib/gitlab/diff/parallel_diff_spec.rb
index 5f76b70c6f5..af18d3c25a6 100644
--- a/spec/lib/gitlab/diff/parallel_diff_spec.rb
+++ b/spec/lib/gitlab/diff/parallel_diff_spec.rb
@@ -6,16 +6,56 @@ describe Gitlab::Diff::ParallelDiff, lib: true do
let(:project) { create(:project) }
let(:repository) { project.repository }
let(:commit) { project.commit(sample_commit.id) }
- let(:diffs) { commit.diffs }
+ let(:diffs) { commit.raw_diffs }
let(:diff) { diffs.first }
let(:diff_file) { Gitlab::Diff::File.new(diff, diff_refs: commit.diff_refs, repository: repository) }
subject { described_class.new(diff_file) }
- let(:parallel_diff_result_array) { YAML.load_file("#{Rails.root}/spec/fixtures/parallel_diff_result.yml") }
-
describe '#parallelize' do
it 'should return an array of arrays containing the parsed diff' do
- expect(subject.parallelize).to match_array(parallel_diff_result_array)
+ diff_lines = diff_file.highlighted_diff_lines
+ expected = [
+ # Unchanged lines
+ { left: diff_lines[0], right: diff_lines[0] },
+ { left: diff_lines[1], right: diff_lines[1] },
+ { left: diff_lines[2], right: diff_lines[2] },
+ { left: diff_lines[3], right: diff_lines[3] },
+ { left: diff_lines[4], right: diff_lines[5] },
+ { left: diff_lines[6], right: diff_lines[6] },
+ { left: diff_lines[7], right: diff_lines[7] },
+ { left: diff_lines[8], right: diff_lines[8] },
+
+ # Changed lines
+ { left: diff_lines[9], right: diff_lines[11] },
+ { left: diff_lines[10], right: diff_lines[12] },
+
+ # Added lines
+ { left: nil, right: diff_lines[13] },
+ { left: nil, right: diff_lines[14] },
+ { left: nil, right: diff_lines[15] },
+ { left: nil, right: diff_lines[16] },
+ { left: nil, right: diff_lines[17] },
+ { left: nil, right: diff_lines[18] },
+
+ # Unchanged lines
+ { left: diff_lines[19], right: diff_lines[19] },
+ { left: diff_lines[20], right: diff_lines[20] },
+ { left: diff_lines[21], right: diff_lines[21] },
+ { left: diff_lines[22], right: diff_lines[22] },
+ { left: diff_lines[23], right: diff_lines[23] },
+ { left: diff_lines[24], right: diff_lines[24] },
+ { left: diff_lines[25], right: diff_lines[25] },
+
+ # Added line
+ { left: nil, right: diff_lines[26] },
+
+ # Unchanged lines
+ { left: diff_lines[27], right: diff_lines[27] },
+ { left: diff_lines[28], right: diff_lines[28] },
+ { left: diff_lines[29], right: diff_lines[29] }
+ ]
+
+ expect(subject.parallelize).to eq(expected)
end
end
end
diff --git a/spec/lib/gitlab/diff/parser_spec.rb b/spec/lib/gitlab/diff/parser_spec.rb
index cdff063a9ed..b983d73f8be 100644
--- a/spec/lib/gitlab/diff/parser_spec.rb
+++ b/spec/lib/gitlab/diff/parser_spec.rb
@@ -5,10 +5,10 @@ describe Gitlab::Diff::Parser, lib: true do
let(:project) { create(:project) }
let(:commit) { project.commit(sample_commit.id) }
- let(:diff) { commit.diffs.first }
+ let(:diff) { commit.raw_diffs.first }
let(:parser) { Gitlab::Diff::Parser.new }
- describe :parse do
+ describe '#parse' do
let(:diff) do
<<eos
--- a/files/ruby/popen.rb
diff --git a/spec/lib/gitlab/diff/position_spec.rb b/spec/lib/gitlab/diff/position_spec.rb
index cf28628cb96..6e8fff6f516 100644
--- a/spec/lib/gitlab/diff/position_spec.rb
+++ b/spec/lib/gitlab/diff/position_spec.rb
@@ -338,4 +338,70 @@ describe Gitlab::Diff::Position, lib: true do
end
end
end
+
+ describe "position for a file in the initial commit" do
+ let(:commit) { project.commit("1a0b36b3cdad1d2ee32457c102a8c0b7056fa863") }
+
+ subject do
+ described_class.new(
+ old_path: "README.md",
+ new_path: "README.md",
+ old_line: nil,
+ new_line: 1,
+ diff_refs: commit.diff_refs
+ )
+ end
+
+ describe "#diff_file" do
+ it "returns the correct diff file" do
+ diff_file = subject.diff_file(project.repository)
+
+ expect(diff_file.new_file).to be true
+ expect(diff_file.new_path).to eq(subject.new_path)
+ expect(diff_file.diff_refs).to eq(subject.diff_refs)
+ end
+ end
+
+ describe "#diff_line" do
+ it "returns the correct diff line" do
+ diff_line = subject.diff_line(project.repository)
+
+ expect(diff_line.added?).to be true
+ expect(diff_line.new_line).to eq(subject.new_line)
+ expect(diff_line.text).to eq("+testme")
+ end
+ end
+
+ describe "#line_code" do
+ it "returns the correct line code" do
+ line_code = Gitlab::Diff::LineCode.generate(subject.file_path, subject.new_line, 0)
+
+ expect(subject.line_code(project.repository)).to eq(line_code)
+ end
+ end
+ end
+
+ describe "#to_json" do
+ let(:hash) do
+ {
+ old_path: "files/ruby/popen.rb",
+ new_path: "files/ruby/popen.rb",
+ old_line: nil,
+ new_line: 14,
+ base_sha: nil,
+ head_sha: nil,
+ start_sha: nil
+ }
+ end
+
+ let(:diff_position) { described_class.new(hash) }
+
+ it "returns the position as JSON" do
+ expect(JSON.parse(diff_position.to_json)).to eq(hash.stringify_keys)
+ end
+
+ it "works when nested under another hash" do
+ expect(JSON.parse(JSON.generate(pos: diff_position))).to eq('pos' => hash.stringify_keys)
+ end
+ end
end
diff --git a/spec/lib/gitlab/diff/position_tracer_spec.rb b/spec/lib/gitlab/diff/position_tracer_spec.rb
index 08312e60f4a..c268f84c759 100644
--- a/spec/lib/gitlab/diff/position_tracer_spec.rb
+++ b/spec/lib/gitlab/diff/position_tracer_spec.rb
@@ -1639,7 +1639,8 @@ describe Gitlab::Diff::PositionTracer, lib: true do
committer: committer
}
- repository.merge(current_user, second_create_file_commit.sha, branch_name, options)
+ merge_request = create(:merge_request, source_branch: second_create_file_commit.sha, target_branch: branch_name, source_project: project)
+ repository.merge(current_user, merge_request, options)
project.commit(branch_name)
end
diff --git a/spec/lib/gitlab/downtime_check/message_spec.rb b/spec/lib/gitlab/downtime_check/message_spec.rb
new file mode 100644
index 00000000000..a5a398abf78
--- /dev/null
+++ b/spec/lib/gitlab/downtime_check/message_spec.rb
@@ -0,0 +1,39 @@
+require 'spec_helper'
+
+describe Gitlab::DowntimeCheck::Message do
+ describe '#to_s' do
+ it 'returns an ANSI formatted String for an offline migration' do
+ message = described_class.new('foo.rb', true, 'hello')
+
+ expect(message.to_s).to eq("[\e[31moffline\e[0m]: foo.rb:\n\nhello\n\n")
+ end
+
+ it 'returns an ANSI formatted String for an online migration' do
+ message = described_class.new('foo.rb')
+
+ expect(message.to_s).to eq("[\e[32monline\e[0m]: foo.rb")
+ end
+ end
+
+ describe '#reason?' do
+ it 'returns false when no reason is specified' do
+ message = described_class.new('foo.rb')
+
+ expect(message.reason?).to eq(false)
+ end
+
+ it 'returns true when a reason is specified' do
+ message = described_class.new('foo.rb', true, 'hello')
+
+ expect(message.reason?).to eq(true)
+ end
+ end
+
+ describe '#reason' do
+ it 'strips excessive whitespace from the returned String' do
+ message = described_class.new('foo.rb', true, " hello\n world\n\n foo")
+
+ expect(message.reason).to eq("hello\nworld\n\nfoo")
+ end
+ end
+end
diff --git a/spec/lib/gitlab/downtime_check_spec.rb b/spec/lib/gitlab/downtime_check_spec.rb
new file mode 100644
index 00000000000..42d895e548e
--- /dev/null
+++ b/spec/lib/gitlab/downtime_check_spec.rb
@@ -0,0 +1,113 @@
+require 'spec_helper'
+
+describe Gitlab::DowntimeCheck do
+ subject { described_class.new }
+ let(:path) { 'foo.rb' }
+
+ describe '#check' do
+ before do
+ expect(subject).to receive(:require).with(path)
+ end
+
+ context 'when a migration does not specify if downtime is required' do
+ it 'raises RuntimeError' do
+ expect(subject).to receive(:class_for_migration_file).
+ with(path).
+ and_return(Class.new)
+
+ expect { subject.check([path]) }.
+ to raise_error(RuntimeError, /it requires downtime/)
+ end
+ end
+
+ context 'when a migration requires downtime' do
+ context 'when no reason is specified' do
+ it 'raises RuntimeError' do
+ stub_const('TestMigration::DOWNTIME', true)
+
+ expect(subject).to receive(:class_for_migration_file).
+ with(path).
+ and_return(TestMigration)
+
+ expect { subject.check([path]) }.
+ to raise_error(RuntimeError, /no reason was given/)
+ end
+ end
+
+ context 'when a reason is specified' do
+ it 'returns an Array of messages' do
+ stub_const('TestMigration::DOWNTIME', true)
+ stub_const('TestMigration::DOWNTIME_REASON', 'foo')
+
+ expect(subject).to receive(:class_for_migration_file).
+ with(path).
+ and_return(TestMigration)
+
+ messages = subject.check([path])
+
+ expect(messages).to be_an_instance_of(Array)
+ expect(messages[0]).to be_an_instance_of(Gitlab::DowntimeCheck::Message)
+
+ message = messages[0]
+
+ expect(message.path).to eq(path)
+ expect(message.offline).to eq(true)
+ expect(message.reason).to eq('foo')
+ end
+ end
+ end
+ end
+
+ describe '#check_and_print' do
+ it 'checks the migrations and prints the results to STDOUT' do
+ stub_const('TestMigration::DOWNTIME', true)
+ stub_const('TestMigration::DOWNTIME_REASON', 'foo')
+
+ expect(subject).to receive(:require).with(path)
+
+ expect(subject).to receive(:class_for_migration_file).
+ with(path).
+ and_return(TestMigration)
+
+ expect(subject).to receive(:puts).with(an_instance_of(String))
+
+ subject.check_and_print([path])
+ end
+ end
+
+ describe '#class_for_migration_file' do
+ it 'returns the class for a migration file path' do
+ expect(subject.class_for_migration_file('123_string.rb')).to eq(String)
+ end
+ end
+
+ describe '#online?' do
+ it 'returns true when a migration can be performed online' do
+ stub_const('TestMigration::DOWNTIME', false)
+
+ expect(subject.online?(TestMigration)).to eq(true)
+ end
+
+ it 'returns false when a migration can not be performed online' do
+ stub_const('TestMigration::DOWNTIME', true)
+
+ expect(subject.online?(TestMigration)).to eq(false)
+ end
+ end
+
+ describe '#downtime_reason' do
+ context 'when a reason is defined' do
+ it 'returns the downtime reason' do
+ stub_const('TestMigration::DOWNTIME_REASON', 'hello')
+
+ expect(subject.downtime_reason(TestMigration)).to eq('hello')
+ end
+ end
+
+ context 'when a reason is not defined' do
+ it 'returns nil' do
+ expect(subject.downtime_reason(Class.new)).to be_nil
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/email/attachment_uploader_spec.rb b/spec/lib/gitlab/email/attachment_uploader_spec.rb
index 476a21bf996..08b2577ecc4 100644
--- a/spec/lib/gitlab/email/attachment_uploader_spec.rb
+++ b/spec/lib/gitlab/email/attachment_uploader_spec.rb
@@ -11,7 +11,6 @@ describe Gitlab::Email::AttachmentUploader, lib: true do
link = links.first
expect(link).not_to be_nil
- expect(link[:is_image]).to be_truthy
expect(link[:alt]).to eq("bricks")
expect(link[:url]).to include("bricks.png")
end
diff --git a/spec/lib/gitlab/email/email_shared_blocks.rb b/spec/lib/gitlab/email/email_shared_blocks.rb
new file mode 100644
index 00000000000..19298e261e3
--- /dev/null
+++ b/spec/lib/gitlab/email/email_shared_blocks.rb
@@ -0,0 +1,41 @@
+require 'gitlab/email/receiver'
+
+shared_context :email_shared_context do
+ let(:mail_key) { "59d8df8370b7e95c5a49fbf86aeb2c93" }
+ let(:receiver) { Gitlab::Email::Receiver.new(email_raw) }
+ let(:markdown) { "![image](uploads/image.png)" }
+
+ def setup_attachment
+ allow_any_instance_of(Gitlab::Email::AttachmentUploader).to receive(:execute).and_return(
+ [
+ {
+ url: "uploads/image.png",
+ alt: "image",
+ markdown: markdown
+ }
+ ]
+ )
+ end
+end
+
+shared_examples :email_shared_examples do
+ context "when the user could not be found" do
+ before do
+ user.destroy
+ end
+
+ it "raises a UserNotFoundError" do
+ expect { receiver.execute }.to raise_error(Gitlab::Email::UserNotFoundError)
+ end
+ end
+
+ context "when the user is not authorized to the project" do
+ before do
+ project.update_attribute(:visibility_level, Project::PRIVATE)
+ end
+
+ it "raises a ProjectNotFound" do
+ expect { receiver.execute }.to raise_error(Gitlab::Email::ProjectNotFound)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/email/handler/create_issue_handler_spec.rb b/spec/lib/gitlab/email/handler/create_issue_handler_spec.rb
new file mode 100644
index 00000000000..a5cc7b02936
--- /dev/null
+++ b/spec/lib/gitlab/email/handler/create_issue_handler_spec.rb
@@ -0,0 +1,79 @@
+require 'spec_helper'
+require_relative '../email_shared_blocks'
+
+xdescribe Gitlab::Email::Handler::CreateIssueHandler, lib: true do
+ include_context :email_shared_context
+ it_behaves_like :email_shared_examples
+
+ before do
+ stub_incoming_email_setting(enabled: true, address: "incoming+%{key}@appmail.adventuretime.ooo")
+ stub_config_setting(host: 'localhost')
+ end
+
+ let(:email_raw) { fixture_file('emails/valid_new_issue.eml') }
+ let(:namespace) { create(:namespace, path: 'gitlabhq') }
+
+ let!(:project) { create(:project, :public, namespace: namespace) }
+ let!(:user) do
+ create(
+ :user,
+ email: 'jake@adventuretime.ooo',
+ authentication_token: 'auth_token'
+ )
+ end
+
+ context "when everything is fine" do
+ it "creates a new issue" do
+ setup_attachment
+
+ expect { receiver.execute }.to change { project.issues.count }.by(1)
+ issue = project.issues.last
+
+ expect(issue.author).to eq(user)
+ expect(issue.title).to eq('New Issue by email')
+ expect(issue.description).to include('reply by email')
+ expect(issue.description).to include(markdown)
+ end
+
+ context "when the reply is blank" do
+ let(:email_raw) { fixture_file("emails/valid_new_issue_empty.eml") }
+
+ it "creates a new issue" do
+ expect { receiver.execute }.to change { project.issues.count }.by(1)
+ issue = project.issues.last
+
+ expect(issue.author).to eq(user)
+ expect(issue.title).to eq('New Issue by email')
+ expect(issue.description).to eq('')
+ end
+ end
+ end
+
+ context "something is wrong" do
+ context "when the issue could not be saved" do
+ before do
+ allow_any_instance_of(Issue).to receive(:persisted?).and_return(false)
+ end
+
+ it "raises an InvalidIssueError" do
+ expect { receiver.execute }.to raise_error(Gitlab::Email::InvalidIssueError)
+ end
+ end
+
+ context "when we can't find the authentication_token" do
+ let(:email_raw) { fixture_file("emails/wrong_authentication_token.eml") }
+
+ it "raises an UserNotFoundError" do
+ expect { receiver.execute }.to raise_error(Gitlab::Email::UserNotFoundError)
+ end
+ end
+
+ context "when project is private" do
+ let(:project) { create(:project, :private, namespace: namespace) }
+
+ it "raises a ProjectNotFound if the user is not a member" do
+ expect { receiver.execute }.to raise_error(Gitlab::Email::ProjectNotFound)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/email/handler/create_note_handler_spec.rb b/spec/lib/gitlab/email/handler/create_note_handler_spec.rb
new file mode 100644
index 00000000000..4909fed6b77
--- /dev/null
+++ b/spec/lib/gitlab/email/handler/create_note_handler_spec.rb
@@ -0,0 +1,177 @@
+require 'spec_helper'
+require_relative '../email_shared_blocks'
+
+describe Gitlab::Email::Handler::CreateNoteHandler, lib: true do
+ include_context :email_shared_context
+ it_behaves_like :email_shared_examples
+
+ before do
+ stub_incoming_email_setting(enabled: true, address: "reply+%{key}@appmail.adventuretime.ooo")
+ stub_config_setting(host: 'localhost')
+ end
+
+ let(:email_raw) { fixture_file('emails/valid_reply.eml') }
+ let(:project) { create(:project, :public) }
+ let(:noteable) { create(:issue, project: project) }
+ let(:user) { create(:user) }
+
+ let!(:sent_notification) { SentNotification.record(noteable, user.id, mail_key) }
+
+ context "when the recipient address doesn't include a mail key" do
+ let(:email_raw) { fixture_file('emails/valid_reply.eml').gsub(mail_key, "") }
+
+ it "raises a UnknownIncomingEmail" do
+ expect { receiver.execute }.to raise_error(Gitlab::Email::UnknownIncomingEmail)
+ end
+ end
+
+ context "when no sent notification for the mail key could be found" do
+ let(:email_raw) { fixture_file('emails/wrong_mail_key.eml') }
+
+ it "raises a SentNotificationNotFoundError" do
+ expect { receiver.execute }.to raise_error(Gitlab::Email::SentNotificationNotFoundError)
+ end
+ end
+
+ context "when the email was auto generated" do
+ let!(:mail_key) { '636ca428858779856c226bb145ef4fad' }
+ let!(:email_raw) { fixture_file("emails/auto_reply.eml") }
+
+ it "raises an AutoGeneratedEmailError" do
+ expect { receiver.execute }.to raise_error(Gitlab::Email::AutoGeneratedEmailError)
+ end
+ end
+
+ context "when the noteable could not be found" do
+ before do
+ noteable.destroy
+ end
+
+ it "raises a NoteableNotFoundError" do
+ expect { receiver.execute }.to raise_error(Gitlab::Email::NoteableNotFoundError)
+ end
+ end
+
+ context "when the note could not be saved" do
+ before do
+ allow_any_instance_of(Note).to receive(:persisted?).and_return(false)
+ end
+
+ it "raises an InvalidNoteError" do
+ expect { receiver.execute }.to raise_error(Gitlab::Email::InvalidNoteError)
+ end
+
+ context 'because the note was commands only' do
+ let!(:email_raw) { fixture_file("emails/commands_only_reply.eml") }
+
+ context 'and current user cannot update noteable' do
+ it 'raises a CommandsOnlyNoteError' do
+ expect { receiver.execute }.to raise_error(Gitlab::Email::InvalidNoteError)
+ end
+ end
+
+ context 'and current user can update noteable' do
+ before do
+ project.team << [user, :developer]
+ end
+
+ it 'does not raise an error' do
+ expect(TodoService.new.todo_exist?(noteable, user)).to be_falsy
+
+ # One system note is created for the 'close' event
+ expect { receiver.execute }.to change { noteable.notes.count }.by(1)
+
+ expect(noteable.reload).to be_closed
+ expect(noteable.due_date).to eq(Date.tomorrow)
+ expect(TodoService.new.todo_exist?(noteable, user)).to be_truthy
+ end
+ end
+ end
+ end
+
+ context 'when the note contains slash commands' do
+ let!(:email_raw) { fixture_file("emails/commands_in_reply.eml") }
+
+ context 'and current user cannot update noteable' do
+ it 'post a note and does not update the noteable' do
+ expect(TodoService.new.todo_exist?(noteable, user)).to be_falsy
+
+ # One system note is created for the new note
+ expect { receiver.execute }.to change { noteable.notes.count }.by(1)
+
+ expect(noteable.reload).to be_open
+ expect(noteable.due_date).to be_nil
+ expect(TodoService.new.todo_exist?(noteable, user)).to be_falsy
+ end
+ end
+
+ context 'and current user can update noteable' do
+ before do
+ project.team << [user, :developer]
+ end
+
+ it 'post a note and updates the noteable' do
+ expect(TodoService.new.todo_exist?(noteable, user)).to be_falsy
+
+ # One system note is created for the new note, one for the 'close' event
+ expect { receiver.execute }.to change { noteable.notes.count }.by(2)
+
+ expect(noteable.reload).to be_closed
+ expect(noteable.due_date).to eq(Date.tomorrow)
+ expect(TodoService.new.todo_exist?(noteable, user)).to be_truthy
+ end
+ end
+ end
+
+ context "when the reply is blank" do
+ let!(:email_raw) { fixture_file("emails/no_content_reply.eml") }
+
+ it "raises an EmptyEmailError" do
+ expect { receiver.execute }.to raise_error(Gitlab::Email::EmptyEmailError)
+ end
+ end
+
+ context "when everything is fine" do
+ before do
+ setup_attachment
+ end
+
+ it "creates a comment" do
+ expect { receiver.execute }.to change { noteable.notes.count }.by(1)
+ note = noteable.notes.last
+
+ expect(note.author).to eq(sent_notification.recipient)
+ expect(note.note).to include("I could not disagree more.")
+ end
+
+ it "adds all attachments" do
+ receiver.execute
+
+ note = noteable.notes.last
+
+ expect(note.note).to include(markdown)
+ end
+
+ context 'when sub-addressing is not supported' do
+ before do
+ stub_incoming_email_setting(enabled: true, address: nil)
+ end
+
+ shared_examples 'an email that contains a mail key' do |header|
+ it "fetches the mail key from the #{header} header and creates a comment" do
+ expect { receiver.execute }.to change { noteable.notes.count }.by(1)
+ note = noteable.notes.last
+
+ expect(note.author).to eq(sent_notification.recipient)
+ expect(note.note).to include('I could not disagree more.')
+ end
+ end
+
+ context 'mail key is in the References header' do
+ let(:email_raw) { fixture_file('emails/reply_without_subaddressing_and_key_inside_references.eml') }
+
+ it_behaves_like 'an email that contains a mail key', 'References'
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/email/message/repository_push_spec.rb b/spec/lib/gitlab/email/message/repository_push_spec.rb
index c19f33e2224..5b966bddb6a 100644
--- a/spec/lib/gitlab/email/message/repository_push_spec.rb
+++ b/spec/lib/gitlab/email/message/repository_push_spec.rb
@@ -16,9 +16,12 @@ describe Gitlab::Email::Message::RepositoryPush do
{ author_id: author.id, ref: 'master', action: :push, compare: compare,
send_from_committer_email: true }
end
- let(:compare) do
+ let(:raw_compare) do
Gitlab::Git::Compare.new(project.repository.raw_repository,
- sample_image_commit.id, sample_commit.id)
+ sample_image_commit.id, sample_commit.id)
+ end
+ let(:compare) do
+ Compare.decorate(raw_compare, project)
end
describe '#project' do
@@ -62,17 +65,17 @@ describe Gitlab::Email::Message::RepositoryPush do
describe '#diffs_count' do
subject { message.diffs_count }
- it { is_expected.to eq compare.diffs.count }
+ it { is_expected.to eq raw_compare.diffs.size }
end
describe '#compare' do
subject { message.compare }
- it { is_expected.to be_an_instance_of Gitlab::Git::Compare }
+ it { is_expected.to be_an_instance_of Compare }
end
describe '#compare_timeout' do
subject { message.compare_timeout }
- it { is_expected.to eq compare.diffs.overflow? }
+ it { is_expected.to eq raw_compare.diffs.overflow? }
end
describe '#reverse_compare?' do
diff --git a/spec/lib/gitlab/email/receiver_spec.rb b/spec/lib/gitlab/email/receiver_spec.rb
index 36267faeb93..2a86b427806 100644
--- a/spec/lib/gitlab/email/receiver_spec.rb
+++ b/spec/lib/gitlab/email/receiver_spec.rb
@@ -1,34 +1,14 @@
-require "spec_helper"
+require 'spec_helper'
+require_relative 'email_shared_blocks'
describe Gitlab::Email::Receiver, lib: true do
- before do
- stub_incoming_email_setting(enabled: true, address: "reply+%{key}@appmail.adventuretime.ooo")
- stub_config_setting(host: 'localhost')
- end
-
- let(:reply_key) { "59d8df8370b7e95c5a49fbf86aeb2c93" }
- let(:email_raw) { fixture_file('emails/valid_reply.eml') }
-
- let(:project) { create(:project, :public) }
- let(:noteable) { create(:issue, project: project) }
- let(:user) { create(:user) }
- let!(:sent_notification) { SentNotification.record(noteable, user.id, reply_key) }
-
- let(:receiver) { described_class.new(email_raw) }
-
- context "when the recipient address doesn't include a reply key" do
- let(:email_raw) { fixture_file('emails/valid_reply.eml').gsub(reply_key, "") }
-
- it "raises a SentNotificationNotFoundError" do
- expect { receiver.execute }.to raise_error(Gitlab::Email::Receiver::SentNotificationNotFoundError)
- end
- end
+ include_context :email_shared_context
- context "when no sent notificiation for the reply key could be found" do
- let(:email_raw) { fixture_file('emails/wrong_reply_key.eml') }
+ context "when we cannot find a capable handler" do
+ let(:email_raw) { fixture_file('emails/valid_reply.eml').gsub(mail_key, "!!!") }
- it "raises a SentNotificationNotFoundError" do
- expect { receiver.execute }.to raise_error(Gitlab::Email::Receiver::SentNotificationNotFoundError)
+ it "raises a UnknownIncomingEmail" do
+ expect { receiver.execute }.to raise_error(Gitlab::Email::UnknownIncomingEmail)
end
end
@@ -36,129 +16,7 @@ describe Gitlab::Email::Receiver, lib: true do
let(:email_raw) { "" }
it "raises an EmptyEmailError" do
- expect { receiver.execute }.to raise_error(Gitlab::Email::Receiver::EmptyEmailError)
- end
- end
-
- context "when the email was auto generated" do
- let!(:reply_key) { '636ca428858779856c226bb145ef4fad' }
- let!(:email_raw) { fixture_file("emails/auto_reply.eml") }
-
- it "raises an AutoGeneratedEmailError" do
- expect { receiver.execute }.to raise_error(Gitlab::Email::Receiver::AutoGeneratedEmailError)
- end
- end
-
- context "when the user could not be found" do
- before do
- user.destroy
- end
-
- it "raises a UserNotFoundError" do
- expect { receiver.execute }.to raise_error(Gitlab::Email::Receiver::UserNotFoundError)
- end
- end
-
- context "when the user has been blocked" do
- before do
- user.block
- end
-
- it "raises a UserBlockedError" do
- expect { receiver.execute }.to raise_error(Gitlab::Email::Receiver::UserBlockedError)
- end
- end
-
- context "when the user is not authorized to create a note" do
- before do
- project.update_attribute(:visibility_level, Project::PRIVATE)
- end
-
- it "raises a UserNotAuthorizedError" do
- expect { receiver.execute }.to raise_error(Gitlab::Email::Receiver::UserNotAuthorizedError)
- end
- end
-
- context "when the noteable could not be found" do
- before do
- noteable.destroy
- end
-
- it "raises a NoteableNotFoundError" do
- expect { receiver.execute }.to raise_error(Gitlab::Email::Receiver::NoteableNotFoundError)
- end
- end
-
- context "when the reply is blank" do
- let!(:email_raw) { fixture_file("emails/no_content_reply.eml") }
-
- it "raises an EmptyEmailError" do
- expect { receiver.execute }.to raise_error(Gitlab::Email::Receiver::EmptyEmailError)
- end
- end
-
- context "when the note could not be saved" do
- before do
- allow_any_instance_of(Note).to receive(:persisted?).and_return(false)
- end
-
- it "raises an InvalidNoteError" do
- expect { receiver.execute }.to raise_error(Gitlab::Email::Receiver::InvalidNoteError)
- end
- end
-
- context "when everything is fine" do
- let(:markdown) { "![image](uploads/image.png)" }
-
- before do
- allow_any_instance_of(Gitlab::Email::AttachmentUploader).to receive(:execute).and_return(
- [
- {
- url: "uploads/image.png",
- is_image: true,
- alt: "image",
- markdown: markdown
- }
- ]
- )
- end
-
- it "creates a comment" do
- expect { receiver.execute }.to change { noteable.notes.count }.by(1)
- note = noteable.notes.last
-
- expect(note.author).to eq(sent_notification.recipient)
- expect(note.note).to include("I could not disagree more.")
- end
-
- it "adds all attachments" do
- receiver.execute
-
- note = noteable.notes.last
-
- expect(note.note).to include(markdown)
- end
-
- context 'when sub-addressing is not supported' do
- before do
- stub_incoming_email_setting(enabled: true, address: nil)
- end
-
- shared_examples 'an email that contains a reply key' do |header|
- it "fetches the reply key from the #{header} header and creates a comment" do
- expect { receiver.execute }.to change { noteable.notes.count }.by(1)
- note = noteable.notes.last
-
- expect(note.author).to eq(sent_notification.recipient)
- expect(note.note).to include('I could not disagree more.')
- end
- end
-
- context 'reply key is in the References header' do
- let(:email_raw) { fixture_file('emails/reply_without_subaddressing_and_key_inside_references.eml') }
-
- it_behaves_like 'an email that contains a reply key', 'References'
- end
+ expect { receiver.execute }.to raise_error(Gitlab::Email::EmptyEmailError)
end
end
end
diff --git a/spec/lib/gitlab/git/hook_spec.rb b/spec/lib/gitlab/git/hook_spec.rb
index a15aa173fbd..d1f947b6850 100644
--- a/spec/lib/gitlab/git/hook_spec.rb
+++ b/spec/lib/gitlab/git/hook_spec.rb
@@ -25,7 +25,6 @@ describe Gitlab::Git::Hook, lib: true do
end
['pre-receive', 'post-receive', 'update'].each do |hook_name|
-
context "when triggering a #{hook_name} hook" do
context "when the hook is successful" do
it "returns success with no errors" do
diff --git a/spec/lib/gitlab/git_access_spec.rb b/spec/lib/gitlab/git_access_spec.rb
index c79ba11f782..f12c9a370f7 100644
--- a/spec/lib/gitlab/git_access_spec.rb
+++ b/spec/lib/gitlab/git_access_spec.rb
@@ -6,67 +6,6 @@ describe Gitlab::GitAccess, lib: true do
let(:user) { create(:user) }
let(:actor) { user }
- describe 'can_push_to_branch?' do
- describe 'push to none protected branch' do
- it "returns true if user is a master" do
- project.team << [user, :master]
- expect(access.can_push_to_branch?("random_branch")).to be_truthy
- end
-
- it "returns true if user is a developer" do
- project.team << [user, :developer]
- expect(access.can_push_to_branch?("random_branch")).to be_truthy
- end
-
- it "returns false if user is a reporter" do
- project.team << [user, :reporter]
- expect(access.can_push_to_branch?("random_branch")).to be_falsey
- end
- end
-
- describe 'push to protected branch' do
- before do
- @branch = create :protected_branch, project: project
- end
-
- it "returns true if user is a master" do
- project.team << [user, :master]
- expect(access.can_push_to_branch?(@branch.name)).to be_truthy
- end
-
- it "returns false if user is a developer" do
- project.team << [user, :developer]
- expect(access.can_push_to_branch?(@branch.name)).to be_falsey
- end
-
- it "returns false if user is a reporter" do
- project.team << [user, :reporter]
- expect(access.can_push_to_branch?(@branch.name)).to be_falsey
- end
- end
-
- describe 'push to protected branch if allowed for developers' do
- before do
- @branch = create :protected_branch, project: project, developers_can_push: true
- end
-
- it "returns true if user is a master" do
- project.team << [user, :master]
- expect(access.can_push_to_branch?(@branch.name)).to be_truthy
- end
-
- it "returns true if user is a developer" do
- project.team << [user, :developer]
- expect(access.can_push_to_branch?(@branch.name)).to be_truthy
- end
-
- it "returns false if user is a reporter" do
- project.team << [user, :reporter]
- expect(access.can_push_to_branch?(@branch.name)).to be_falsey
- end
- end
- end
-
describe '#check with single protocols allowed' do
def disable_protocol(protocol)
settings = ::ApplicationSetting.create_from_defaults
@@ -80,11 +19,11 @@ describe Gitlab::GitAccess, lib: true do
end
it 'blocks ssh git push' do
- expect(@acc.check('git-receive-pack').allowed?).to be_falsey
+ expect(@acc.check('git-receive-pack', '_any').allowed?).to be_falsey
end
it 'blocks ssh git pull' do
- expect(@acc.check('git-upload-pack').allowed?).to be_falsey
+ expect(@acc.check('git-upload-pack', '_any').allowed?).to be_falsey
end
end
@@ -95,22 +34,22 @@ describe Gitlab::GitAccess, lib: true do
end
it 'blocks http push' do
- expect(@acc.check('git-receive-pack').allowed?).to be_falsey
+ expect(@acc.check('git-receive-pack', '_any').allowed?).to be_falsey
end
it 'blocks http git pull' do
- expect(@acc.check('git-upload-pack').allowed?).to be_falsey
+ expect(@acc.check('git-upload-pack', '_any').allowed?).to be_falsey
end
end
end
describe 'download_access_check' do
+ subject { access.check('git-upload-pack', '_any') }
+
describe 'master permissions' do
before { project.team << [user, :master] }
context 'pull code' do
- subject { access.download_access_check }
-
it { expect(subject.allowed?).to be_truthy }
end
end
@@ -119,8 +58,6 @@ describe Gitlab::GitAccess, lib: true do
before { project.team << [user, :guest] }
context 'pull code' do
- subject { access.download_access_check }
-
it { expect(subject.allowed?).to be_falsey }
end
end
@@ -132,16 +69,12 @@ describe Gitlab::GitAccess, lib: true do
end
context 'pull code' do
- subject { access.download_access_check }
-
it { expect(subject.allowed?).to be_falsey }
end
end
describe 'without acccess to project' do
context 'pull code' do
- subject { access.download_access_check }
-
it { expect(subject.allowed?).to be_falsey }
end
end
@@ -151,113 +84,237 @@ describe Gitlab::GitAccess, lib: true do
let(:actor) { key }
context 'pull code' do
- before { key.projects << project }
- subject { access.download_access_check }
+ context 'when project is authorized' do
+ before { key.projects << project }
- it { expect(subject.allowed?).to be_truthy }
+ it { expect(subject).to be_allowed }
+ end
+
+ context 'when unauthorized' do
+ context 'from public project' do
+ let(:project) { create(:project, :public) }
+
+ it { expect(subject).to be_allowed }
+ end
+
+ context 'from internal project' do
+ let(:project) { create(:project, :internal) }
+
+ it { expect(subject).not_to be_allowed }
+ end
+
+ context 'from private project' do
+ let(:project) { create(:project, :internal) }
+
+ it { expect(subject).not_to be_allowed }
+ end
+ end
end
end
end
describe 'push_access_check' do
- def protect_feature_branch
- create(:protected_branch, name: 'feature', project: project)
- end
+ before { merge_into_protected_branch }
+ let(:unprotected_branch) { FFaker::Internet.user_name }
- def changes
- {
- push_new_branch: "#{Gitlab::Git::BLANK_SHA} 570e7b2ab refs/heads/wow",
+ let(:changes) do
+ { push_new_branch: "#{Gitlab::Git::BLANK_SHA} 570e7b2ab refs/heads/wow",
push_master: '6f6d7e7ed 570e7b2ab refs/heads/master',
push_protected_branch: '6f6d7e7ed 570e7b2ab refs/heads/feature',
push_remove_protected_branch: "570e7b2ab #{Gitlab::Git::BLANK_SHA} "\
'refs/heads/feature',
push_tag: '6f6d7e7ed 570e7b2ab refs/tags/v1.0.0',
push_new_tag: "#{Gitlab::Git::BLANK_SHA} 570e7b2ab refs/tags/v7.8.9",
- push_all: ['6f6d7e7ed 570e7b2ab refs/heads/master', '6f6d7e7ed 570e7b2ab refs/heads/feature']
- }
+ push_all: ['6f6d7e7ed 570e7b2ab refs/heads/master', '6f6d7e7ed 570e7b2ab refs/heads/feature'],
+ merge_into_protected_branch: "0b4bc9a #{merge_into_protected_branch} refs/heads/feature" }
end
- def self.permissions_matrix
- {
- master: {
- push_new_branch: true,
- push_master: true,
- push_protected_branch: true,
- push_remove_protected_branch: false,
- push_tag: true,
- push_new_tag: true,
- push_all: true,
- },
-
- developer: {
- push_new_branch: true,
- push_master: true,
- push_protected_branch: false,
- push_remove_protected_branch: false,
- push_tag: false,
- push_new_tag: true,
- push_all: false,
- },
-
- reporter: {
- push_new_branch: false,
- push_master: false,
- push_protected_branch: false,
- push_remove_protected_branch: false,
- push_tag: false,
- push_new_tag: false,
- push_all: false,
- },
-
- guest: {
- push_new_branch: false,
- push_master: false,
- push_protected_branch: false,
- push_remove_protected_branch: false,
- push_tag: false,
- push_new_tag: false,
- push_all: false,
- }
- }
+ def stub_git_hooks
+ # Running the `pre-receive` hook is expensive, and not necessary for this test.
+ allow_any_instance_of(GitHooksService).to receive(:execute).and_yield
end
- def self.updated_permissions_matrix
- updated_permissions_matrix = permissions_matrix.dup
- updated_permissions_matrix[:developer][:push_protected_branch] = true
- updated_permissions_matrix[:developer][:push_all] = true
- updated_permissions_matrix
+ def merge_into_protected_branch
+ @protected_branch_merge_commit ||= begin
+ stub_git_hooks
+ project.repository.add_branch(user, unprotected_branch, 'feature')
+ target_branch = project.repository.lookup('feature')
+ source_branch = project.repository.commit_file(user, FFaker::InternetSE.login_user_name, FFaker::HipsterIpsum.paragraph, FFaker::HipsterIpsum.sentence, unprotected_branch, false)
+ rugged = project.repository.rugged
+ author = { email: "email@example.com", time: Time.now, name: "Example Git User" }
+
+ merge_index = rugged.merge_commits(target_branch, source_branch)
+ Rugged::Commit.create(rugged, author: author, committer: author, message: "commit message", parents: [target_branch, source_branch], tree: merge_index.write_tree(rugged))
+ end
end
- permissions_matrix.keys.each do |role|
- describe "#{role} access" do
- before { protect_feature_branch }
- before { project.team << [user, role] }
+ def self.run_permission_checks(permissions_matrix)
+ permissions_matrix.keys.each do |role|
+ describe "#{role} access" do
+ before do
+ if role == :admin
+ user.update_attribute(:admin, true)
+ else
+ project.team << [user, role]
+ end
+ end
- permissions_matrix[role].each do |action, allowed|
- context action do
- subject { access.push_access_check(changes[action]) }
+ permissions_matrix[role].each do |action, allowed|
+ context action do
+ subject { access.push_access_check(changes[action]) }
- it { expect(subject.allowed?).to allowed ? be_truthy : be_falsey }
+ it { expect(subject.allowed?).to allowed ? be_truthy : be_falsey }
+ end
end
end
end
end
- context "with enabled developers push to protected branches " do
- updated_permissions_matrix.keys.each do |role|
- describe "#{role} access" do
- before { create(:protected_branch, name: 'feature', developers_can_push: true, project: project) }
- before { project.team << [user, role] }
+ permissions_matrix = {
+ admin: {
+ push_new_branch: true,
+ push_master: true,
+ push_protected_branch: true,
+ push_remove_protected_branch: false,
+ push_tag: true,
+ push_new_tag: true,
+ push_all: true,
+ merge_into_protected_branch: true
+ },
+
+ master: {
+ push_new_branch: true,
+ push_master: true,
+ push_protected_branch: true,
+ push_remove_protected_branch: false,
+ push_tag: true,
+ push_new_tag: true,
+ push_all: true,
+ merge_into_protected_branch: true
+ },
+
+ developer: {
+ push_new_branch: true,
+ push_master: true,
+ push_protected_branch: false,
+ push_remove_protected_branch: false,
+ push_tag: false,
+ push_new_tag: true,
+ push_all: false,
+ merge_into_protected_branch: false
+ },
+
+ reporter: {
+ push_new_branch: false,
+ push_master: false,
+ push_protected_branch: false,
+ push_remove_protected_branch: false,
+ push_tag: false,
+ push_new_tag: false,
+ push_all: false,
+ merge_into_protected_branch: false
+ },
+
+ guest: {
+ push_new_branch: false,
+ push_master: false,
+ push_protected_branch: false,
+ push_remove_protected_branch: false,
+ push_tag: false,
+ push_new_tag: false,
+ push_all: false,
+ merge_into_protected_branch: false
+ }
+ }
- updated_permissions_matrix[role].each do |action, allowed|
- context action do
- subject { access.push_access_check(changes[action]) }
+ [['feature', 'exact'], ['feat*', 'wildcard']].each do |protected_branch_name, protected_branch_type|
+ context do
+ before { create(:protected_branch, name: protected_branch_name, project: project) }
- it { expect(subject.allowed?).to allowed ? be_truthy : be_falsey }
+ run_permission_checks(permissions_matrix)
+ end
+
+ context "when developers are allowed to push into the #{protected_branch_type} protected branch" do
+ before { create(:protected_branch, :developers_can_push, name: protected_branch_name, project: project) }
+
+ run_permission_checks(permissions_matrix.deep_merge(developer: { push_protected_branch: true, push_all: true, merge_into_protected_branch: true }))
+ end
+
+ context "developers are allowed to merge into the #{protected_branch_type} protected branch" do
+ before { create(:protected_branch, :developers_can_merge, name: protected_branch_name, project: project) }
+
+ context "when a merge request exists for the given source/target branch" do
+ context "when the merge request is in progress" do
+ before do
+ create(:merge_request, source_project: project, source_branch: unprotected_branch, target_branch: 'feature',
+ state: 'locked', in_progress_merge_commit_sha: merge_into_protected_branch)
end
+
+ run_permission_checks(permissions_matrix.deep_merge(developer: { merge_into_protected_branch: true }))
+ end
+
+ context "when the merge request is not in progress" do
+ before do
+ create(:merge_request, source_project: project, source_branch: unprotected_branch, target_branch: 'feature', in_progress_merge_commit_sha: nil)
+ end
+
+ run_permission_checks(permissions_matrix.deep_merge(developer: { merge_into_protected_branch: false }))
+ end
+
+ context "when a merge request does not exist for the given source/target branch" do
+ run_permission_checks(permissions_matrix.deep_merge(developer: { merge_into_protected_branch: false }))
end
end
end
+
+ context "when developers are allowed to push and merge into the #{protected_branch_type} protected branch" do
+ before { create(:protected_branch, :developers_can_merge, :developers_can_push, name: protected_branch_name, project: project) }
+
+ run_permission_checks(permissions_matrix.deep_merge(developer: { push_protected_branch: true, push_all: true, merge_into_protected_branch: true }))
+ end
+
+ context "when no one is allowed to push to the #{protected_branch_name} protected branch" do
+ before { create(:protected_branch, :no_one_can_push, name: protected_branch_name, project: project) }
+
+ run_permission_checks(permissions_matrix.deep_merge(developer: { push_protected_branch: false, push_all: false, merge_into_protected_branch: false },
+ master: { push_protected_branch: false, push_all: false, merge_into_protected_branch: false },
+ admin: { push_protected_branch: false, push_all: false, merge_into_protected_branch: false }))
+ end
+ end
+ end
+
+ describe 'deploy key permissions' do
+ let(:key) { create(:deploy_key) }
+ let(:actor) { key }
+
+ context 'push code' do
+ subject { access.check('git-receive-pack', '_any') }
+
+ context 'when project is authorized' do
+ before { key.projects << project }
+
+ it { expect(subject).not_to be_allowed }
+ end
+
+ context 'when unauthorized' do
+ context 'to public project' do
+ let(:project) { create(:project, :public) }
+
+ it { expect(subject).not_to be_allowed }
+ end
+
+ context 'to internal project' do
+ let(:project) { create(:project, :internal) }
+
+ it { expect(subject).not_to be_allowed }
+ end
+
+ context 'to private project' do
+ let(:project) { create(:project, :internal) }
+
+ it { expect(subject).not_to be_allowed }
+ end
+ end
end
end
end
diff --git a/spec/lib/gitlab/github_import/branch_formatter_spec.rb b/spec/lib/gitlab/github_import/branch_formatter_spec.rb
index fc9d5204148..e5300dbba1e 100644
--- a/spec/lib/gitlab/github_import/branch_formatter_spec.rb
+++ b/spec/lib/gitlab/github_import/branch_formatter_spec.rb
@@ -32,20 +32,6 @@ describe Gitlab::GithubImport::BranchFormatter, lib: true do
end
end
- describe '#name' do
- it 'returns raw ref when branch exists' do
- branch = described_class.new(project, double(raw))
-
- expect(branch.name).to eq 'feature'
- end
-
- it 'returns formatted ref when branch does not exist' do
- branch = described_class.new(project, double(raw.merge(ref: 'removed-branch', sha: '2e5d3239642f9161dcbbc4b70a211a68e5e45e2b')))
-
- expect(branch.name).to eq 'removed-branch-2e5d3239'
- end
- end
-
describe '#repo' do
it 'returns raw repo' do
branch = described_class.new(project, double(raw))
diff --git a/spec/lib/gitlab/github_import/client_spec.rb b/spec/lib/gitlab/github_import/client_spec.rb
index 3b023a35446..613c47d55f1 100644
--- a/spec/lib/gitlab/github_import/client_spec.rb
+++ b/spec/lib/gitlab/github_import/client_spec.rb
@@ -61,4 +61,11 @@ describe Gitlab::GithubImport::Client, lib: true do
expect(client.api.api_endpoint).to eq 'https://github.company.com/'
end
end
+
+ it 'does not raise error when rate limit is disabled' do
+ stub_request(:get, /api.github.com/)
+ allow(client.api).to receive(:rate_limit!).and_raise(Octokit::NotFound)
+
+ expect { client.issues }.not_to raise_error
+ end
end
diff --git a/spec/lib/gitlab/github_import/hook_formatter_spec.rb b/spec/lib/gitlab/github_import/hook_formatter_spec.rb
deleted file mode 100644
index 110ba428258..00000000000
--- a/spec/lib/gitlab/github_import/hook_formatter_spec.rb
+++ /dev/null
@@ -1,65 +0,0 @@
-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/github_import/importer_spec.rb b/spec/lib/gitlab/github_import/importer_spec.rb
new file mode 100644
index 00000000000..b7c3bc4e1a7
--- /dev/null
+++ b/spec/lib/gitlab/github_import/importer_spec.rb
@@ -0,0 +1,132 @@
+require 'spec_helper'
+
+describe Gitlab::GithubImport::Importer, lib: true do
+ describe '#execute' do
+ context 'when an error occurs' do
+ let(:project) { create(:project, import_url: 'https://github.com/octocat/Hello-World.git', wiki_enabled: false) }
+ let(:octocat) { double(id: 123456, login: 'octocat') }
+ let(:created_at) { DateTime.strptime('2011-01-26T19:01:12Z') }
+ let(:updated_at) { DateTime.strptime('2011-01-27T19:01:12Z') }
+ let(:repository) { double(id: 1, fork: false) }
+ let(:source_sha) { create(:commit, project: project).id }
+ let(:source_branch) { double(ref: 'feature', repo: repository, sha: source_sha) }
+ let(:target_sha) { create(:commit, project: project, git_commit: RepoHelpers.another_sample_commit).id }
+ let(:target_branch) { double(ref: 'master', repo: repository, sha: target_sha) }
+
+ let(:label) do
+ double(
+ name: 'Bug',
+ color: 'ff0000',
+ url: 'https://api.github.com/repos/octocat/Hello-World/labels/bug'
+ )
+ end
+
+ let(:milestone) do
+ double(
+ number: 1347,
+ state: 'open',
+ title: '1.0',
+ description: 'Version 1.0',
+ due_on: nil,
+ created_at: created_at,
+ updated_at: updated_at,
+ closed_at: nil,
+ url: 'https://api.github.com/repos/octocat/Hello-World/milestones/1'
+ )
+ end
+
+ let(:issue1) do
+ double(
+ number: 1347,
+ milestone: nil,
+ state: 'open',
+ title: 'Found a bug',
+ body: "I'm having a problem with this.",
+ assignee: nil,
+ user: octocat,
+ comments: 0,
+ pull_request: nil,
+ created_at: created_at,
+ updated_at: updated_at,
+ closed_at: nil,
+ url: 'https://api.github.com/repos/octocat/Hello-World/issues/1347'
+ )
+ end
+
+ let(:issue2) do
+ double(
+ number: 1348,
+ milestone: nil,
+ state: 'open',
+ title: nil,
+ body: "I'm having a problem with this.",
+ assignee: nil,
+ user: octocat,
+ comments: 0,
+ pull_request: nil,
+ created_at: created_at,
+ updated_at: updated_at,
+ closed_at: nil,
+ url: 'https://api.github.com/repos/octocat/Hello-World/issues/1348'
+ )
+ end
+
+ let(:pull_request) do
+ double(
+ number: 1347,
+ milestone: nil,
+ state: 'open',
+ title: 'New feature',
+ body: 'Please pull these awesome changes',
+ head: source_branch,
+ base: target_branch,
+ assignee: nil,
+ user: octocat,
+ created_at: created_at,
+ updated_at: updated_at,
+ closed_at: nil,
+ merged_at: nil,
+ url: 'https://api.github.com/repos/octocat/Hello-World/pulls/1347'
+ )
+ end
+
+ before do
+ allow(project).to receive(:import_data).and_return(double.as_null_object)
+ allow_any_instance_of(Octokit::Client).to receive(:rate_limit!).and_raise(Octokit::NotFound)
+ allow_any_instance_of(Octokit::Client).to receive(:labels).and_return([label, label])
+ allow_any_instance_of(Octokit::Client).to receive(:milestones).and_return([milestone, milestone])
+ allow_any_instance_of(Octokit::Client).to receive(:issues).and_return([issue1, issue2])
+ allow_any_instance_of(Octokit::Client).to receive(:pull_requests).and_return([pull_request, pull_request])
+ allow_any_instance_of(Octokit::Client).to receive(:last_response).and_return(double(rels: { next: nil }))
+ allow_any_instance_of(Gitlab::Shell).to receive(:import_repository).and_raise(Gitlab::Shell::Error)
+ end
+
+ it 'returns true' do
+ expect(described_class.new(project).execute).to eq true
+ end
+
+ it 'does not raise an error' do
+ expect { described_class.new(project).execute }.not_to raise_error
+ end
+
+ it 'stores error messages' do
+ error = {
+ message: 'The remote data could not be fully imported.',
+ errors: [
+ { type: :label, url: "https://api.github.com/repos/octocat/Hello-World/labels/bug", errors: "Validation failed: Title has already been taken" },
+ { type: :milestone, url: "https://api.github.com/repos/octocat/Hello-World/milestones/1", errors: "Validation failed: Title has already been taken" },
+ { type: :issue, url: "https://api.github.com/repos/octocat/Hello-World/issues/1347", errors: "Invalid Repository. Use user/repo format." },
+ { type: :issue, url: "https://api.github.com/repos/octocat/Hello-World/issues/1348", errors: "Validation failed: Title can't be blank, Title is too short (minimum is 0 characters)" },
+ { type: :pull_request, url: "https://api.github.com/repos/octocat/Hello-World/pulls/1347", errors: "Invalid Repository. Use user/repo format." },
+ { type: :pull_request, url: "https://api.github.com/repos/octocat/Hello-World/pulls/1347", errors: "Validation failed: Validate branches Cannot Create: This merge request already exists: [\"New feature\"]" },
+ { type: :wiki, errors: "Gitlab::Shell::Error" }
+ ]
+ }
+
+ described_class.new(project).execute
+
+ expect(project.import_error).to eq error.to_json
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb b/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb
index 79931ecd134..b667abf063d 100644
--- a/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb
+++ b/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb
@@ -9,6 +9,7 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do
let(:source_branch) { double(ref: 'feature', repo: source_repo, sha: source_sha) }
let(:target_repo) { repository }
let(:target_branch) { double(ref: 'master', repo: target_repo, sha: target_sha) }
+ let(:removed_branch) { double(ref: 'removed-branch', repo: source_repo, sha: '2e5d3239642f9161dcbbc4b70a211a68e5e45e2b') }
let(:octocat) { double(id: 123456, login: 'octocat') }
let(:created_at) { DateTime.strptime('2011-01-26T19:01:12Z') }
let(:updated_at) { DateTime.strptime('2011-01-27T19:01:12Z') }
@@ -26,7 +27,8 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do
created_at: created_at,
updated_at: updated_at,
closed_at: nil,
- merged_at: nil
+ merged_at: nil,
+ url: 'https://api.github.com/repos/octocat/Hello-World/pulls/1347'
}
end
@@ -165,6 +167,42 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do
end
end
+ describe '#source_branch_name' do
+ context 'when source branch exists' do
+ let(:raw_data) { double(base_data) }
+
+ it 'returns branch ref' do
+ expect(pull_request.source_branch_name).to eq 'feature'
+ end
+ end
+
+ context 'when source branch does not exist' do
+ let(:raw_data) { double(base_data.merge(head: removed_branch)) }
+
+ it 'prefixes branch name with pull request number' do
+ expect(pull_request.source_branch_name).to eq 'pull/1347/removed-branch'
+ end
+ end
+ end
+
+ describe '#target_branch_name' do
+ context 'when source branch exists' do
+ let(:raw_data) { double(base_data) }
+
+ it 'returns branch ref' do
+ expect(pull_request.target_branch_name).to eq 'master'
+ end
+ end
+
+ context 'when target branch does not exist' do
+ let(:raw_data) { double(base_data.merge(base: removed_branch)) }
+
+ it 'prefixes branch name with pull request number' do
+ expect(pull_request.target_branch_name).to eq 'pull/1347/removed-branch'
+ end
+ end
+ end
+
describe '#valid?' do
context 'when source, and target repos are not a fork' do
let(:raw_data) { double(base_data) }
@@ -178,8 +216,8 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do
let(:source_repo) { double(id: 2) }
let(:raw_data) { double(base_data) }
- it 'returns false' do
- expect(pull_request.valid?).to eq false
+ it 'returns true' do
+ expect(pull_request.valid?).to eq true
end
end
@@ -187,9 +225,17 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do
let(:target_repo) { double(id: 2) }
let(:raw_data) { double(base_data) }
- it 'returns false' do
- expect(pull_request.valid?).to eq false
+ it 'returns true' do
+ expect(pull_request.valid?).to eq true
end
end
end
+
+ describe '#url' do
+ let(:raw_data) { double(base_data) }
+
+ it 'return raw url' do
+ expect(pull_request.url).to eq 'https://api.github.com/repos/octocat/Hello-World/pulls/1347'
+ end
+ end
end
diff --git a/spec/lib/gitlab/gitlab_import/importer_spec.rb b/spec/lib/gitlab/gitlab_import/importer_spec.rb
new file mode 100644
index 00000000000..d3f1deb3837
--- /dev/null
+++ b/spec/lib/gitlab/gitlab_import/importer_spec.rb
@@ -0,0 +1,53 @@
+require 'spec_helper'
+
+describe Gitlab::GitlabImport::Importer, lib: true do
+ include ImportSpecHelper
+
+ describe '#execute' do
+ before do
+ stub_omniauth_provider('gitlab')
+ stub_request('issues', [
+ {
+ 'id' => 2579857,
+ 'iid' => 3,
+ 'title' => 'Issue',
+ 'description' => 'Lorem ipsum',
+ 'state' => 'opened',
+ 'author' => {
+ 'id' => 283999,
+ 'name' => 'John Doe'
+ }
+ }
+ ])
+ stub_request('issues/2579857/notes', [])
+ end
+
+ it 'persists issues' do
+ project = create(:empty_project, import_source: 'asd/vim')
+ project.build_import_data(credentials: { password: 'password' })
+
+ subject = described_class.new(project)
+ subject.execute
+
+ expected_attributes = {
+ iid: 3,
+ title: 'Issue',
+ description: "*Created by: John Doe*\n\nLorem ipsum",
+ state: 'opened',
+ author_id: project.creator_id
+ }
+
+ expect(project.issues.first).to have_attributes(expected_attributes)
+ end
+
+ def stub_request(path, body)
+ url = "https://gitlab.com/api/v3/projects/asd%2Fvim/#{path}?page=1&per_page=100"
+
+ WebMock.stub_request(:get, url).
+ to_return(
+ headers: { 'Content-Type' => 'application/json' },
+ body: body
+ )
+ end
+ end
+end
diff --git a/spec/lib/gitlab/gitorious_import/project_creator_spec.rb b/spec/lib/gitlab/gitorious_import/project_creator_spec.rb
deleted file mode 100644
index 946712ca38e..00000000000
--- a/spec/lib/gitlab/gitorious_import/project_creator_spec.rb
+++ /dev/null
@@ -1,26 +0,0 @@
-require 'spec_helper'
-
-describe Gitlab::GitoriousImport::ProjectCreator, lib: true do
- let(:user) { create(:user) }
- let(:repo) { Gitlab::GitoriousImport::Repository.new('foo/bar-baz-qux') }
- let(:namespace){ create(:group, owner: user) }
-
- before do
- namespace.add_owner(user)
- end
-
- it 'creates project' do
- allow_any_instance_of(Project).to receive(:add_import_job)
-
- project_creator = Gitlab::GitoriousImport::ProjectCreator.new(repo, namespace, user)
- project = project_creator.execute
-
- expect(project.name).to eq("Bar Baz Qux")
- expect(project.path).to eq("bar-baz-qux")
- expect(project.namespace).to eq(namespace)
- expect(project.visibility_level).to eq(Gitlab::VisibilityLevel::PUBLIC)
- expect(project.import_type).to eq("gitorious")
- expect(project.import_source).to eq("foo/bar-baz-qux")
- expect(project.import_url).to eq("https://gitorious.org/foo/bar-baz-qux.git")
- end
-end
diff --git a/spec/lib/gitlab/highlight_spec.rb b/spec/lib/gitlab/highlight_spec.rb
index 364532e94e3..fc021416d92 100644
--- a/spec/lib/gitlab/highlight_spec.rb
+++ b/spec/lib/gitlab/highlight_spec.rb
@@ -17,6 +17,18 @@ describe Gitlab::Highlight, lib: true do
expect(lines[21]).to eq(%Q{<span id="LC22" class="line"> <span class="k">unless</span> <span class="no">File</span><span class="p">.</span><span class="nf">directory?</span><span class="p">(</span><span class="n">path</span><span class="p">)</span></span>\n})
expect(lines[26]).to eq(%Q{<span id="LC27" class="line"> <span class="vi">@cmd_status</span> <span class="o">=</span> <span class="mi">0</span></span>\n})
end
+
+ describe 'with CRLF' do
+ let(:branch) { 'crlf-diff' }
+ let(:blob) { repository.blob_at_branch(branch, path) }
+ let(:lines) do
+ Gitlab::Highlight.highlight_lines(project.repository, 'crlf-diff', 'files/whitespace')
+ end
+
+ it 'strips extra LFs' do
+ expect(lines[0]).to eq("<span id=\"LC1\" class=\"line\">test </span>")
+ end
+ end
end
describe 'custom highlighting from .gitattributes' do
diff --git a/spec/lib/gitlab/import_export/avatar_restorer_spec.rb b/spec/lib/gitlab/import_export/avatar_restorer_spec.rb
new file mode 100644
index 00000000000..5ae178414cc
--- /dev/null
+++ b/spec/lib/gitlab/import_export/avatar_restorer_spec.rb
@@ -0,0 +1,25 @@
+require 'spec_helper'
+
+describe Gitlab::ImportExport::AvatarRestorer, lib: true do
+ let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: 'test') }
+ let(:project) { create(:empty_project) }
+
+ before do
+ allow_any_instance_of(described_class).to receive(:avatar_export_file)
+ .and_return(Rails.root + "spec/fixtures/dk.png")
+ end
+
+ after do
+ project.remove_avatar!
+ end
+
+ it 'restores a project avatar' do
+ expect(described_class.new(project: project, shared: shared).restore).to be true
+ end
+
+ it 'saves the avatar into the project' do
+ described_class.new(project: project, shared: shared).restore
+
+ expect(project.reload.avatar.file.exists?).to be true
+ end
+end
diff --git a/spec/lib/gitlab/import_export/avatar_saver_spec.rb b/spec/lib/gitlab/import_export/avatar_saver_spec.rb
new file mode 100644
index 00000000000..d6ee94442cb
--- /dev/null
+++ b/spec/lib/gitlab/import_export/avatar_saver_spec.rb
@@ -0,0 +1,27 @@
+require 'spec_helper'
+
+describe Gitlab::ImportExport::AvatarSaver, lib: true do
+ let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: 'test') }
+ let(:export_path) { "#{Dir::tmpdir}/project_tree_saver_spec" }
+ let(:project_with_avatar) { create(:empty_project, avatar: fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "image/png")) }
+ let(:project) { create(:empty_project) }
+
+ before do
+ FileUtils.mkdir_p("#{shared.export_path}/avatar/")
+ allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path)
+ end
+
+ after do
+ FileUtils.rm_rf("#{shared.export_path}/avatar")
+ end
+
+ it 'saves a project avatar' do
+ described_class.new(project: project_with_avatar, shared: shared).save
+
+ expect(File).to exist("#{shared.export_path}/avatar/dk.png")
+ end
+
+ it 'is fine not to have an avatar' do
+ expect(described_class.new(project: project, shared: shared).save).to be true
+ end
+end
diff --git a/spec/lib/gitlab/import_export/import_export_spec.rb b/spec/lib/gitlab/import_export/import_export_spec.rb
new file mode 100644
index 00000000000..d6409a29550
--- /dev/null
+++ b/spec/lib/gitlab/import_export/import_export_spec.rb
@@ -0,0 +1,21 @@
+require 'spec_helper'
+
+describe Gitlab::ImportExport, services: true do
+ describe 'export filename' do
+ let(:project) { create(:project, :public, path: 'project-path') }
+
+ it 'contains the project path' do
+ expect(described_class.export_filename(project: project)).to include(project.path)
+ end
+
+ it 'contains the namespace path' do
+ expect(described_class.export_filename(project: project)).to include(project.namespace.path)
+ end
+
+ it 'does not go over a certain length' do
+ project.path = 'a' * 100
+
+ expect(described_class.export_filename(project: project).length).to be < 70
+ end
+ end
+end
diff --git a/spec/lib/gitlab/import_export/members_mapper_spec.rb b/spec/lib/gitlab/import_export/members_mapper_spec.rb
index 6d5aa0d04a2..770e8b0c2f4 100644
--- a/spec/lib/gitlab/import_export/members_mapper_spec.rb
+++ b/spec/lib/gitlab/import_export/members_mapper_spec.rb
@@ -26,6 +26,20 @@ describe Gitlab::ImportExport::MembersMapper, services: true do
"email" => user2.email,
"username" => user2.username
}
+ },
+ {
+ "id" => 3,
+ "access_level" => 40,
+ "source_id" => 14,
+ "source_type" => "Project",
+ "user_id" => nil,
+ "notification_level" => 3,
+ "created_at" => "2016-03-11T10:21:44.822Z",
+ "updated_at" => "2016-03-11T10:21:44.822Z",
+ "created_by_id" => 1,
+ "invite_email" => 'invite@test.com',
+ "invite_token" => 'token',
+ "invite_accepted_at" => nil
}]
end
@@ -47,5 +61,11 @@ describe Gitlab::ImportExport::MembersMapper, services: true do
expect(members_mapper.missing_author_ids.first).to eq(-1)
end
+
+ it 'has invited members with no user' do
+ members_mapper.map
+
+ expect(ProjectMember.find_by_invite_email('invite@test.com')).not_to be_nil
+ end
end
end
diff --git a/spec/lib/gitlab/import_export/project.json b/spec/lib/gitlab/import_export/project.json
index 0b30e8c9b04..cbbf98dca94 100644
--- a/spec/lib/gitlab/import_export/project.json
+++ b/spec/lib/gitlab/import_export/project.json
@@ -18,7 +18,6 @@
"position": 0,
"branch_name": null,
"description": "Aliquam enim illo et possimus.",
- "milestone_id": 18,
"state": "opened",
"iid": 10,
"updated_by_id": null,
@@ -26,6 +25,53 @@
"deleted_at": null,
"due_date": null,
"moved_to_id": null,
+ "test_ee_field": "test",
+ "milestone": {
+ "id": 1,
+ "title": "v0.0",
+ "project_id": 8,
+ "description": "test milestone",
+ "due_date": null,
+ "created_at": "2016-06-14T15:02:04.415Z",
+ "updated_at": "2016-06-14T15:02:04.415Z",
+ "state": "active",
+ "iid": 1,
+ "events": [
+ {
+ "id": 487,
+ "target_type": "Milestone",
+ "target_id": 1,
+ "title": null,
+ "data": null,
+ "project_id": 46,
+ "created_at": "2016-06-14T15:02:04.418Z",
+ "updated_at": "2016-06-14T15:02:04.418Z",
+ "action": 1,
+ "author_id": 18
+ }
+ ]
+ },
+ "label_links": [
+ {
+ "id": 2,
+ "label_id": 2,
+ "target_id": 3,
+ "target_type": "Issue",
+ "created_at": "2016-07-22T08:57:02.840Z",
+ "updated_at": "2016-07-22T08:57:02.840Z",
+ "label": {
+ "id": 2,
+ "title": "test2",
+ "color": "#428bca",
+ "project_id": 8,
+ "created_at": "2016-07-22T08:55:44.161Z",
+ "updated_at": "2016-07-22T08:55:44.161Z",
+ "template": false,
+ "description": "",
+ "priority": null
+ }
+ }
+ ],
"notes": [
{
"id": 351,
@@ -232,7 +278,6 @@
"position": 0,
"branch_name": null,
"description": "Voluptate vel reprehenderit facilis omnis voluptas magnam tenetur.",
- "milestone_id": 16,
"state": "opened",
"iid": 9,
"updated_by_id": null,
@@ -446,7 +491,6 @@
"position": 0,
"branch_name": null,
"description": "Ea recusandae neque autem tempora.",
- "milestone_id": 16,
"state": "closed",
"iid": 8,
"updated_by_id": null,
@@ -660,7 +704,6 @@
"position": 0,
"branch_name": null,
"description": "Maiores architecto quos in dolorem.",
- "milestone_id": 17,
"state": "opened",
"iid": 7,
"updated_by_id": null,
@@ -874,7 +917,6 @@
"position": 0,
"branch_name": null,
"description": "Ut aut ut et tenetur velit aut id modi.",
- "milestone_id": 16,
"state": "opened",
"iid": 6,
"updated_by_id": null,
@@ -1088,7 +1130,6 @@
"position": 0,
"branch_name": null,
"description": "Dicta nisi nihil non ipsa velit.",
- "milestone_id": 20,
"state": "closed",
"iid": 5,
"updated_by_id": null,
@@ -1302,7 +1343,6 @@
"position": 0,
"branch_name": null,
"description": "Ut et explicabo vel voluptatem consequuntur ut sed.",
- "milestone_id": 19,
"state": "closed",
"iid": 4,
"updated_by_id": null,
@@ -1516,7 +1556,6 @@
"position": 0,
"branch_name": null,
"description": "Non asperiores velit accusantium voluptate.",
- "milestone_id": 18,
"state": "closed",
"iid": 3,
"updated_by_id": null,
@@ -1730,7 +1769,6 @@
"position": 0,
"branch_name": null,
"description": "Molestiae corporis magnam et fugit aliquid nulla quia.",
- "milestone_id": 17,
"state": "closed",
"iid": 2,
"updated_by_id": null,
@@ -1944,7 +1982,6 @@
"position": 0,
"branch_name": null,
"description": "Quod ad architecto qui est sed quia.",
- "milestone_id": 20,
"state": "closed",
"iid": 1,
"updated_by_id": null,
@@ -2258,117 +2295,6 @@
"author_id": 25
}
]
- },
- {
- "id": 18,
- "title": "v2.0",
- "project_id": 5,
- "description": "Error dolorem rerum aut nulla.",
- "due_date": null,
- "created_at": "2016-06-14T15:02:04.576Z",
- "updated_at": "2016-06-14T15:02:04.576Z",
- "state": "active",
- "iid": 3,
- "events": [
- {
- "id": 242,
- "target_type": "Milestone",
- "target_id": 18,
- "title": null,
- "data": null,
- "project_id": 36,
- "created_at": "2016-06-14T15:02:04.579Z",
- "updated_at": "2016-06-14T15:02:04.579Z",
- "action": 1,
- "author_id": 1
- },
- {
- "id": 58,
- "target_type": "Milestone",
- "target_id": 18,
- "title": null,
- "data": null,
- "project_id": 5,
- "created_at": "2016-06-14T15:02:04.579Z",
- "updated_at": "2016-06-14T15:02:04.579Z",
- "action": 1,
- "author_id": 22
- }
- ]
- },
- {
- "id": 17,
- "title": "v1.0",
- "project_id": 5,
- "description": "Molestiae perspiciatis voluptates doloremque commodi veniam consequatur.",
- "due_date": null,
- "created_at": "2016-06-14T15:02:04.569Z",
- "updated_at": "2016-06-14T15:02:04.569Z",
- "state": "active",
- "iid": 2,
- "events": [
- {
- "id": 243,
- "target_type": "Milestone",
- "target_id": 17,
- "title": null,
- "data": null,
- "project_id": 36,
- "created_at": "2016-06-14T15:02:04.570Z",
- "updated_at": "2016-06-14T15:02:04.570Z",
- "action": 1,
- "author_id": 1
- },
- {
- "id": 57,
- "target_type": "Milestone",
- "target_id": 17,
- "title": null,
- "data": null,
- "project_id": 5,
- "created_at": "2016-06-14T15:02:04.570Z",
- "updated_at": "2016-06-14T15:02:04.570Z",
- "action": 1,
- "author_id": 20
- }
- ]
- },
- {
- "id": 16,
- "title": "v0.0",
- "project_id": 5,
- "description": "Velit numquam et sed sit.",
- "due_date": null,
- "created_at": "2016-06-14T15:02:04.561Z",
- "updated_at": "2016-06-14T15:02:04.561Z",
- "state": "closed",
- "iid": 1,
- "events": [
- {
- "id": 244,
- "target_type": "Milestone",
- "target_id": 16,
- "title": null,
- "data": null,
- "project_id": 36,
- "created_at": "2016-06-14T15:02:04.563Z",
- "updated_at": "2016-06-14T15:02:04.563Z",
- "action": 1,
- "author_id": 26
- },
- {
- "id": 56,
- "target_type": "Milestone",
- "target_id": 16,
- "title": null,
- "data": null,
- "project_id": 5,
- "created_at": "2016-06-14T15:02:04.563Z",
- "updated_at": "2016-06-14T15:02:04.563Z",
- "action": 1,
- "author_id": 26
- }
- ]
}
],
"snippets": [
@@ -2467,10 +2393,9 @@
"source_project_id": 5,
"author_id": 1,
"assignee_id": null,
- "title": "Cannot be automatically merged",
+ "title": "MR1",
"created_at": "2016-06-14T15:02:36.568Z",
"updated_at": "2016-06-14T15:02:56.815Z",
- "milestone_id": null,
"state": "opened",
"merge_status": "unchecked",
"target_project_id": 5,
@@ -2764,7 +2689,7 @@
"committer_email": "dmitriy.zaporozhets@gmail.com"
}
],
- "st_diffs": [
+ "utf8_st_diffs": [
{
"diff": "Binary files a/.DS_Store and /dev/null differ\n",
"new_path": ".DS_Store",
@@ -2902,13 +2827,12 @@
"id": 26,
"target_branch": "master",
"source_branch": "feature",
- "source_project_id": 5,
+ "source_project_id": 4,
"author_id": 1,
"assignee_id": null,
- "title": "Can be automatically merged",
+ "title": "MR2",
"created_at": "2016-06-14T15:02:36.418Z",
"updated_at": "2016-06-14T15:02:57.013Z",
- "milestone_id": null,
"state": "opened",
"merge_status": "unchecked",
"target_project_id": 5,
@@ -3137,7 +3061,7 @@
"committer_email": "dmitriy.zaporozhets@gmail.com"
}
],
- "st_diffs": [
+ "utf8_st_diffs": [
{
"diff": "--- /dev/null\n+++ b/files/ruby/feature.rb\n@@ -0,0 +1,5 @@\n+class Feature\n+ def foo\n+ puts 'bar'\n+ end\n+end\n",
"new_path": "files/ruby/feature.rb",
@@ -3193,7 +3117,6 @@
"title": "Qui accusantium et inventore facilis doloribus occaecati officiis.",
"created_at": "2016-06-14T15:02:25.168Z",
"updated_at": "2016-06-14T15:02:59.521Z",
- "milestone_id": 17,
"state": "opened",
"merge_status": "unchecked",
"target_project_id": 5,
@@ -3422,7 +3345,7 @@
"committer_email": "james@jameslopez.es"
}
],
- "st_diffs": [
+ "utf8_st_diffs": [
{
"diff": "--- /dev/null\n+++ b/test\n",
"new_path": "test",
@@ -3478,7 +3401,6 @@
"title": "In voluptas aut sequi voluptatem ullam vel corporis illum consequatur.",
"created_at": "2016-06-14T15:02:24.760Z",
"updated_at": "2016-06-14T15:02:59.749Z",
- "milestone_id": 20,
"state": "opened",
"merge_status": "unchecked",
"target_project_id": 5,
@@ -3959,7 +3881,7 @@
"committer_email": "dmitriy.zaporozhets@gmail.com"
}
],
- "st_diffs": [
+ "utf8_st_diffs": [
{
"diff": "Binary files a/.DS_Store and /dev/null differ\n",
"new_path": ".DS_Store",
@@ -4169,7 +4091,6 @@
"title": "Voluptates consequatur eius nemo amet libero animi illum delectus tempore.",
"created_at": "2016-06-14T15:02:24.415Z",
"updated_at": "2016-06-14T15:02:59.958Z",
- "milestone_id": 17,
"state": "opened",
"merge_status": "unchecked",
"target_project_id": 5,
@@ -4208,7 +4129,18 @@
"name": "User 4"
},
"events": [
-
+ {
+ "id": 529,
+ "target_type": "Note",
+ "target_id": 2521,
+ "title": "test levels",
+ "data": null,
+ "project_id": 4,
+ "created_at": "2016-07-07T14:35:12.128Z",
+ "updated_at": "2016-07-07T14:35:12.128Z",
+ "action": 6,
+ "author_id": 1
+ }
]
},
{
@@ -4585,7 +4517,7 @@
"committer_email": "marmis85@gmail.com"
}
],
- "st_diffs": [
+ "utf8_st_diffs": [
{
"diff": "--- a/CHANGELOG\n+++ b/CHANGELOG\n@@ -1,4 +1,6 @@\n-v 6.7.0\n+v6.8.0\n+\n+v6.7.0\n - Add support for Gemnasium as a Project Service (Olivier Gonzalez)\n - Add edit file button to MergeRequest diff\n - Public groups (Jason Hollingsworth)\n",
"new_path": "CHANGELOG",
@@ -4707,7 +4639,6 @@
"title": "In a rerum harum nihil accusamus aut quia nobis non.",
"created_at": "2016-06-14T15:02:24.000Z",
"updated_at": "2016-06-14T15:03:00.225Z",
- "milestone_id": 19,
"state": "opened",
"merge_status": "unchecked",
"target_project_id": 5,
@@ -5096,7 +5027,7 @@
"committer_email": "stanhu@packetzoom.com"
}
],
- "st_diffs": [
+ "utf8_st_diffs": [
{
"diff": "--- a/CHANGELOG\n+++ b/CHANGELOG\n@@ -1,4 +1,6 @@\n-v 6.7.0\n+v6.8.0\n+\n+v6.7.0\n - Add support for Gemnasium as a Project Service (Olivier Gonzalez)\n - Add edit file button to MergeRequest diff\n - Public groups (Jason Hollingsworth)\n",
"new_path": "CHANGELOG",
@@ -5207,7 +5138,6 @@
"title": "Corporis provident similique perspiciatis dolores eos animi.",
"created_at": "2016-06-14T15:02:23.767Z",
"updated_at": "2016-06-14T15:03:00.475Z",
- "milestone_id": 18,
"state": "opened",
"merge_status": "unchecked",
"target_project_id": 5,
@@ -5422,7 +5352,7 @@
"id": 11,
"state": "empty",
"st_commits": null,
- "st_diffs": [
+ "utf8_st_diffs": [
],
"merge_request_id": 11,
@@ -5468,7 +5398,6 @@
"title": "Eligendi reprehenderit doloribus quia et sit id.",
"created_at": "2016-06-14T15:02:23.014Z",
"updated_at": "2016-06-14T15:03:00.685Z",
- "milestone_id": 20,
"state": "opened",
"merge_status": "unchecked",
"target_project_id": 5,
@@ -5949,7 +5878,7 @@
"committer_email": "dmitriy.zaporozhets@gmail.com"
}
],
- "st_diffs": [
+ "utf8_st_diffs": [
{
"diff": "Binary files a/.DS_Store and /dev/null differ\n",
"new_path": ".DS_Store",
@@ -6159,7 +6088,6 @@
"title": "Et ipsam voluptas velit sequi illum ut.",
"created_at": "2016-06-14T15:02:22.825Z",
"updated_at": "2016-06-14T15:03:00.904Z",
- "milestone_id": 16,
"state": "opened",
"merge_status": "unchecked",
"target_project_id": 5,
@@ -6388,7 +6316,7 @@
"committer_email": "james@jameslopez.es"
}
],
- "st_diffs": [
+ "utf8_st_diffs": [
{
"diff": "--- /dev/null\n+++ b/test\n",
"new_path": "test",
diff --git a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
index a72aaa44e82..4d857945fde 100644
--- a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
@@ -24,11 +24,75 @@ describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do
expect(Ci::Pipeline.first.notes).not_to be_empty
end
- it 'restores the correct event' do
+ it 'restores the correct event with symbolised data' do
restored_project_json
expect(Event.where.not(data: nil).first.data[:ref]).not_to be_empty
end
+
+ it 'preserves updated_at on issues' do
+ restored_project_json
+
+ issue = Issue.where(description: 'Aliquam enim illo et possimus.').first
+
+ expect(issue.reload.updated_at.to_s).to eq('2016-06-14 15:02:47 UTC')
+ end
+
+ context 'event at forth level of the tree' do
+ let(:event) { Event.where(title: 'test levels').first }
+
+ before do
+ restored_project_json
+ end
+
+ it 'restores the event' do
+ expect(event).not_to be_nil
+ end
+
+ it 'event belongs to note, belongs to merge request, belongs to a project' do
+ expect(event.note.noteable.project).not_to be_nil
+ end
+ end
+
+ it 'has the correct data for merge request st_diffs' do
+ # makes sure we are renaming the custom method +utf8_st_diffs+ into +st_diffs+
+
+ expect { restored_project_json }.to change(MergeRequestDiff.where.not(st_diffs: nil), :count).by(9)
+ end
+
+ it 'has labels associated to label links, associated to issues' do
+ restored_project_json
+
+ expect(Label.first.label_links.first.target).not_to be_nil
+ end
+
+ it 'has milestones associated to issues' do
+ restored_project_json
+
+ expect(Milestone.find_by_description('test milestone').issues).not_to be_empty
+ end
+
+ context 'Merge requests' do
+ before do
+ restored_project_json
+ end
+
+ it 'always has the new project as a target' do
+ expect(MergeRequest.find_by_title('MR1').target_project).to eq(project)
+ end
+
+ it 'has the same source project as originally if source/target are the same' do
+ expect(MergeRequest.find_by_title('MR1').source_project).to eq(project)
+ end
+
+ it 'has the new project as target if source/target differ' do
+ expect(MergeRequest.find_by_title('MR2').target_project).to eq(project)
+ end
+
+ it 'has no source if source/target differ' do
+ expect(MergeRequest.find_by_title('MR2').source_project_id).to eq(-1)
+ end
+ end
end
end
end
diff --git a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb
index 1424de9e60b..3a86a4ce07c 100644
--- a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb
+++ b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb
@@ -31,10 +31,6 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do
expect(saved_project_json).to include({ "visibility_level" => 20 })
end
- it 'has events' do
- expect(saved_project_json['milestones'].first['events']).not_to be_empty
- end
-
it 'has milestones' do
expect(saved_project_json['milestones']).not_to be_empty
end
@@ -43,8 +39,12 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do
expect(saved_project_json['merge_requests']).not_to be_empty
end
- it 'has labels' do
- expect(saved_project_json['labels']).not_to be_empty
+ it 'has merge request\'s milestones' do
+ expect(saved_project_json['merge_requests'].first['milestone']).not_to be_empty
+ end
+
+ it 'has events' do
+ expect(saved_project_json['merge_requests'].first['milestone']['events']).not_to be_empty
end
it 'has snippets' do
@@ -102,25 +102,38 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do
it 'has ci pipeline notes' do
expect(saved_project_json['pipelines'].first['notes']).not_to be_empty
end
+
+ it 'has labels with no associations' do
+ expect(saved_project_json['labels']).not_to be_empty
+ end
+
+ it 'has labels associated to records' do
+ expect(saved_project_json['issues'].first['label_links'].first['label']).not_to be_empty
+ end
+
+ it 'does not complain about non UTF-8 characters in MR diffs' do
+ ActiveRecord::Base.connection.execute("UPDATE merge_request_diffs SET st_diffs = '---\n- :diff: !binary |-\n LS0tIC9kZXYvbnVsbAorKysgYi9pbWFnZXMvbnVjb3IucGRmCkBAIC0wLDAg\n KzEsMTY3OSBAQAorJVBERi0xLjUNJeLjz9MNCisxIDAgb2JqDTw8L01ldGFk\n YXR'")
+
+ expect(project_tree_saver.save).to be true
+ end
end
end
def setup_project
issue = create(:issue, assignee: user)
- merge_request = create(:merge_request)
- label = create(:label)
snippet = create(:project_snippet)
release = create(:release)
project = create(:project,
:public,
issues: [issue],
- merge_requests: [merge_request],
- labels: [label],
snippets: [snippet],
releases: [release]
)
-
+ label = create(:label, project: project)
+ create(:label_link, label: label, target: issue)
+ milestone = create(:milestone, project: project)
+ merge_request = create(:merge_request, source_project: project, milestone: milestone)
commit_status = create(:commit_status, project: project)
ci_pipeline = create(:ci_pipeline,
@@ -130,7 +143,7 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do
statuses: [commit_status])
create(:ci_build, pipeline: ci_pipeline, project: project)
- milestone = create(:milestone, project: project)
+ create(:milestone, project: project)
create(:note, noteable: issue, project: project)
create(:note, noteable: merge_request, project: project)
create(:note, noteable: snippet, project: project)
diff --git a/spec/lib/gitlab/import_export/reader_spec.rb b/spec/lib/gitlab/import_export/reader_spec.rb
index b76e14deca1..b6dec41d218 100644
--- a/spec/lib/gitlab/import_export/reader_spec.rb
+++ b/spec/lib/gitlab/import_export/reader_spec.rb
@@ -12,7 +12,8 @@ describe Gitlab::ImportExport::Reader, lib: true do
except: [:iid],
include: [:merge_request_diff, :merge_request_test]
} },
- { commit_statuses: { include: :commit } }]
+ { commit_statuses: { include: :commit } },
+ { project_members: { include: { user: { only: [:email] } } } }]
}
end
diff --git a/spec/lib/gitlab/import_export/version_checker_spec.rb b/spec/lib/gitlab/import_export/version_checker_spec.rb
new file mode 100644
index 00000000000..90c6d1c67f6
--- /dev/null
+++ b/spec/lib/gitlab/import_export/version_checker_spec.rb
@@ -0,0 +1,30 @@
+require 'spec_helper'
+
+describe Gitlab::ImportExport::VersionChecker, services: true do
+ describe 'bundle a project Git repo' do
+ let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: '') }
+ let(:version) { Gitlab::ImportExport.version }
+
+ before do
+ allow(File).to receive(:open).and_return(version)
+ end
+
+ it 'returns true if Import/Export have the same version' do
+ expect(described_class.check!(shared: shared)).to be true
+ end
+
+ context 'newer version' do
+ let(:version) { '900.0'}
+
+ it 'returns false if export version is newer' do
+ expect(described_class.check!(shared: shared)).to be false
+ end
+
+ it 'shows the correct error message' do
+ described_class.check!(shared: shared)
+
+ expect(shared.errors.first).to eq("Import version mismatch: Required <= #{Gitlab::ImportExport.version} but was #{version}")
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/incoming_email_spec.rb b/spec/lib/gitlab/incoming_email_spec.rb
index afb3e26f8fb..1dcf2c0668b 100644
--- a/spec/lib/gitlab/incoming_email_spec.rb
+++ b/spec/lib/gitlab/incoming_email_spec.rb
@@ -43,9 +43,9 @@ describe Gitlab::IncomingEmail, lib: true do
end
end
- context 'self.key_from_fallback_reply_message_id' do
+ context 'self.key_from_fallback_message_id' do
it 'returns reply key' do
- expect(described_class.key_from_fallback_reply_message_id('reply-key@localhost')).to eq('key')
+ expect(described_class.key_from_fallback_message_id('reply-key@localhost')).to eq('key')
end
end
end
diff --git a/spec/lib/gitlab/ldap/access_spec.rb b/spec/lib/gitlab/ldap/access_spec.rb
index f5b66b8156f..534bcbf39fe 100644
--- a/spec/lib/gitlab/ldap/access_spec.rb
+++ b/spec/lib/gitlab/ldap/access_spec.rb
@@ -4,7 +4,7 @@ describe Gitlab::LDAP::Access, lib: true do
let(:access) { Gitlab::LDAP::Access.new user }
let(:user) { create(:omniauth_user) }
- describe :allowed? do
+ describe '#allowed?' do
subject { access.allowed? }
context 'when the user cannot be found' do
@@ -64,7 +64,7 @@ describe Gitlab::LDAP::Access, lib: true do
user.ldap_block
end
- it 'should unblock user in GitLab' do
+ it 'unblocks user in GitLab' do
access.allowed?
expect(user).not_to be_blocked
end
diff --git a/spec/lib/gitlab/ldap/user_spec.rb b/spec/lib/gitlab/ldap/user_spec.rb
index 03199a2523e..89790c9e1af 100644
--- a/spec/lib/gitlab/ldap/user_spec.rb
+++ b/spec/lib/gitlab/ldap/user_spec.rb
@@ -25,7 +25,7 @@ describe Gitlab::LDAP::User, lib: true do
OmniAuth::AuthHash.new(uid: 'my-uid', provider: 'ldapmain', info: info_upper_case)
end
- describe :changed? do
+ describe '#changed?' do
it "marks existing ldap user as changed" do
create(:omniauth_user, extern_uid: 'my-uid', provider: 'ldapmain')
expect(ldap_user.changed?).to be_truthy
@@ -36,7 +36,7 @@ describe Gitlab::LDAP::User, lib: true do
expect(ldap_user.changed?).to be_truthy
end
- it "dont marks existing ldap user as changed" do
+ it "does not mark existing ldap user as changed" do
create(:omniauth_user, email: 'john@example.com', extern_uid: 'my-uid', provider: 'ldapmain', ldap_email: true)
expect(ldap_user.changed?).to be_falsey
end
diff --git a/spec/lib/gitlab/lfs/lfs_router_spec.rb b/spec/lib/gitlab/lfs/lfs_router_spec.rb
deleted file mode 100644
index 659facd6c19..00000000000
--- a/spec/lib/gitlab/lfs/lfs_router_spec.rb
+++ /dev/null
@@ -1,730 +0,0 @@
-require 'spec_helper'
-
-describe Gitlab::Lfs::Router, lib: true do
- let(:project) { create(:project) }
- let(:public_project) { create(:project, :public) }
- let(:forked_project) { fork_project(public_project, user) }
-
- let(:user) { create(:user) }
- let(:user_two) { create(:user) }
- let!(:lfs_object) { create(:lfs_object, :with_file) }
-
- let(:request) { Rack::Request.new(env) }
- let(:env) do
- {
- 'rack.input' => '',
- 'REQUEST_METHOD' => 'GET',
- }
- end
-
- let(:lfs_router_auth) { new_lfs_router(project, user: user) }
- let(:lfs_router_ci_auth) { new_lfs_router(project, ci: true) }
- let(:lfs_router_noauth) { new_lfs_router(project) }
- let(:lfs_router_public_auth) { new_lfs_router(public_project, user: user) }
- let(:lfs_router_public_ci_auth) { new_lfs_router(public_project, ci: true) }
- let(:lfs_router_public_noauth) { new_lfs_router(public_project) }
- let(:lfs_router_forked_noauth) { new_lfs_router(forked_project) }
- let(:lfs_router_forked_auth) { new_lfs_router(forked_project, user: user_two) }
- let(:lfs_router_forked_ci_auth) { new_lfs_router(forked_project, ci: true) }
-
- let(:sample_oid) { "b68143e6463773b1b6c6fd009a76c32aeec041faff32ba2ed42fd7f708a17f80" }
- let(:sample_size) { 499013 }
- let(:respond_with_deprecated) {[ 501, { "Content-Type" => "application/json; charset=utf-8" }, ["{\"message\":\"Server supports batch API only, please update your Git LFS client to version 1.0.1 and up.\",\"documentation_url\":\"#{Gitlab.config.gitlab.url}/help\"}"]]}
- let(:respond_with_disabled) {[ 501, { "Content-Type" => "application/json; charset=utf-8" }, ["{\"message\":\"Git LFS is not enabled on this GitLab server, contact your admin.\",\"documentation_url\":\"#{Gitlab.config.gitlab.url}/help\"}"]]}
-
- describe 'when lfs is disabled' do
- before do
- allow(Gitlab.config.lfs).to receive(:enabled).and_return(false)
- env['REQUEST_METHOD'] = 'POST'
- body = {
- 'objects' => [
- { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897',
- 'size' => 1575078
- },
- { 'oid' => sample_oid,
- 'size' => sample_size
- }
- ],
- 'operation' => 'upload'
- }.to_json
- env['rack.input'] = StringIO.new(body)
- env["PATH_INFO"] = "#{project.repository.path_with_namespace}.git/info/lfs/objects/batch"
- end
-
- it 'responds with 501' do
- expect(lfs_router_auth.try_call).to match_array(respond_with_disabled)
- end
- end
-
- describe 'when fetching lfs object using deprecated API' do
- before do
- enable_lfs
- env["PATH_INFO"] = "#{project.repository.path_with_namespace}.git/info/lfs/objects/#{sample_oid}"
- end
-
- it 'responds with 501' do
- expect(lfs_router_auth.try_call).to match_array(respond_with_deprecated)
- end
- end
-
- describe 'when fetching lfs object' do
- before do
- enable_lfs
- env['HTTP_ACCEPT'] = "application/vnd.git-lfs+json; charset=utf-8"
- env["PATH_INFO"] = "#{project.repository.path_with_namespace}.git/gitlab-lfs/objects/#{sample_oid}"
- end
-
- describe 'and request comes from gitlab-workhorse' do
- context 'without user being authorized' do
- it "responds with status 401" do
- expect(lfs_router_noauth.try_call.first).to eq(401)
- end
- end
-
- context 'with required headers' do
- before do
- project.lfs_objects << lfs_object
- env['HTTP_X_SENDFILE_TYPE'] = "X-Sendfile"
- end
-
- context 'when user does not have project access' do
- it "responds with status 403" do
- expect(lfs_router_auth.try_call.first).to eq(403)
- end
- end
-
- context 'when user has project access' do
- before do
- project.team << [user, :master]
- end
-
- it "responds with status 200" do
- expect(lfs_router_auth.try_call.first).to eq(200)
- end
-
- it "responds with the file location" do
- expect(lfs_router_auth.try_call[1]['Content-Type']).to eq("application/octet-stream")
- expect(lfs_router_auth.try_call[1]['X-Sendfile']).to eq(lfs_object.file.path)
- end
- end
-
- context 'when CI is authorized' do
- it "responds with status 200" do
- expect(lfs_router_ci_auth.try_call.first).to eq(200)
- end
-
- it "responds with the file location" do
- expect(lfs_router_ci_auth.try_call[1]['Content-Type']).to eq("application/octet-stream")
- expect(lfs_router_ci_auth.try_call[1]['X-Sendfile']).to eq(lfs_object.file.path)
- end
- end
- end
-
- context 'without required headers' do
- it "responds with status 403" do
- expect(lfs_router_auth.try_call.first).to eq(403)
- end
- end
- end
- end
-
- describe 'when handling lfs request using deprecated API' do
- before do
- enable_lfs
- env['REQUEST_METHOD'] = 'POST'
- env["PATH_INFO"] = "#{project.repository.path_with_namespace}.git/info/lfs/objects"
- end
-
- it 'responds with 501' do
- expect(lfs_router_auth.try_call).to match_array(respond_with_deprecated)
- end
- end
-
- describe 'when handling lfs batch request' do
- before do
- enable_lfs
- env['REQUEST_METHOD'] = 'POST'
- env['PATH_INFO'] = "#{project.repository.path_with_namespace}.git/info/lfs/objects/batch"
- end
-
- describe 'download' do
- before do
- body = { 'operation' => 'download',
- 'objects' => [
- { 'oid' => sample_oid,
- 'size' => sample_size
- }]
- }.to_json
- env['rack.input'] = StringIO.new(body)
- end
-
- shared_examples 'an authorized requests' do
- context 'when downloading an lfs object that is assigned to our project' do
- before do
- project.lfs_objects << lfs_object
- end
-
- it 'responds with status 200 and href to download' do
- response = router.try_call
- expect(response.first).to eq(200)
- response_body = ActiveSupport::JSON.decode(response.last.first)
-
- expect(response_body).to eq('objects' => [
- { 'oid' => sample_oid,
- 'size' => sample_size,
- 'actions' => {
- 'download' => {
- 'href' => "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}",
- 'header' => { 'Authorization' => auth }
- }
- }
- }])
- end
- end
-
- context 'when downloading an lfs object that is assigned to other project' do
- before do
- public_project.lfs_objects << lfs_object
- end
-
- it 'responds with status 200 and error message' do
- response = router.try_call
- expect(response.first).to eq(200)
- response_body = ActiveSupport::JSON.decode(response.last.first)
-
- expect(response_body).to eq('objects' => [
- { 'oid' => sample_oid,
- 'size' => sample_size,
- 'error' => {
- 'code' => 404,
- 'message' => "Object does not exist on the server or you don't have permissions to access it",
- }
- }])
- end
- end
-
- context 'when downloading a lfs object that does not exist' do
- before do
- body = { 'operation' => 'download',
- 'objects' => [
- { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897',
- 'size' => 1575078
- }]
- }.to_json
- env['rack.input'] = StringIO.new(body)
- end
-
- it "responds with status 200 and error message" do
- response = router.try_call
- expect(response.first).to eq(200)
- response_body = ActiveSupport::JSON.decode(response.last.first)
-
- expect(response_body).to eq('objects' => [
- { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897',
- 'size' => 1575078,
- 'error' => {
- 'code' => 404,
- 'message' => "Object does not exist on the server or you don't have permissions to access it",
- }
- }])
- end
- end
-
- context 'when downloading one new and one existing lfs object' do
- before do
- body = { 'operation' => 'download',
- 'objects' => [
- { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897',
- 'size' => 1575078
- },
- { 'oid' => sample_oid,
- 'size' => sample_size
- }
- ]
- }.to_json
- env['rack.input'] = StringIO.new(body)
- project.lfs_objects << lfs_object
- end
-
- it "responds with status 200 with upload hypermedia link for the new object" do
- response = router.try_call
- expect(response.first).to eq(200)
- response_body = ActiveSupport::JSON.decode(response.last.first)
-
- expect(response_body).to eq('objects' => [
- { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897',
- 'size' => 1575078,
- 'error' => {
- 'code' => 404,
- 'message' => "Object does not exist on the server or you don't have permissions to access it",
- }
- },
- { 'oid' => sample_oid,
- 'size' => sample_size,
- 'actions' => {
- 'download' => {
- 'href' => "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}",
- 'header' => { 'Authorization' => auth }
- }
- }
- }])
- end
- end
- end
-
- context 'when user is authenticated' do
- let(:auth) { authorize(user) }
-
- before do
- env["HTTP_AUTHORIZATION"] = auth
- project.team << [user, role]
- end
-
- it_behaves_like 'an authorized requests' do
- let(:role) { :reporter }
- let(:router) { lfs_router_auth }
- end
-
- context 'when user does is not member of the project' do
- let(:role) { :guest }
-
- it 'responds with 403' do
- expect(lfs_router_auth.try_call.first).to eq(403)
- end
- end
-
- context 'when user does not have download access' do
- let(:role) { :guest }
-
- it 'responds with 403' do
- expect(lfs_router_auth.try_call.first).to eq(403)
- end
- end
- end
-
- context 'when CI is authorized' do
- let(:auth) { 'gitlab-ci-token:password' }
-
- before do
- env["HTTP_AUTHORIZATION"] = auth
- end
-
- it_behaves_like 'an authorized requests' do
- let(:router) { lfs_router_ci_auth }
- end
- end
-
- context 'when user is not authenticated' do
- describe 'is accessing public project' do
- before do
- public_project.lfs_objects << lfs_object
- end
-
- it 'responds with status 200 and href to download' do
- response = lfs_router_public_noauth.try_call
- expect(response.first).to eq(200)
- response_body = ActiveSupport::JSON.decode(response.last.first)
-
- expect(response_body).to eq('objects' => [
- { 'oid' => sample_oid,
- 'size' => sample_size,
- 'actions' => {
- 'download' => {
- 'href' => "#{public_project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}",
- 'header' => {}
- }
- }
- }])
- end
- end
-
- describe 'is accessing non-public project' do
- before do
- project.lfs_objects << lfs_object
- end
-
- it 'responds with authorization required' do
- expect(lfs_router_noauth.try_call.first).to eq(401)
- end
- end
- end
- end
-
- describe 'upload' do
- before do
- body = { 'operation' => 'upload',
- 'objects' => [
- { 'oid' => sample_oid,
- 'size' => sample_size
- }]
- }.to_json
- env['rack.input'] = StringIO.new(body)
- end
-
- describe 'when request is authenticated' do
- describe 'when user has project push access' do
- before do
- @auth = authorize(user)
- env["HTTP_AUTHORIZATION"] = @auth
- project.team << [user, :developer]
- end
-
- context 'when pushing an lfs object that already exists' do
- before do
- public_project.lfs_objects << lfs_object
- end
-
- it "responds with status 200 and links the object to the project" do
- response_body = lfs_router_auth.try_call.last
- response = ActiveSupport::JSON.decode(response_body.first)
-
- 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)).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)
- end
- end
-
- context 'when pushing a lfs object that does not exist' do
- before do
- body = { 'operation' => 'upload',
- 'objects' => [
- { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897',
- 'size' => 1575078
- }]
- }.to_json
- env['rack.input'] = StringIO.new(body)
- end
-
- it "responds with status 200 and upload hypermedia link" do
- response = lfs_router_auth.try_call
- expect(response.first).to eq(200)
-
- response_body = ActiveSupport::JSON.decode(response.last.first)
- expect(response_body['objects']).to be_kind_of(Array)
- expect(response_body['objects'].first['oid']).to eq("91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897")
- expect(response_body['objects'].first['size']).to eq(1575078)
- expect(lfs_object.projects.pluck(:id)).not_to include(project.id)
- expect(response_body['objects'].first['actions']['upload']['href']).to eq("#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}.git/gitlab-lfs/objects/91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897/1575078")
- expect(response_body['objects'].first['actions']['upload']['header']).to eq('Authorization' => @auth)
- end
- end
-
- context 'when pushing one new and one existing lfs object' do
- before do
- body = { 'operation' => 'upload',
- 'objects' => [
- { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897',
- 'size' => 1575078
- },
- { 'oid' => sample_oid,
- 'size' => sample_size
- }
- ]
- }.to_json
- env['rack.input'] = StringIO.new(body)
- project.lfs_objects << lfs_object
- end
-
- it "responds with status 200 with upload hypermedia link for the new object" do
- response = lfs_router_auth.try_call
- expect(response.first).to eq(200)
-
- response_body = ActiveSupport::JSON.decode(response.last.first)
- expect(response_body['objects']).to be_kind_of(Array)
-
- expect(response_body['objects'].first['oid']).to eq("91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897")
- expect(response_body['objects'].first['size']).to eq(1575078)
- expect(response_body['objects'].first['actions']['upload']['href']).to eq("#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}.git/gitlab-lfs/objects/91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897/1575078")
- expect(response_body['objects'].first['actions']['upload']['header']).to eq("Authorization" => @auth)
-
- 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).not_to have_key('actions')
- end
- end
- end
-
- context 'when user does not have push access' do
- it 'responds with 403' do
- expect(lfs_router_auth.try_call.first).to eq(403)
- end
- end
-
- context 'when CI is authorized' do
- it 'responds with 401' do
- expect(lfs_router_ci_auth.try_call.first).to eq(401)
- end
- end
- end
-
- context 'when user is not authenticated' do
- context 'when user has push access' do
- before do
- project.team << [user, :master]
- end
-
- it "responds with status 401" do
- expect(lfs_router_public_noauth.try_call.first).to eq(401)
- end
- end
-
- context 'when user does not have push access' do
- it "responds with status 401" do
- expect(lfs_router_public_noauth.try_call.first).to eq(401)
- end
- end
- end
-
- context 'when CI is authorized' do
- let(:auth) { 'gitlab-ci-token:password' }
-
- before do
- env["HTTP_AUTHORIZATION"] = auth
- end
-
- it "responds with status 403" do
- expect(lfs_router_public_ci_auth.try_call.first).to eq(401)
- end
- end
- end
-
- describe 'unsupported' do
- before do
- body = { 'operation' => 'other',
- 'objects' => [
- { 'oid' => sample_oid,
- 'size' => sample_size
- }]
- }.to_json
- env['rack.input'] = StringIO.new(body)
- end
-
- it 'responds with status 404' do
- expect(lfs_router_public_noauth.try_call.first).to eq(404)
- end
- end
- end
-
- describe 'when pushing a lfs object' do
- before do
- enable_lfs
- env['REQUEST_METHOD'] = 'PUT'
- end
-
- shared_examples 'unauthorized' do
- context 'and request is sent by gitlab-workhorse to authorize the request' do
- before do
- header_for_upload_authorize(router.project)
- end
-
- it 'responds with status 401' do
- expect(router.try_call.first).to eq(401)
- end
- end
-
- context 'and request is sent by gitlab-workhorse to finalize the upload' do
- before do
- headers_for_upload_finalize(router.project)
- end
-
- it 'responds with status 401' do
- expect(router.try_call.first).to eq(401)
- end
- end
-
- context 'and request is sent with a malformed headers' do
- before do
- env["PATH_INFO"] = "#{router.project.repository.path_with_namespace}.git/gitlab-lfs/objects/#{sample_oid}/#{sample_size}"
- env["HTTP_X_GITLAB_LFS_TMP"] = "cat /etc/passwd"
- end
-
- it 'does not recognize it as a valid lfs command' do
- expect(router.try_call).to eq(nil)
- end
- end
- end
-
- shared_examples 'forbidden' do
- context 'and request is sent by gitlab-workhorse to authorize the request' do
- before do
- header_for_upload_authorize(router.project)
- end
-
- it 'responds with 403' do
- expect(router.try_call.first).to eq(403)
- end
- end
-
- context 'and request is sent by gitlab-workhorse to finalize the upload' do
- before do
- headers_for_upload_finalize(router.project)
- end
-
- it 'responds with 403' do
- expect(router.try_call.first).to eq(403)
- end
- end
- end
-
- describe 'to one project' do
- describe 'when user is authenticated' do
- describe 'when user has push access to the project' do
- before do
- project.team << [user, :developer]
- end
-
- context 'and request is sent by gitlab-workhorse to authorize the request' do
- before do
- header_for_upload_authorize(project)
- end
-
- it 'responds with status 200, location of lfs store and object details' do
- json_response = ActiveSupport::JSON.decode(lfs_router_auth.try_call.last.first)
-
- expect(lfs_router_auth.try_call.first).to eq(200)
- expect(json_response['StoreLFSPath']).to eq("#{Gitlab.config.shared.path}/lfs-objects/tmp/upload")
- expect(json_response['LfsOid']).to eq(sample_oid)
- expect(json_response['LfsSize']).to eq(sample_size)
- end
- end
-
- context 'and request is sent by gitlab-workhorse to finalize the upload' do
- before do
- headers_for_upload_finalize(project)
- end
-
- it 'responds with status 200 and lfs object is linked to the project' do
- expect(lfs_router_auth.try_call.first).to eq(200)
- expect(lfs_object.projects.pluck(:id)).to include(project.id)
- end
- end
- end
-
- describe 'and user does not have push access' do
- let(:router) { lfs_router_auth }
-
- it_behaves_like 'forbidden'
- end
- end
-
- context 'when CI is authenticated' do
- let(:router) { lfs_router_ci_auth }
-
- it_behaves_like 'unauthorized'
- end
-
- context 'for unauthenticated' do
- let(:router) { new_lfs_router(project) }
-
- it_behaves_like 'unauthorized'
- end
- end
-
- describe 'to a forked project' do
- let(:forked_project) { fork_project(public_project, user) }
-
- describe 'when user is authenticated' do
- describe 'when user has push access to the project' do
- before do
- forked_project.team << [user_two, :developer]
- end
-
- context 'and request is sent by gitlab-workhorse to authorize the request' do
- before do
- header_for_upload_authorize(forked_project)
- end
-
- it 'responds with status 200, location of lfs store and object details' do
- json_response = ActiveSupport::JSON.decode(lfs_router_forked_auth.try_call.last.first)
-
- expect(lfs_router_forked_auth.try_call.first).to eq(200)
- expect(json_response['StoreLFSPath']).to eq("#{Gitlab.config.shared.path}/lfs-objects/tmp/upload")
- expect(json_response['LfsOid']).to eq(sample_oid)
- expect(json_response['LfsSize']).to eq(sample_size)
- end
- end
-
- context 'and request is sent by gitlab-workhorse to finalize the upload' do
- before do
- headers_for_upload_finalize(forked_project)
- end
-
- it 'responds with status 200 and lfs object is linked to the source project' do
- expect(lfs_router_forked_auth.try_call.first).to eq(200)
- expect(lfs_object.projects.pluck(:id)).to include(public_project.id)
- end
- end
- end
-
- describe 'and user does not have push access' do
- let(:router) { lfs_router_forked_auth }
-
- it_behaves_like 'forbidden'
- end
- end
-
- context 'when CI is authenticated' do
- let(:router) { lfs_router_forked_ci_auth }
-
- it_behaves_like 'unauthorized'
- end
-
- context 'for unauthenticated' do
- let(:router) { lfs_router_forked_noauth }
-
- it_behaves_like 'unauthorized'
- end
-
- describe 'and second project not related to fork or a source project' do
- let(:second_project) { create(:project) }
- let(:lfs_router_second_project) { new_lfs_router(second_project, user: user) }
-
- before do
- public_project.lfs_objects << lfs_object
- headers_for_upload_finalize(second_project)
- end
-
- context 'when pushing the same lfs object to the second project' do
- before do
- second_project.team << [user, :master]
- end
-
- it 'responds with 200 and links the lfs object to the project' do
- expect(lfs_router_second_project.try_call.first).to eq(200)
- expect(lfs_object.projects.pluck(:id)).to include(second_project.id, public_project.id)
- end
- end
- end
- end
- end
-
- def enable_lfs
- allow(Gitlab.config.lfs).to receive(:enabled).and_return(true)
- end
-
- def authorize(user)
- ActionController::HttpAuthentication::Basic.encode_credentials(user.username, user.password)
- end
-
- def new_lfs_router(project, user: nil, ci: false)
- Gitlab::Lfs::Router.new(project, user, ci, request)
- end
-
- def header_for_upload_authorize(project)
- env["PATH_INFO"] = "#{project.repository.path_with_namespace}.git/gitlab-lfs/objects/#{sample_oid}/#{sample_size}/authorize"
- end
-
- def headers_for_upload_finalize(project)
- env["PATH_INFO"] = "#{project.repository.path_with_namespace}.git/gitlab-lfs/objects/#{sample_oid}/#{sample_size}"
- env["HTTP_X_GITLAB_LFS_TMP"] = "#{sample_oid}6e561c9d4"
- end
-
- def fork_project(project, user, object = nil)
- allow(RepositoryForkWorker).to receive(:perform_async).and_return(true)
- Projects::ForkService.new(project, user, {}).execute
- end
-end
diff --git a/spec/lib/gitlab/metrics/instrumentation_spec.rb b/spec/lib/gitlab/metrics/instrumentation_spec.rb
index 8809b7e3f12..d88bcae41fb 100644
--- a/spec/lib/gitlab/metrics/instrumentation_spec.rb
+++ b/spec/lib/gitlab/metrics/instrumentation_spec.rb
@@ -39,6 +39,12 @@ describe Gitlab::Metrics::Instrumentation do
allow(@dummy).to receive(:name).and_return('Dummy')
end
+ describe '.series' do
+ it 'returns a String' do
+ expect(described_class.series).to be_an_instance_of(String)
+ end
+ end
+
describe '.configure' do
it 'yields self' do
described_class.configure do |c|
@@ -78,8 +84,7 @@ describe Gitlab::Metrics::Instrumentation do
allow(described_class).to receive(:transaction).
and_return(transaction)
- expect(transaction).to receive(:measure_method).
- with('Dummy.foo')
+ expect_any_instance_of(Gitlab::Metrics::MethodCall).to receive(:measure)
@dummy.foo
end
@@ -157,8 +162,7 @@ describe Gitlab::Metrics::Instrumentation do
allow(described_class).to receive(:transaction).
and_return(transaction)
- expect(transaction).to receive(:measure_method).
- with('Dummy#bar')
+ expect_any_instance_of(Gitlab::Metrics::MethodCall).to receive(:measure)
@dummy.new.bar
end
diff --git a/spec/lib/gitlab/metrics/metric_spec.rb b/spec/lib/gitlab/metrics/metric_spec.rb
index f718d536130..f26fca52c50 100644
--- a/spec/lib/gitlab/metrics/metric_spec.rb
+++ b/spec/lib/gitlab/metrics/metric_spec.rb
@@ -23,6 +23,24 @@ describe Gitlab::Metrics::Metric do
it { is_expected.to eq({ host: 'localtoast' }) }
end
+ describe '#type' do
+ subject { metric.type }
+
+ it { is_expected.to eq(:metric) }
+ end
+
+ describe '#event?' do
+ it 'returns false for a regular metric' do
+ expect(metric.event?).to eq(false)
+ end
+
+ it 'returns true for an event metric' do
+ expect(metric).to receive(:type).and_return(:event)
+
+ expect(metric.event?).to eq(true)
+ end
+ end
+
describe '#to_hash' do
it 'returns a Hash' do
expect(metric.to_hash).to be_an_instance_of(Hash)
diff --git a/spec/lib/gitlab/metrics/rack_middleware_spec.rb b/spec/lib/gitlab/metrics/rack_middleware_spec.rb
index f264ed64029..bcaffd27909 100644
--- a/spec/lib/gitlab/metrics/rack_middleware_spec.rb
+++ b/spec/lib/gitlab/metrics/rack_middleware_spec.rb
@@ -19,7 +19,7 @@ describe Gitlab::Metrics::RackMiddleware do
end
it 'tags a transaction with the name and action of a controller' do
- klass = double(:klass, name: 'TestController')
+ klass = double(:klass, name: 'TestController', content_type: 'text/html')
controller = double(:controller, class: klass, action_name: 'show')
env['action_controller.instance'] = controller
@@ -32,7 +32,7 @@ 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
+ it 'tags a transaction with the method and path 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)
@@ -45,6 +45,15 @@ describe Gitlab::Metrics::RackMiddleware do
middleware.call(env)
end
+
+ it 'tracks any raised exceptions' do
+ expect(app).to receive(:call).with(env).and_raise(RuntimeError)
+
+ expect_any_instance_of(Gitlab::Metrics::Transaction).
+ to receive(:add_event).with(:rails_exception)
+
+ expect { middleware.call(env) }.to raise_error(RuntimeError)
+ end
end
describe '#transaction_from_env' do
@@ -78,17 +87,30 @@ describe Gitlab::Metrics::RackMiddleware do
describe '#tag_controller' do
let(:transaction) { middleware.transaction_from_env(env) }
+ let(:content_type) { 'text/html' }
- it 'tags a transaction with the name and action of a controller' do
+ before do
klass = double(:klass, name: 'TestController')
- controller = double(:controller, class: klass, action_name: 'show')
+ controller = double(:controller, class: klass, action_name: 'show', content_type: content_type)
env['action_controller.instance'] = controller
+ end
+ it 'tags a transaction with the name and action of a controller' do
middleware.tag_controller(transaction, env)
expect(transaction.action).to eq('TestController#show')
end
+
+ context 'when the response content type is not :html' do
+ let(:content_type) { 'application/json' }
+
+ it 'appends the mime type to the transaction action' do
+ middleware.tag_controller(transaction, env)
+
+ expect(transaction.action).to eq('TestController#show.json')
+ end
+ end
end
describe '#tag_endpoint' do
diff --git a/spec/lib/gitlab/metrics/sidekiq_middleware_spec.rb b/spec/lib/gitlab/metrics/sidekiq_middleware_spec.rb
index 4d2aa03e722..acaba785606 100644
--- a/spec/lib/gitlab/metrics/sidekiq_middleware_spec.rb
+++ b/spec/lib/gitlab/metrics/sidekiq_middleware_spec.rb
@@ -12,7 +12,9 @@ describe Gitlab::Metrics::SidekiqMiddleware do
with('TestWorker#perform').
and_call_original
- expect_any_instance_of(Gitlab::Metrics::Transaction).to receive(:set).with(:sidekiq_queue_duration, instance_of(Float))
+ expect_any_instance_of(Gitlab::Metrics::Transaction).to receive(:set).
+ with(:sidekiq_queue_duration, instance_of(Float))
+
expect_any_instance_of(Gitlab::Metrics::Transaction).to receive(:finish)
middleware.call(worker, message, :test) { nil }
@@ -25,10 +27,28 @@ describe Gitlab::Metrics::SidekiqMiddleware do
with('TestWorker#perform').
and_call_original
- expect_any_instance_of(Gitlab::Metrics::Transaction).to receive(:set).with(:sidekiq_queue_duration, instance_of(Float))
+ expect_any_instance_of(Gitlab::Metrics::Transaction).to receive(:set).
+ with(:sidekiq_queue_duration, instance_of(Float))
+
expect_any_instance_of(Gitlab::Metrics::Transaction).to receive(:finish)
middleware.call(worker, {}, :test) { nil }
end
+
+ it 'tracks any raised exceptions' do
+ worker = double(:worker, class: double(:class, name: 'TestWorker'))
+
+ expect_any_instance_of(Gitlab::Metrics::Transaction).
+ to receive(:run).and_raise(RuntimeError)
+
+ expect_any_instance_of(Gitlab::Metrics::Transaction).
+ to receive(:add_event).with(:sidekiq_exception)
+
+ expect_any_instance_of(Gitlab::Metrics::Transaction).
+ to receive(:finish)
+
+ expect { middleware.call(worker, message, :test) }.
+ to raise_error(RuntimeError)
+ end
end
end
diff --git a/spec/lib/gitlab/metrics/system_spec.rb b/spec/lib/gitlab/metrics/system_spec.rb
index cf0e282c2fb..9e2ea89a712 100644
--- a/spec/lib/gitlab/metrics/system_spec.rb
+++ b/spec/lib/gitlab/metrics/system_spec.rb
@@ -28,20 +28,20 @@ describe Gitlab::Metrics::System do
end
describe '.cpu_time' do
- it 'returns a Float' do
- expect(described_class.cpu_time).to be_an_instance_of(Float)
+ it 'returns a Fixnum' do
+ expect(described_class.cpu_time).to be_an_instance_of(Fixnum)
end
end
describe '.real_time' do
- it 'returns a Float' do
- expect(described_class.real_time).to be_an_instance_of(Float)
+ it 'returns a Fixnum' do
+ expect(described_class.real_time).to be_an_instance_of(Fixnum)
end
end
describe '.monotonic_time' do
- it 'returns a Float' do
- expect(described_class.monotonic_time).to be_an_instance_of(Float)
+ it 'returns a Fixnum' do
+ expect(described_class.monotonic_time).to be_an_instance_of(Fixnum)
end
end
end
diff --git a/spec/lib/gitlab/metrics/transaction_spec.rb b/spec/lib/gitlab/metrics/transaction_spec.rb
index 3b1c67a2147..3887c04c832 100644
--- a/spec/lib/gitlab/metrics/transaction_spec.rb
+++ b/spec/lib/gitlab/metrics/transaction_spec.rb
@@ -46,19 +46,11 @@ describe Gitlab::Metrics::Transaction do
end
end
- describe '#measure_method' do
- it 'adds a new method if it does not exist already' do
- transaction.measure_method('Foo#bar') { 'foo' }
+ describe '#method_call_for' do
+ it 'returns a MethodCall' do
+ method = transaction.method_call_for('Foo#bar')
- expect(transaction.methods['Foo#bar']).
- to be_an_instance_of(Gitlab::Metrics::MethodCall)
- end
-
- it 'adds timings to an existing method call' do
- transaction.measure_method('Foo#bar') { 'foo' }
- transaction.measure_method('Foo#bar') { 'foo' }
-
- expect(transaction.methods['Foo#bar'].call_count).to eq(2)
+ expect(method).to be_an_instance_of(Gitlab::Metrics::MethodCall)
end
end
@@ -150,5 +142,62 @@ describe Gitlab::Metrics::Transaction do
transaction.submit
end
+
+ it 'does not add an action tag for events' do
+ transaction.action = 'Foo#bar'
+ transaction.add_event(:meow)
+
+ hash = {
+ series: 'events',
+ tags: { event: :meow },
+ values: { count: 1 },
+ timestamp: an_instance_of(Fixnum)
+ }
+
+ expect(Gitlab::Metrics).to receive(:submit_metrics).
+ with([hash])
+
+ transaction.submit
+ end
+ end
+
+ describe '#add_event' do
+ it 'adds a metric' do
+ transaction.add_event(:meow)
+
+ expect(transaction.metrics[0]).to be_an_instance_of(Gitlab::Metrics::Metric)
+ end
+
+ it "does not prefix the metric's series name" do
+ transaction.add_event(:meow)
+
+ metric = transaction.metrics[0]
+
+ expect(metric.series).to eq(described_class::EVENT_SERIES)
+ end
+
+ it 'tracks a counter for every event' do
+ transaction.add_event(:meow)
+
+ metric = transaction.metrics[0]
+
+ expect(metric.values).to eq(count: 1)
+ end
+
+ it 'tracks the event name' do
+ transaction.add_event(:meow)
+
+ metric = transaction.metrics[0]
+
+ expect(metric.tags).to eq(event: :meow)
+ end
+
+ it 'allows tracking of custom tags' do
+ transaction.add_event(:meow, animal: 'cat')
+
+ metric = transaction.metrics[0]
+
+ expect(metric.tags).to eq(event: :meow, animal: 'cat')
+ end
end
end
diff --git a/spec/lib/gitlab/metrics_spec.rb b/spec/lib/gitlab/metrics_spec.rb
index 96f7eabbca6..ab6e311b1e8 100644
--- a/spec/lib/gitlab/metrics_spec.rb
+++ b/spec/lib/gitlab/metrics_spec.rb
@@ -147,4 +147,34 @@ describe Gitlab::Metrics do
end
end
end
+
+ describe '#series_prefix' do
+ it 'returns a String' do
+ expect(described_class.series_prefix).to be_an_instance_of(String)
+ end
+ end
+
+ describe '.add_event' do
+ context 'without a transaction' do
+ it 'does nothing' do
+ expect_any_instance_of(Gitlab::Metrics::Transaction).
+ not_to receive(:add_event)
+
+ Gitlab::Metrics.add_event(:meow)
+ end
+ end
+
+ context 'with a transaction' do
+ it 'adds an event' do
+ transaction = Gitlab::Metrics::Transaction.new
+
+ expect(transaction).to receive(:add_event).with(:meow)
+
+ expect(Gitlab::Metrics).to receive(:current_transaction).
+ and_return(transaction)
+
+ Gitlab::Metrics.add_event(:meow)
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/middleware/rails_queue_duration_spec.rb b/spec/lib/gitlab/middleware/rails_queue_duration_spec.rb
index fd6f684db0c..168090d5b5c 100644
--- a/spec/lib/gitlab/middleware/rails_queue_duration_spec.rb
+++ b/spec/lib/gitlab/middleware/rails_queue_duration_spec.rb
@@ -22,7 +22,7 @@ describe Gitlab::Middleware::RailsQueueDuration do
end
it 'sets proxy_flight_time and calls the app when the header is present' do
- env['HTTP_GITLAB_WORHORSE_PROXY_START'] = '123'
+ env['HTTP_GITLAB_WORKHORSE_PROXY_START'] = '123'
expect(transaction).to receive(:set).with(:rails_queue_duration, an_instance_of(Float))
expect(middleware.call(env)).to eq('yay')
end
diff --git a/spec/lib/gitlab/o_auth/user_spec.rb b/spec/lib/gitlab/o_auth/user_spec.rb
index 1fca8a13037..78c669e8fa5 100644
--- a/spec/lib/gitlab/o_auth/user_spec.rb
+++ b/spec/lib/gitlab/o_auth/user_spec.rb
@@ -42,7 +42,7 @@ describe Gitlab::OAuth::User, lib: true do
describe 'signup' do
shared_examples 'to verify compliance with allow_single_sign_on' do
context 'provider is marked as external' do
- it 'should mark user as external' do
+ it 'marks user as external' do
stub_omniauth_config(allow_single_sign_on: ['twitter'], external_providers: ['twitter'])
oauth_user.save
expect(gl_user).to be_valid
@@ -51,7 +51,7 @@ describe Gitlab::OAuth::User, lib: true do
end
context 'provider was external, now has been removed' do
- it 'should not mark external user as internal' do
+ it 'does not mark external user as internal' do
create(:omniauth_user, extern_uid: 'my-uid', provider: 'twitter', external: true)
stub_omniauth_config(allow_single_sign_on: ['twitter'], external_providers: ['facebook'])
oauth_user.save
@@ -62,7 +62,7 @@ describe Gitlab::OAuth::User, lib: true do
context 'provider is not external' do
context 'when adding a new OAuth identity' do
- it 'should not promote an external user to internal' do
+ it 'does not promote an external user to internal' do
user = create(:user, email: 'john@mail.com', external: true)
user.identities.create(provider: provider, extern_uid: uid)
diff --git a/spec/lib/gitlab/project_search_results_spec.rb b/spec/lib/gitlab/project_search_results_spec.rb
index 270b89972d7..29abb4d4d07 100644
--- a/spec/lib/gitlab/project_search_results_spec.rb
+++ b/spec/lib/gitlab/project_search_results_spec.rb
@@ -33,7 +33,7 @@ describe Gitlab::ProjectSearchResults, lib: true do
let!(:security_issue_1) { create(:issue, :confidential, project: project, title: 'Security issue 1', author: author) }
let!(:security_issue_2) { create(:issue, :confidential, title: 'Security issue 2', project: project, assignee: assignee) }
- it 'should not list project confidential issues for non project members' do
+ it 'does not list project confidential issues for non project members' do
results = described_class.new(non_member, project, query)
issues = results.objects('issues')
@@ -43,7 +43,7 @@ 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
+ it 'does not list project confidential issues for project members with guest role' do
project.team << [member, :guest]
results = described_class.new(member, project, query)
@@ -55,7 +55,7 @@ describe Gitlab::ProjectSearchResults, lib: true do
expect(results.issues_count).to eq 1
end
- it 'should list project confidential issues for author' do
+ it 'lists project confidential issues for author' do
results = described_class.new(author, project, query)
issues = results.objects('issues')
@@ -65,7 +65,7 @@ describe Gitlab::ProjectSearchResults, lib: true do
expect(results.issues_count).to eq 2
end
- it 'should list project confidential issues for assignee' do
+ it 'lists project confidential issues for assignee' do
results = described_class.new(assignee, project.id, query)
issues = results.objects('issues')
@@ -75,7 +75,7 @@ describe Gitlab::ProjectSearchResults, lib: true do
expect(results.issues_count).to eq 2
end
- it 'should list project confidential issues for project members' do
+ it 'lists project confidential issues for project members' do
project.team << [member, :developer]
results = described_class.new(member, project, query)
@@ -87,7 +87,7 @@ describe Gitlab::ProjectSearchResults, lib: true do
expect(results.issues_count).to eq 3
end
- it 'should list all project issues for admin' do
+ it 'lists all project issues for admin' do
results = described_class.new(admin, project, query)
issues = results.objects('issues')
diff --git a/spec/lib/gitlab/redis_spec.rb b/spec/lib/gitlab/redis_spec.rb
new file mode 100644
index 00000000000..e54f5ffb312
--- /dev/null
+++ b/spec/lib/gitlab/redis_spec.rb
@@ -0,0 +1,79 @@
+require 'spec_helper'
+
+describe Gitlab::Redis do
+ let(:redis_config) { Rails.root.join('config', 'resque.yml').to_s }
+
+ before(:each) { described_class.reset_params! }
+ after(:each) { described_class.reset_params! }
+
+ describe '.params' do
+ subject { described_class.params }
+
+ context 'when url contains unix socket reference' do
+ let(:config_old) { Rails.root.join('spec/fixtures/config/redis_old_format_socket.yml').to_s }
+ let(:config_new) { Rails.root.join('spec/fixtures/config/redis_new_format_socket.yml').to_s }
+
+ context 'with old format' do
+ it 'returns path key instead' do
+ expect_any_instance_of(described_class).to receive(:config_file) { config_old }
+
+ is_expected.to include(path: '/path/to/old/redis.sock')
+ is_expected.not_to have_key(:url)
+ end
+ end
+
+ context 'with new format' do
+ it 'returns path key instead' do
+ expect_any_instance_of(described_class).to receive(:config_file) { config_new }
+
+ is_expected.to include(path: '/path/to/redis.sock')
+ is_expected.not_to have_key(:url)
+ end
+ end
+ end
+
+ context 'when url is host based' do
+ let(:config_old) { Rails.root.join('spec/fixtures/config/redis_old_format_host.yml') }
+ let(:config_new) { Rails.root.join('spec/fixtures/config/redis_new_format_host.yml') }
+
+ context 'with old format' do
+ it 'returns hash with host, port, db, and password' do
+ expect_any_instance_of(described_class).to receive(:config_file) { config_old }
+
+ is_expected.to include(host: 'localhost', password: 'mypassword', port: 6379, db: 99)
+ is_expected.not_to have_key(:url)
+ end
+ end
+
+ context 'with new format' do
+ it 'returns hash with host, port, db, and password' do
+ expect_any_instance_of(described_class).to receive(:config_file) { config_new }
+
+ is_expected.to include(host: 'localhost', password: 'mynewpassword', port: 6379, db: 99)
+ is_expected.not_to have_key(:url)
+ end
+ end
+ end
+ end
+
+ describe '#raw_config_hash' do
+ it 'returns default redis url when no config file is present' do
+ expect(subject).to receive(:fetch_config) { false }
+
+ expect(subject.send(:raw_config_hash)).to eq(url: Gitlab::Redis::DEFAULT_REDIS_URL)
+ end
+
+ it 'returns old-style single url config in a hash' do
+ expect(subject).to receive(:fetch_config) { 'redis://myredis:6379' }
+ expect(subject.send(:raw_config_hash)).to eq(url: 'redis://myredis:6379')
+ end
+ end
+
+ describe '#fetch_config' do
+ it 'returns false when no config file is present' do
+ allow(File).to receive(:exist?).with(redis_config) { false }
+
+ expect(subject.send(:fetch_config)).to be_falsey
+ end
+ end
+end
diff --git a/spec/lib/gitlab/saml/user_spec.rb b/spec/lib/gitlab/saml/user_spec.rb
index 56bf08e7041..02c139f1a0d 100644
--- a/spec/lib/gitlab/saml/user_spec.rb
+++ b/spec/lib/gitlab/saml/user_spec.rb
@@ -67,7 +67,7 @@ describe Gitlab::Saml::User, lib: true do
end
context 'user was external, now should not be' do
- it 'should make user internal' do
+ it 'makes user internal' do
existing_user.update_attribute('external', true)
saml_user.save
expect(gl_user).to be_valid
@@ -94,14 +94,14 @@ describe Gitlab::Saml::User, lib: true do
context 'with allow_single_sign_on default (["saml"])' do
before { stub_omniauth_config(allow_single_sign_on: ['saml']) }
- it 'should not throw an error' do
+ it 'does not throw an error' do
expect{ saml_user.save }.not_to raise_error
end
end
context 'with allow_single_sign_on disabled' do
before { stub_omniauth_config(allow_single_sign_on: false) }
- it 'should throw an error' do
+ it 'throws an error' do
expect{ saml_user.save }.to raise_error StandardError
end
end
@@ -223,7 +223,7 @@ describe Gitlab::Saml::User, lib: true do
context 'dont block on create' do
before { stub_omniauth_config(block_auto_created_users: false) }
- it 'should not block the user' do
+ it 'does not block the user' do
saml_user.save
expect(gl_user).to be_valid
expect(gl_user).not_to be_blocked
@@ -233,7 +233,7 @@ describe Gitlab::Saml::User, lib: true do
context 'block on create' do
before { stub_omniauth_config(block_auto_created_users: true) }
- it 'should block user' do
+ it 'blocks user' do
saml_user.save
expect(gl_user).to be_valid
expect(gl_user).to be_blocked
diff --git a/spec/lib/gitlab/search_results_spec.rb b/spec/lib/gitlab/search_results_spec.rb
index 1bb444bf34f..8a656ab0ee9 100644
--- a/spec/lib/gitlab/search_results_spec.rb
+++ b/spec/lib/gitlab/search_results_spec.rb
@@ -73,7 +73,7 @@ describe Gitlab::SearchResults do
let!(:security_issue_4) { create(:issue, :confidential, project: project_3, title: 'Security issue 4', assignee: assignee) }
let!(:security_issue_5) { create(:issue, :confidential, project: project_4, title: 'Security issue 5') }
- it 'should not list confidential issues for non project members' do
+ it 'does not list confidential issues for non project members' do
results = described_class.new(non_member, limit_projects, query)
issues = results.objects('issues')
@@ -86,7 +86,7 @@ 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
+ it 'does not list confidential issues for project members with guest role' do
project_1.team << [member, :guest]
project_2.team << [member, :guest]
@@ -102,7 +102,7 @@ describe Gitlab::SearchResults do
expect(results.issues_count).to eq 1
end
- it 'should list confidential issues for author' do
+ it 'lists confidential issues for author' do
results = described_class.new(author, limit_projects, query)
issues = results.objects('issues')
@@ -115,7 +115,7 @@ describe Gitlab::SearchResults do
expect(results.issues_count).to eq 3
end
- it 'should list confidential issues for assignee' do
+ it 'lists confidential issues for assignee' do
results = described_class.new(assignee, limit_projects, query)
issues = results.objects('issues')
@@ -128,7 +128,7 @@ describe Gitlab::SearchResults do
expect(results.issues_count).to eq 3
end
- it 'should list confidential issues for project members' do
+ it 'lists confidential issues for project members' do
project_1.team << [member, :developer]
project_2.team << [member, :developer]
@@ -144,7 +144,7 @@ describe Gitlab::SearchResults do
expect(results.issues_count).to eq 4
end
- it 'should list all issues for admin' do
+ it 'lists all issues for admin' do
results = described_class.new(admin, limit_projects, query)
issues = results.objects('issues')
diff --git a/spec/lib/gitlab/slash_commands/command_definition_spec.rb b/spec/lib/gitlab/slash_commands/command_definition_spec.rb
new file mode 100644
index 00000000000..c9c2f314e57
--- /dev/null
+++ b/spec/lib/gitlab/slash_commands/command_definition_spec.rb
@@ -0,0 +1,173 @@
+require 'spec_helper'
+
+describe Gitlab::SlashCommands::CommandDefinition do
+ subject { described_class.new(:command) }
+
+ describe "#all_names" do
+ context "when the command has aliases" do
+ before do
+ subject.aliases = [:alias1, :alias2]
+ end
+
+ it "returns an array with the name and aliases" do
+ expect(subject.all_names).to eq([:command, :alias1, :alias2])
+ end
+ end
+
+ context "when the command doesn't have aliases" do
+ it "returns an array with the name" do
+ expect(subject.all_names).to eq([:command])
+ end
+ end
+ end
+
+ describe "#noop?" do
+ context "when the command has an action block" do
+ before do
+ subject.action_block = proc { }
+ end
+
+ it "returns false" do
+ expect(subject.noop?).to be false
+ end
+ end
+
+ context "when the command doesn't have an action block" do
+ it "returns true" do
+ expect(subject.noop?).to be true
+ end
+ end
+ end
+
+ describe "#available?" do
+ let(:opts) { { go: false } }
+
+ context "when the command has a condition block" do
+ before do
+ subject.condition_block = proc { go }
+ end
+
+ context "when the condition block returns true" do
+ before do
+ opts[:go] = true
+ end
+
+ it "returns true" do
+ expect(subject.available?(opts)).to be true
+ end
+ end
+
+ context "when the condition block returns false" do
+ it "returns false" do
+ expect(subject.available?(opts)).to be false
+ end
+ end
+ end
+
+ context "when the command doesn't have a condition block" do
+ it "returns true" do
+ expect(subject.available?(opts)).to be true
+ end
+ end
+ end
+
+ describe "#execute" do
+ let(:context) { OpenStruct.new(run: false) }
+
+ context "when the command is a noop" do
+ it "doesn't execute the command" do
+ expect(context).not_to receive(:instance_exec)
+
+ subject.execute(context, {}, nil)
+
+ expect(context.run).to be false
+ end
+ end
+
+ context "when the command is not a noop" do
+ before do
+ subject.action_block = proc { self.run = true }
+ end
+
+ context "when the command is not available" do
+ before do
+ subject.condition_block = proc { false }
+ end
+
+ it "doesn't execute the command" do
+ subject.execute(context, {}, nil)
+
+ expect(context.run).to be false
+ end
+ end
+
+ context "when the command is available" do
+ context "when the commnd has no arguments" do
+ before do
+ subject.action_block = proc { self.run = true }
+ end
+
+ context "when the command is provided an argument" do
+ it "executes the command" do
+ subject.execute(context, {}, true)
+
+ expect(context.run).to be true
+ end
+ end
+
+ context "when the command is not provided an argument" do
+ it "executes the command" do
+ subject.execute(context, {}, nil)
+
+ expect(context.run).to be true
+ end
+ end
+ end
+
+ context "when the command has 1 required argument" do
+ before do
+ subject.action_block = ->(arg) { self.run = arg }
+ end
+
+ context "when the command is provided an argument" do
+ it "executes the command" do
+ subject.execute(context, {}, true)
+
+ expect(context.run).to be true
+ end
+ end
+
+ context "when the command is not provided an argument" do
+ it "doesn't execute the command" do
+ subject.execute(context, {}, nil)
+
+ expect(context.run).to be false
+ end
+ end
+ end
+
+ context "when the command has 1 optional argument" do
+ before do
+ subject.action_block = proc { |arg = nil| self.run = arg || true }
+ end
+
+ context "when the command is provided an argument" do
+ it "executes the command" do
+ subject.execute(context, {}, true)
+
+ expect(context.run).to be true
+ end
+ end
+
+ context "when the command is not provided an argument" do
+ it "executes the command" do
+ subject.execute(context, {}, nil)
+
+ expect(context.run).to be true
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/slash_commands/dsl_spec.rb b/spec/lib/gitlab/slash_commands/dsl_spec.rb
new file mode 100644
index 00000000000..26217a0e3b2
--- /dev/null
+++ b/spec/lib/gitlab/slash_commands/dsl_spec.rb
@@ -0,0 +1,77 @@
+require 'spec_helper'
+
+describe Gitlab::SlashCommands::Dsl do
+ before :all do
+ DummyClass = Struct.new(:project) do
+ include Gitlab::SlashCommands::Dsl
+
+ desc 'A command with no args'
+ command :no_args, :none do
+ "Hello World!"
+ end
+
+ params 'The first argument'
+ command :one_arg, :once, :first do |arg1|
+ arg1
+ end
+
+ desc do
+ "A dynamic description for #{noteable.upcase}"
+ end
+ params 'The first argument', 'The second argument'
+ command :two_args do |arg1, arg2|
+ [arg1, arg2]
+ end
+
+ command :cc
+
+ condition do
+ project == 'foo'
+ end
+ command :cond_action do |arg|
+ arg
+ end
+ end
+ end
+
+ describe '.command_definitions' do
+ it 'returns an array with commands definitions' do
+ no_args_def, one_arg_def, two_args_def, cc_def, cond_action_def = DummyClass.command_definitions
+
+ expect(no_args_def.name).to eq(:no_args)
+ expect(no_args_def.aliases).to eq([:none])
+ expect(no_args_def.description).to eq('A command with no args')
+ expect(no_args_def.params).to eq([])
+ expect(no_args_def.condition_block).to be_nil
+ expect(no_args_def.action_block).to be_a_kind_of(Proc)
+
+ expect(one_arg_def.name).to eq(:one_arg)
+ expect(one_arg_def.aliases).to eq([:once, :first])
+ expect(one_arg_def.description).to eq('')
+ expect(one_arg_def.params).to eq(['The first argument'])
+ expect(one_arg_def.condition_block).to be_nil
+ expect(one_arg_def.action_block).to be_a_kind_of(Proc)
+
+ expect(two_args_def.name).to eq(:two_args)
+ expect(two_args_def.aliases).to eq([])
+ expect(two_args_def.to_h(noteable: "issue")[:description]).to eq('A dynamic description for ISSUE')
+ expect(two_args_def.params).to eq(['The first argument', 'The second argument'])
+ expect(two_args_def.condition_block).to be_nil
+ expect(two_args_def.action_block).to be_a_kind_of(Proc)
+
+ expect(cc_def.name).to eq(:cc)
+ expect(cc_def.aliases).to eq([])
+ expect(cc_def.description).to eq('')
+ expect(cc_def.params).to eq([])
+ expect(cc_def.condition_block).to be_nil
+ expect(cc_def.action_block).to be_nil
+
+ expect(cond_action_def.name).to eq(:cond_action)
+ expect(cond_action_def.aliases).to eq([])
+ expect(cond_action_def.description).to eq('')
+ expect(cond_action_def.params).to eq([])
+ expect(cond_action_def.condition_block).to be_a_kind_of(Proc)
+ expect(cond_action_def.action_block).to be_a_kind_of(Proc)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/slash_commands/extractor_spec.rb b/spec/lib/gitlab/slash_commands/extractor_spec.rb
new file mode 100644
index 00000000000..1e4954c4af8
--- /dev/null
+++ b/spec/lib/gitlab/slash_commands/extractor_spec.rb
@@ -0,0 +1,215 @@
+require 'spec_helper'
+
+describe Gitlab::SlashCommands::Extractor do
+ let(:definitions) do
+ Class.new do
+ include Gitlab::SlashCommands::Dsl
+
+ command(:reopen, :open) { }
+ command(:assign) { }
+ command(:labels) { }
+ command(:power) { }
+ end.command_definitions
+ end
+
+ let(:extractor) { described_class.new(definitions) }
+
+ shared_examples 'command with no argument' do
+ it 'extracts command' do
+ msg, commands = extractor.extract_commands(original_msg)
+
+ expect(commands).to eq [['reopen']]
+ expect(msg).to eq final_msg
+ end
+ end
+
+ shared_examples 'command with a single argument' do
+ it 'extracts command' do
+ msg, commands = extractor.extract_commands(original_msg)
+
+ expect(commands).to eq [['assign', '@joe']]
+ expect(msg).to eq final_msg
+ end
+ end
+
+ shared_examples 'command with multiple arguments' do
+ it 'extracts command' do
+ msg, commands = extractor.extract_commands(original_msg)
+
+ expect(commands).to eq [['labels', '~foo ~"bar baz" label']]
+ expect(msg).to eq final_msg
+ end
+ end
+
+ describe '#extract_commands' do
+ describe 'command with no argument' do
+ context 'at the start of content' do
+ it_behaves_like 'command with no argument' do
+ let(:original_msg) { "/reopen\nworld" }
+ let(:final_msg) { "world" }
+ end
+ end
+
+ context 'in the middle of content' do
+ it_behaves_like 'command with no argument' do
+ let(:original_msg) { "hello\n/reopen\nworld" }
+ let(:final_msg) { "hello\nworld" }
+ end
+ end
+
+ context 'in the middle of a line' do
+ it 'does not extract command' do
+ msg = "hello\nworld /reopen"
+ msg, commands = extractor.extract_commands(msg)
+
+ expect(commands).to be_empty
+ expect(msg).to eq "hello\nworld /reopen"
+ end
+ end
+
+ context 'at the end of content' do
+ it_behaves_like 'command with no argument' do
+ let(:original_msg) { "hello\n/reopen" }
+ let(:final_msg) { "hello" }
+ end
+ end
+ end
+
+ describe 'command with a single argument' do
+ context 'at the start of content' do
+ it_behaves_like 'command with a single argument' do
+ let(:original_msg) { "/assign @joe\nworld" }
+ let(:final_msg) { "world" }
+ end
+ end
+
+ context 'in the middle of content' do
+ it_behaves_like 'command with a single argument' do
+ let(:original_msg) { "hello\n/assign @joe\nworld" }
+ let(:final_msg) { "hello\nworld" }
+ end
+ end
+
+ context 'in the middle of a line' do
+ it 'does not extract command' do
+ msg = "hello\nworld /assign @joe"
+ msg, commands = extractor.extract_commands(msg)
+
+ expect(commands).to be_empty
+ expect(msg).to eq "hello\nworld /assign @joe"
+ end
+ end
+
+ context 'at the end of content' do
+ it_behaves_like 'command with a single argument' do
+ let(:original_msg) { "hello\n/assign @joe" }
+ let(:final_msg) { "hello" }
+ end
+ end
+
+ context 'when argument is not separated with a space' do
+ it 'does not extract command' do
+ msg = "hello\n/assign@joe\nworld"
+ msg, commands = extractor.extract_commands(msg)
+
+ expect(commands).to be_empty
+ expect(msg).to eq "hello\n/assign@joe\nworld"
+ end
+ end
+ end
+
+ describe 'command with multiple arguments' do
+ context 'at the start of content' do
+ it_behaves_like 'command with multiple arguments' do
+ let(:original_msg) { %(/labels ~foo ~"bar baz" label\nworld) }
+ let(:final_msg) { "world" }
+ end
+ end
+
+ context 'in the middle of content' do
+ it_behaves_like 'command with multiple arguments' do
+ let(:original_msg) { %(hello\n/labels ~foo ~"bar baz" label\nworld) }
+ let(:final_msg) { "hello\nworld" }
+ end
+ end
+
+ context 'in the middle of a line' do
+ it 'does not extract command' do
+ msg = %(hello\nworld /labels ~foo ~"bar baz" label)
+ msg, commands = extractor.extract_commands(msg)
+
+ expect(commands).to be_empty
+ expect(msg).to eq %(hello\nworld /labels ~foo ~"bar baz" label)
+ end
+ end
+
+ context 'at the end of content' do
+ it_behaves_like 'command with multiple arguments' do
+ let(:original_msg) { %(hello\n/labels ~foo ~"bar baz" label) }
+ let(:final_msg) { "hello" }
+ end
+ end
+
+ context 'when argument is not separated with a space' do
+ it 'does not extract command' do
+ msg = %(hello\n/labels~foo ~"bar baz" label\nworld)
+ msg, commands = extractor.extract_commands(msg)
+
+ expect(commands).to be_empty
+ expect(msg).to eq %(hello\n/labels~foo ~"bar baz" label\nworld)
+ end
+ end
+ end
+
+ it 'extracts command with multiple arguments and various prefixes' do
+ msg = %(hello\n/power @user.name %9.10 ~"bar baz.2"\nworld)
+ msg, commands = extractor.extract_commands(msg)
+
+ expect(commands).to eq [['power', '@user.name %9.10 ~"bar baz.2"']]
+ expect(msg).to eq "hello\nworld"
+ end
+
+ it 'extracts multiple commands' do
+ msg = %(hello\n/power @user.name %9.10 ~"bar baz.2" label\nworld\n/reopen)
+ msg, commands = extractor.extract_commands(msg)
+
+ expect(commands).to eq [['power', '@user.name %9.10 ~"bar baz.2" label'], ['reopen']]
+ expect(msg).to eq "hello\nworld"
+ end
+
+ it 'does not alter original content if no command is found' do
+ msg = 'Fixes #123'
+ msg, commands = extractor.extract_commands(msg)
+
+ expect(commands).to be_empty
+ expect(msg).to eq 'Fixes #123'
+ end
+
+ it 'does not extract commands inside a blockcode' do
+ msg = "Hello\r\n```\r\nThis is some text\r\n/close\r\n/assign @user\r\n```\r\n\r\nWorld"
+ expected = msg.delete("\r")
+ msg, commands = extractor.extract_commands(msg)
+
+ expect(commands).to be_empty
+ expect(msg).to eq expected
+ end
+
+ it 'does not extract commands inside a blockquote' do
+ msg = "Hello\r\n>>>\r\nThis is some text\r\n/close\r\n/assign @user\r\n>>>\r\n\r\nWorld"
+ expected = msg.delete("\r")
+ msg, commands = extractor.extract_commands(msg)
+
+ expect(commands).to be_empty
+ expect(msg).to eq expected
+ end
+
+ it 'does not extract commands inside a HTML tag' do
+ msg = "Hello\r\n<div>\r\nThis is some text\r\n/close\r\n/assign @user\r\n</div>\r\n\r\nWorld"
+ expected = msg.delete("\r")
+ msg, commands = extractor.extract_commands(msg)
+
+ expect(commands).to be_empty
+ expect(msg).to eq expected
+ end
+ end
+end
diff --git a/spec/lib/gitlab/template/gitignore_spec.rb b/spec/lib/gitlab/template/gitignore_template_spec.rb
index bc0ec9325cc..9750a012e22 100644
--- a/spec/lib/gitlab/template/gitignore_spec.rb
+++ b/spec/lib/gitlab/template/gitignore_template_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Gitlab::Template::Gitignore do
+describe Gitlab::Template::GitignoreTemplate do
subject { described_class }
describe '.all' do
@@ -24,7 +24,7 @@ describe Gitlab::Template::Gitignore do
it 'returns the Gitignore object of a valid file' do
ruby = subject.find('Ruby')
- expect(ruby).to be_a Gitlab::Template::Gitignore
+ expect(ruby).to be_a Gitlab::Template::GitignoreTemplate
expect(ruby.name).to eq('Ruby')
end
end
diff --git a/spec/lib/gitlab/template/gitlab_ci_yml_template_spec.rb b/spec/lib/gitlab/template/gitlab_ci_yml_template_spec.rb
new file mode 100644
index 00000000000..e3b8321eda3
--- /dev/null
+++ b/spec/lib/gitlab/template/gitlab_ci_yml_template_spec.rb
@@ -0,0 +1,41 @@
+require 'spec_helper'
+
+describe Gitlab::Template::GitlabCiYmlTemplate do
+ subject { described_class }
+
+ describe '.all' do
+ it 'strips the gitlab-ci suffix' do
+ expect(subject.all.first.name).not_to end_with('.gitlab-ci.yml')
+ end
+
+ it 'combines the globals and rest' do
+ all = subject.all.map(&:name)
+
+ expect(all).to include('Elixir')
+ expect(all).to include('Docker')
+ 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 GitlabCiYml object of a valid file' do
+ ruby = subject.find('Ruby')
+
+ expect(ruby).to be_a Gitlab::Template::GitlabCiYmlTemplate
+ expect(ruby.name).to eq('Ruby')
+ end
+ end
+
+ describe '#content' do
+ it 'loads the full file' do
+ gitignore = subject.new(Rails.root.join('vendor/gitlab-ci-yml/Ruby.gitlab-ci.yml'))
+
+ expect(gitignore.name).to eq 'Ruby'
+ expect(gitignore.content).to start_with('#')
+ end
+ end
+end
diff --git a/spec/lib/gitlab/template/issue_template_spec.rb b/spec/lib/gitlab/template/issue_template_spec.rb
new file mode 100644
index 00000000000..f770857e958
--- /dev/null
+++ b/spec/lib/gitlab/template/issue_template_spec.rb
@@ -0,0 +1,89 @@
+require 'spec_helper'
+
+describe Gitlab::Template::IssueTemplate do
+ subject { described_class }
+
+ let(:user) { create(:user) }
+ let(:project) { create(:project) }
+ let(:file_path_1) { '.gitlab/issue_templates/bug.md' }
+ let(:file_path_2) { '.gitlab/issue_templates/template_test.md' }
+ let(:file_path_3) { '.gitlab/issue_templates/feature_proposal.md' }
+
+ before do
+ project.team.add_user(user, Gitlab::Access::MASTER)
+ project.repository.commit_file(user, file_path_1, "something valid", "test 3", "master", false)
+ project.repository.commit_file(user, file_path_2, "template_test", "test 1", "master", false)
+ project.repository.commit_file(user, file_path_3, "feature_proposal", "test 2", "master", false)
+ end
+
+ describe '.all' do
+ it 'strips the md suffix' do
+ expect(subject.all(project).first.name).not_to end_with('.issue_template')
+ end
+
+ it 'combines the globals and rest' do
+ all = subject.all(project).map(&:name)
+
+ expect(all).to include('bug')
+ expect(all).to include('feature_proposal')
+ expect(all).to include('template_test')
+ end
+ end
+
+ describe '.find' do
+ it 'returns nil if the file does not exist' do
+ expect { subject.find('mepmep-yadida', project) }.to raise_error(Gitlab::Template::Finders::RepoTemplateFinder::FileNotFoundError)
+ end
+
+ it 'returns the issue object of a valid file' do
+ ruby = subject.find('bug', project)
+
+ expect(ruby).to be_a Gitlab::Template::IssueTemplate
+ expect(ruby.name).to eq('bug')
+ end
+ end
+
+ describe '.by_category' do
+ it 'return array of templates' do
+ all = subject.by_category('', project).map(&:name)
+ expect(all).to include('bug')
+ expect(all).to include('feature_proposal')
+ expect(all).to include('template_test')
+ end
+
+ context 'when repo is bare or empty' do
+ let(:empty_project) { create(:empty_project) }
+ before { empty_project.team.add_user(user, Gitlab::Access::MASTER) }
+
+ it "returns empty array" do
+ templates = subject.by_category('', empty_project)
+ expect(templates).to be_empty
+ end
+ end
+ end
+
+ describe '#content' do
+ it 'loads the full file' do
+ issue_template = subject.new('.gitlab/issue_templates/bug.md', project)
+
+ expect(issue_template.name).to eq 'bug'
+ expect(issue_template.content).to eq('something valid')
+ end
+
+ it 'raises error when file is not found' do
+ issue_template = subject.new('.gitlab/issue_templates/bugnot.md', project)
+ expect { issue_template.content }.to raise_error(Gitlab::Template::Finders::RepoTemplateFinder::FileNotFoundError)
+ end
+
+ context "when repo is empty" do
+ let(:empty_project) { create(:empty_project) }
+
+ before { empty_project.team.add_user(user, Gitlab::Access::MASTER) }
+
+ it "raises file not found" do
+ issue_template = subject.new('.gitlab/issue_templates/not_existent.md', empty_project)
+ expect { issue_template.content }.to raise_error(Gitlab::Template::Finders::RepoTemplateFinder::FileNotFoundError)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/template/merge_request_template_spec.rb b/spec/lib/gitlab/template/merge_request_template_spec.rb
new file mode 100644
index 00000000000..bb0f68043fa
--- /dev/null
+++ b/spec/lib/gitlab/template/merge_request_template_spec.rb
@@ -0,0 +1,89 @@
+require 'spec_helper'
+
+describe Gitlab::Template::MergeRequestTemplate do
+ subject { described_class }
+
+ let(:user) { create(:user) }
+ let(:project) { create(:project) }
+ let(:file_path_1) { '.gitlab/merge_request_templates/bug.md' }
+ let(:file_path_2) { '.gitlab/merge_request_templates/template_test.md' }
+ let(:file_path_3) { '.gitlab/merge_request_templates/feature_proposal.md' }
+
+ before do
+ project.team.add_user(user, Gitlab::Access::MASTER)
+ project.repository.commit_file(user, file_path_1, "something valid", "test 3", "master", false)
+ project.repository.commit_file(user, file_path_2, "template_test", "test 1", "master", false)
+ project.repository.commit_file(user, file_path_3, "feature_proposal", "test 2", "master", false)
+ end
+
+ describe '.all' do
+ it 'strips the md suffix' do
+ expect(subject.all(project).first.name).not_to end_with('.issue_template')
+ end
+
+ it 'combines the globals and rest' do
+ all = subject.all(project).map(&:name)
+
+ expect(all).to include('bug')
+ expect(all).to include('feature_proposal')
+ expect(all).to include('template_test')
+ end
+ end
+
+ describe '.find' do
+ it 'returns nil if the file does not exist' do
+ expect { subject.find('mepmep-yadida', project) }.to raise_error(Gitlab::Template::Finders::RepoTemplateFinder::FileNotFoundError)
+ end
+
+ it 'returns the merge request object of a valid file' do
+ ruby = subject.find('bug', project)
+
+ expect(ruby).to be_a Gitlab::Template::MergeRequestTemplate
+ expect(ruby.name).to eq('bug')
+ end
+ end
+
+ describe '.by_category' do
+ it 'return array of templates' do
+ all = subject.by_category('', project).map(&:name)
+ expect(all).to include('bug')
+ expect(all).to include('feature_proposal')
+ expect(all).to include('template_test')
+ end
+
+ context 'when repo is bare or empty' do
+ let(:empty_project) { create(:empty_project) }
+ before { empty_project.team.add_user(user, Gitlab::Access::MASTER) }
+
+ it "returns empty array" do
+ templates = subject.by_category('', empty_project)
+ expect(templates).to be_empty
+ end
+ end
+ end
+
+ describe '#content' do
+ it 'loads the full file' do
+ issue_template = subject.new('.gitlab/merge_request_templates/bug.md', project)
+
+ expect(issue_template.name).to eq 'bug'
+ expect(issue_template.content).to eq('something valid')
+ end
+
+ it 'raises error when file is not found' do
+ issue_template = subject.new('.gitlab/merge_request_templates/bugnot.md', project)
+ expect { issue_template.content }.to raise_error(Gitlab::Template::Finders::RepoTemplateFinder::FileNotFoundError)
+ end
+
+ context "when repo is empty" do
+ let(:empty_project) { create(:empty_project) }
+
+ before { empty_project.team.add_user(user, Gitlab::Access::MASTER) }
+
+ it "raises file not found" do
+ issue_template = subject.new('.gitlab/merge_request_templates/not_existent.md', empty_project)
+ expect { issue_template.content }.to raise_error(Gitlab::Template::Finders::RepoTemplateFinder::FileNotFoundError)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/upgrader_spec.rb b/spec/lib/gitlab/upgrader_spec.rb
index e958e087a80..edadab043d7 100644
--- a/spec/lib/gitlab/upgrader_spec.rb
+++ b/spec/lib/gitlab/upgrader_spec.rb
@@ -9,19 +9,19 @@ describe Gitlab::Upgrader, lib: true do
end
describe 'latest_version?' do
- it 'should be true if newest version' do
+ it 'is true if newest version' do
allow(upgrader).to receive(:latest_version_raw).and_return(current_version)
expect(upgrader.latest_version?).to be_truthy
end
end
describe 'latest_version_raw' do
- it 'should be latest version for GitLab 5' do
+ it 'is the latest version for GitLab 5' do
allow(upgrader).to receive(:current_version_raw).and_return("5.3.0")
expect(upgrader.latest_version_raw).to eq("v5.4.2")
end
- it 'should get the latest version from tags' do
+ it 'gets the latest version from tags' do
allow(upgrader).to receive(:fetch_git_tags).and_return([
'6f0733310546402c15d3ae6128a95052f6c8ea96 refs/tags/v7.1.1',
'facfec4b242ce151af224e20715d58e628aa5e74 refs/tags/v7.1.1^{}',
diff --git a/spec/lib/gitlab/user_access_spec.rb b/spec/lib/gitlab/user_access_spec.rb
new file mode 100644
index 00000000000..d3c3b800b94
--- /dev/null
+++ b/spec/lib/gitlab/user_access_spec.rb
@@ -0,0 +1,138 @@
+require 'spec_helper'
+
+describe Gitlab::UserAccess, lib: true do
+ let(:access) { Gitlab::UserAccess.new(user, project: project) }
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+
+ describe 'can_push_to_branch?' do
+ describe 'push to none protected branch' do
+ it 'returns true if user is a master' do
+ project.team << [user, :master]
+
+ expect(access.can_push_to_branch?('random_branch')).to be_truthy
+ end
+
+ it 'returns true if user is a developer' do
+ project.team << [user, :developer]
+
+ expect(access.can_push_to_branch?('random_branch')).to be_truthy
+ end
+
+ it 'returns false if user is a reporter' do
+ project.team << [user, :reporter]
+
+ expect(access.can_push_to_branch?('random_branch')).to be_falsey
+ end
+ end
+
+ describe 'push to empty project' do
+ let(:empty_project) { create(:project_empty_repo) }
+ let(:project_access) { Gitlab::UserAccess.new(user, project: empty_project) }
+
+ it 'returns true if user is master' do
+ empty_project.team << [user, :master]
+
+ expect(project_access.can_push_to_branch?('master')).to be_truthy
+ end
+
+ it 'returns false if user is developer and project is fully protected' do
+ empty_project.team << [user, :developer]
+ stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_FULL)
+
+ expect(project_access.can_push_to_branch?('master')).to be_falsey
+ end
+
+ it 'returns false if user is developer and it is not allowed to push new commits but can merge into branch' do
+ empty_project.team << [user, :developer]
+ stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_DEV_CAN_MERGE)
+
+ expect(project_access.can_push_to_branch?('master')).to be_falsey
+ end
+
+ it 'returns true if user is developer and project is unprotected' do
+ empty_project.team << [user, :developer]
+ stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_NONE)
+
+ expect(project_access.can_push_to_branch?('master')).to be_truthy
+ end
+
+ it 'returns true if user is developer and project grants developers permission' do
+ empty_project.team << [user, :developer]
+ stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_DEV_CAN_PUSH)
+
+ expect(project_access.can_push_to_branch?('master')).to be_truthy
+ end
+ end
+
+ describe 'push to protected branch' do
+ let(:branch) { create :protected_branch, project: project }
+
+ it 'returns true if user is a master' do
+ project.team << [user, :master]
+
+ expect(access.can_push_to_branch?(branch.name)).to be_truthy
+ end
+
+ it 'returns false if user is a developer' do
+ project.team << [user, :developer]
+
+ expect(access.can_push_to_branch?(branch.name)).to be_falsey
+ end
+
+ it 'returns false if user is a reporter' do
+ project.team << [user, :reporter]
+
+ expect(access.can_push_to_branch?(branch.name)).to be_falsey
+ end
+ end
+
+ describe 'push to protected branch if allowed for developers' do
+ before do
+ @branch = create :protected_branch, :developers_can_push, project: project
+ end
+
+ it 'returns true if user is a master' do
+ project.team << [user, :master]
+
+ expect(access.can_push_to_branch?(@branch.name)).to be_truthy
+ end
+
+ it 'returns true if user is a developer' do
+ project.team << [user, :developer]
+
+ expect(access.can_push_to_branch?(@branch.name)).to be_truthy
+ end
+
+ it 'returns false if user is a reporter' do
+ project.team << [user, :reporter]
+
+ expect(access.can_push_to_branch?(@branch.name)).to be_falsey
+ end
+ end
+
+ describe 'merge to protected branch if allowed for developers' do
+ before do
+ @branch = create :protected_branch, :developers_can_merge, project: project
+ end
+
+ it 'returns true if user is a master' do
+ project.team << [user, :master]
+
+ expect(access.can_merge_to_branch?(@branch.name)).to be_truthy
+ end
+
+ it 'returns true if user is a developer' do
+ project.team << [user, :developer]
+
+ expect(access.can_merge_to_branch?(@branch.name)).to be_truthy
+ end
+
+ it 'returns false if user is a reporter' do
+ project.team << [user, :reporter]
+
+ expect(access.can_merge_to_branch?(@branch.name)).to be_falsey
+ end
+ end
+ end
+end
diff --git a/spec/lib/repository_cache_spec.rb b/spec/lib/repository_cache_spec.rb
index 63b5292b098..f227926f39c 100644
--- a/spec/lib/repository_cache_spec.rb
+++ b/spec/lib/repository_cache_spec.rb
@@ -1,33 +1,34 @@
-require_relative '../../lib/repository_cache'
+require 'spec_helper'
describe RepositoryCache, lib: true do
+ let(:project) { create(:project) }
let(:backend) { double('backend').as_null_object }
- let(:cache) { RepositoryCache.new('example', backend) }
+ let(:cache) { RepositoryCache.new('example', project.id, backend) }
describe '#cache_key' do
it 'includes the namespace' do
- expect(cache.cache_key(:foo)).to eq 'foo:example'
+ expect(cache.cache_key(:foo)).to eq "foo:example:#{project.id}"
end
end
describe '#expire' do
it 'expires the given key from the cache' do
cache.expire(:foo)
- expect(backend).to have_received(:delete).with('foo:example')
+ expect(backend).to have_received(:delete).with("foo:example:#{project.id}")
end
end
describe '#fetch' do
it 'fetches the given key from the cache' do
cache.fetch(:bar)
- expect(backend).to have_received(:fetch).with('bar:example')
+ expect(backend).to have_received(:fetch).with("bar:example:#{project.id}")
end
it 'accepts a block' do
p = -> {}
cache.fetch(:baz, &p)
- expect(backend).to have_received(:fetch).with('baz:example', &p)
+ expect(backend).to have_received(:fetch).with("baz:example:#{project.id}", &p)
end
end
end
diff --git a/spec/mailers/emails/merge_requests_spec.rb b/spec/mailers/emails/merge_requests_spec.rb
new file mode 100644
index 00000000000..4d3811af254
--- /dev/null
+++ b/spec/mailers/emails/merge_requests_spec.rb
@@ -0,0 +1,19 @@
+require 'spec_helper'
+require 'email_spec'
+require 'mailers/shared/notify'
+
+describe Notify, "merge request notifications" do
+ include EmailSpec::Matchers
+
+ describe "#resolved_all_discussions_email" do
+ let(:user) { create(:user) }
+ let(:merge_request) { create(:merge_request) }
+ let(:current_user) { create(:user) }
+
+ subject { Notify.resolved_all_discussions_email(user.id, merge_request.id, current_user.id) }
+
+ it "includes the name of the resolver" do
+ expect(subject).to have_body_text current_user.name
+ end
+ end
+end
diff --git a/spec/mailers/emails/profile_spec.rb b/spec/mailers/emails/profile_spec.rb
index c6758ccad39..781472d0c00 100644
--- a/spec/mailers/emails/profile_spec.rb
+++ b/spec/mailers/emails/profile_spec.rb
@@ -48,7 +48,7 @@ describe Notify do
it_behaves_like 'it should not have Gmail Actions links'
it_behaves_like 'a user cannot unsubscribe through footer link'
- it 'should not contain the new user\'s password' do
+ it 'does not contain the new user\'s password' do
is_expected.not_to have_body_text /password/
end
end
diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb
index 0a9b10bebea..eae9c060c38 100644
--- a/spec/mailers/notify_spec.rb
+++ b/spec/mailers/notify_spec.rb
@@ -12,7 +12,7 @@ describe Notify do
context 'for a project' do
describe 'items that are assignable, the email' do
let(:current_user) { create(:user, email: "current@email.com") }
- let(:assignee) { create(:user, email: 'assignee@example.com') }
+ let(:assignee) { create(:user, email: 'assignee@example.com', name: 'John Doe') }
let(:previous_assignee) { create(:user, name: 'Previous Assignee') }
shared_examples 'an assignee email' do
@@ -493,7 +493,12 @@ describe Notify do
end
def invite_to_project(project:, email:, inviter:)
- ProjectMember.add_user(project.project_members, 'toto@example.com', Gitlab::Access::DEVELOPER, inviter)
+ Member.add_user(
+ project.project_members,
+ 'toto@example.com',
+ Gitlab::Access::DEVELOPER,
+ current_user: inviter
+ )
project.project_members.invite.last
end
@@ -591,7 +596,7 @@ describe Notify do
is_expected.to have_body_text /#{note.note}/
end
- it 'not contains note author' do
+ it 'does not contain note author' do
is_expected.not_to have_body_text /wrote\:/
end
@@ -740,7 +745,12 @@ describe Notify do
end
def invite_to_group(group:, email:, inviter:)
- GroupMember.add_user(group.group_members, 'toto@example.com', Gitlab::Access::DEVELOPER, inviter)
+ Member.add_user(
+ group.group_members,
+ 'toto@example.com',
+ Gitlab::Access::DEVELOPER,
+ current_user: inviter
+ )
group.group_members.invite.last
end
@@ -944,8 +954,9 @@ describe Notify do
describe 'email on push with multiple commits' do
let(:example_site_path) { root_path }
let(:user) { create(:user) }
- let(:compare) { Gitlab::Git::Compare.new(project.repository.raw_repository, sample_image_commit.id, sample_commit.id) }
- let(:commits) { Commit.decorate(compare.commits, nil) }
+ let(:raw_compare) { Gitlab::Git::Compare.new(project.repository.raw_repository, sample_image_commit.id, sample_commit.id) }
+ let(:compare) { Compare.decorate(raw_compare, project) }
+ let(:commits) { compare.commits }
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) { Gitlab::Diff::DiffRefs.new(base_sha: project.merge_base_commit(sample_image_commit.id, sample_commit.id).id, head_sha: sample_commit.id) }
@@ -1046,8 +1057,9 @@ describe Notify do
describe 'email on push with a single commit' do
let(:example_site_path) { root_path }
let(:user) { create(:user) }
- 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(:raw_compare) { Gitlab::Git::Compare.new(project.repository.raw_repository, sample_commit.parent_id, sample_commit.id) }
+ let(:compare) { Compare.decorate(raw_compare, project) }
+ let(:commits) { compare.commits }
let(:diff_path) { namespace_project_commit_path(project.namespace, project, commits.first) }
let(:diff_refs) { Gitlab::Diff::DiffRefs.new(base_sha: project.merge_base_commit(sample_image_commit.id, sample_commit.id).id, head_sha: sample_commit.id) }
diff --git a/spec/models/ability_spec.rb b/spec/models/ability_spec.rb
index 1acb5846fcf..aa3b2bbf471 100644
--- a/spec/models/ability_spec.rb
+++ b/spec/models/ability_spec.rb
@@ -1,6 +1,62 @@
require 'spec_helper'
describe Ability, lib: true do
+ describe '.can_edit_note?' do
+ let(:project) { create(:empty_project) }
+ let!(:note) { create(:note_on_issue, project: project) }
+
+ context 'using an anonymous user' do
+ it 'returns false' do
+ expect(described_class.can_edit_note?(nil, note)).to be_falsy
+ end
+ end
+
+ context 'using a system note' do
+ it 'returns false' do
+ system_note = create(:note, system: true)
+ user = create(:user)
+
+ expect(described_class.can_edit_note?(user, system_note)).to be_falsy
+ end
+ end
+
+ context 'using users with different access levels' do
+ let(:user) { create(:user) }
+
+ it 'returns true for the author' do
+ expect(described_class.can_edit_note?(note.author, note)).to be_truthy
+ end
+
+ it 'returns false for a guest user' do
+ project.team << [user, :guest]
+
+ expect(described_class.can_edit_note?(user, note)).to be_falsy
+ end
+
+ it 'returns false for a developer' do
+ project.team << [user, :developer]
+
+ expect(described_class.can_edit_note?(user, note)).to be_falsy
+ end
+
+ it 'returns true for a master' do
+ project.team << [user, :master]
+
+ expect(described_class.can_edit_note?(user, note)).to be_truthy
+ end
+
+ it 'returns true for a group owner' do
+ group = create(:group)
+ project.project_group_links.create(
+ group: group,
+ group_access: Gitlab::Access::MASTER)
+ group.add_owner(user)
+
+ expect(described_class.can_edit_note?(user, note)).to be_truthy
+ end
+ end
+ end
+
describe '.users_that_can_read_project' do
context 'using a public project' do
it 'returns all the users' do
@@ -114,4 +170,116 @@ describe Ability, lib: true do
end
end
end
+
+ shared_examples_for ".project_abilities" do |enable_request_store|
+ before do
+ RequestStore.begin! if enable_request_store
+ end
+
+ after do
+ if enable_request_store
+ RequestStore.end!
+ RequestStore.clear!
+ end
+ end
+
+ describe '.project_abilities' do
+ let!(:project) { create(:empty_project, :public) }
+ let!(:user) { create(:user) }
+
+ it 'returns permissions for admin user' do
+ admin = create(:admin)
+
+ results = described_class.project_abilities(admin, project)
+
+ expect(results.count).to eq(68)
+ end
+
+ it 'returns permissions for an owner' do
+ results = described_class.project_abilities(project.owner, project)
+
+ expect(results.count).to eq(68)
+ end
+
+ it 'returns permissions for a master' do
+ project.team << [user, :master]
+
+ results = described_class.project_abilities(user, project)
+
+ expect(results.count).to eq(60)
+ end
+
+ it 'returns permissions for a developer' do
+ project.team << [user, :developer]
+
+ results = described_class.project_abilities(user, project)
+
+ expect(results.count).to eq(44)
+ end
+
+ it 'returns permissions for a guest' do
+ project.team << [user, :guest]
+
+ results = described_class.project_abilities(user, project)
+
+ expect(results.count).to eq(21)
+ end
+ end
+ end
+
+ describe '.project_abilities with RequestStore' do
+ it_behaves_like ".project_abilities", true
+ end
+
+ describe '.project_abilities without RequestStore' do
+ it_behaves_like ".project_abilities", false
+ end
+
+ describe '.issues_readable_by_user' do
+ context 'with an admin user' do
+ it 'returns all given issues' do
+ user = build(:user, admin: true)
+ issue = build(:issue)
+
+ expect(described_class.issues_readable_by_user([issue], user)).
+ to eq([issue])
+ end
+ end
+
+ context 'with a regular user' do
+ it 'returns the issues readable by the user' do
+ user = build(:user)
+ issue = build(:issue)
+
+ expect(issue).to receive(:readable_by?).with(user).and_return(true)
+
+ expect(described_class.issues_readable_by_user([issue], user)).
+ to eq([issue])
+ end
+
+ it 'returns an empty Array when no issues are readable' do
+ user = build(:user)
+ issue = build(:issue)
+
+ expect(issue).to receive(:readable_by?).with(user).and_return(false)
+
+ expect(described_class.issues_readable_by_user([issue], user)).to eq([])
+ end
+ end
+
+ context 'without a regular user' do
+ it 'returns issues that are publicly visible' do
+ hidden_issue = build(:issue)
+ visible_issue = build(:issue)
+
+ expect(hidden_issue).to receive(:publicly_visible?).and_return(false)
+ expect(visible_issue).to receive(:publicly_visible?).and_return(true)
+
+ issues = described_class.
+ issues_readable_by_user([hidden_issue, visible_issue])
+
+ expect(issues).to eq([visible_issue])
+ end
+ end
+ end
end
diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb
index 2ea1320267c..cc215d252f9 100644
--- a/spec/models/application_setting_spec.rb
+++ b/spec/models/application_setting_spec.rb
@@ -53,24 +53,61 @@ describe ApplicationSetting, models: true do
end
context 'restricted signup domains' do
- it 'set single domain' do
- setting.restricted_signup_domains_raw = 'example.com'
- expect(setting.restricted_signup_domains).to eq(['example.com'])
+ it 'sets single domain' do
+ setting.domain_whitelist_raw = 'example.com'
+ expect(setting.domain_whitelist).to eq(['example.com'])
end
- it 'set multiple domains with spaces' do
- setting.restricted_signup_domains_raw = 'example.com *.example.com'
- expect(setting.restricted_signup_domains).to eq(['example.com', '*.example.com'])
+ it 'sets multiple domains with spaces' do
+ setting.domain_whitelist_raw = 'example.com *.example.com'
+ expect(setting.domain_whitelist).to eq(['example.com', '*.example.com'])
end
- it 'set multiple domains with newlines and a space' do
- setting.restricted_signup_domains_raw = "example.com\n *.example.com"
- expect(setting.restricted_signup_domains).to eq(['example.com', '*.example.com'])
+ it 'sets multiple domains with newlines and a space' do
+ setting.domain_whitelist_raw = "example.com\n *.example.com"
+ expect(setting.domain_whitelist).to eq(['example.com', '*.example.com'])
end
- it 'set multiple domains with commas' do
- setting.restricted_signup_domains_raw = "example.com, *.example.com"
- expect(setting.restricted_signup_domains).to eq(['example.com', '*.example.com'])
+ it 'sets multiple domains with commas' do
+ setting.domain_whitelist_raw = "example.com, *.example.com"
+ expect(setting.domain_whitelist).to eq(['example.com', '*.example.com'])
+ end
+ end
+
+ context 'blacklisted signup domains' do
+ it 'sets single domain' do
+ setting.domain_blacklist_raw = 'example.com'
+ expect(setting.domain_blacklist).to contain_exactly('example.com')
+ end
+
+ it 'sets multiple domains with spaces' do
+ setting.domain_blacklist_raw = 'example.com *.example.com'
+ expect(setting.domain_blacklist).to contain_exactly('example.com', '*.example.com')
+ end
+
+ it 'sets multiple domains with newlines and a space' do
+ setting.domain_blacklist_raw = "example.com\n *.example.com"
+ expect(setting.domain_blacklist).to contain_exactly('example.com', '*.example.com')
+ end
+
+ it 'sets multiple domains with commas' do
+ setting.domain_blacklist_raw = "example.com, *.example.com"
+ expect(setting.domain_blacklist).to contain_exactly('example.com', '*.example.com')
+ end
+
+ it 'sets multiple domains with semicolon' do
+ setting.domain_blacklist_raw = "example.com; *.example.com"
+ expect(setting.domain_blacklist).to contain_exactly('example.com', '*.example.com')
+ end
+
+ it 'sets multiple domains with mixture of everything' do
+ setting.domain_blacklist_raw = "example.com; *.example.com\n test.com\sblock.com yes.com"
+ expect(setting.domain_blacklist).to contain_exactly('example.com', '*.example.com', 'test.com', 'block.com', 'yes.com')
+ end
+
+ it 'sets multiple domain with file' do
+ setting.domain_blacklist_file = File.open(Rails.root.join('spec/fixtures/', 'domain_blacklist.txt'))
+ expect(setting.domain_blacklist).to contain_exactly('example.com', 'test.com', 'foo.bar')
end
end
end
diff --git a/spec/models/blob_spec.rb b/spec/models/blob_spec.rb
index 78e95c8fac5..cee20234e1f 100644
--- a/spec/models/blob_spec.rb
+++ b/spec/models/blob_spec.rb
@@ -33,6 +33,22 @@ describe Blob do
end
end
+ describe '#video?' do
+ it 'is falsey with image extension' do
+ git_blob = Gitlab::Git::Blob.new(name: 'image.png')
+
+ expect(described_class.decorate(git_blob)).not_to be_video
+ end
+
+ UploaderHelper::VIDEO_EXT.each do |ext|
+ it "is truthy when extension is .#{ext}" do
+ git_blob = Gitlab::Git::Blob.new(name: "video.#{ext}")
+
+ expect(described_class.decorate(git_blob)).to be_video
+ end
+ end
+ end
+
describe '#to_partial_path' do
def stubbed_blob(overrides = {})
overrides.reverse_merge!(
@@ -78,4 +94,26 @@ describe Blob do
expect(blob.to_partial_path).to eq 'download'
end
end
+
+ describe '#size_within_svg_limits?' do
+ let(:blob) { described_class.decorate(double(:blob)) }
+
+ it 'returns true when the blob size is smaller than the SVG limit' do
+ expect(blob).to receive(:size).and_return(42)
+
+ expect(blob.size_within_svg_limits?).to eq(true)
+ end
+
+ it 'returns true when the blob size is equal to the SVG limit' do
+ expect(blob).to receive(:size).and_return(Blob::MAXIMUM_SVG_SIZE)
+
+ expect(blob.size_within_svg_limits?).to eq(true)
+ end
+
+ it 'returns false when the blob size is larger than the SVG limit' do
+ expect(blob).to receive(:size).and_return(1.terabyte)
+
+ expect(blob.size_within_svg_limits?).to eq(false)
+ end
+ end
end
diff --git a/spec/models/board_spec.rb b/spec/models/board_spec.rb
new file mode 100644
index 00000000000..12d29540137
--- /dev/null
+++ b/spec/models/board_spec.rb
@@ -0,0 +1,12 @@
+require 'rails_helper'
+
+describe Board do
+ describe 'relationships' do
+ it { is_expected.to belong_to(:project) }
+ it { is_expected.to have_many(:lists).order(list_type: :asc, position: :asc).dependent(:delete_all) }
+ end
+
+ describe 'validations' do
+ it { is_expected.to validate_presence_of(:project) }
+ end
+end
diff --git a/spec/models/broadcast_message_spec.rb b/spec/models/broadcast_message_spec.rb
index 6ad8bfef4f2..02d6263094a 100644
--- a/spec/models/broadcast_message_spec.rb
+++ b/spec/models/broadcast_message_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
describe BroadcastMessage, models: true do
- include ActiveSupport::Testing::TimeHelpers
-
subject { create(:broadcast_message) }
it { is_expected.to be_valid }
@@ -23,19 +21,19 @@ describe BroadcastMessage, models: true do
end
describe '.current' do
- it "should return last message if time match" do
+ it "returns last message if time match" do
message = create(:broadcast_message)
expect(BroadcastMessage.current).to eq message
end
- it "should return nil if time not come" do
+ it "returns nil if time not come" do
create(:broadcast_message, :future)
expect(BroadcastMessage.current).to be_nil
end
- it "should return nil if time has passed" do
+ it "returns nil if time has passed" do
create(:broadcast_message, :expired)
expect(BroadcastMessage.current).to be_nil
diff --git a/spec/models/build_spec.rb b/spec/models/build_spec.rb
index e8171788872..ee2c3d04984 100644
--- a/spec/models/build_spec.rb
+++ b/spec/models/build_spec.rb
@@ -5,7 +5,9 @@ describe Ci::Build, models: true do
let(:pipeline) do
create(:ci_pipeline, project: project,
- sha: project.commit.id)
+ sha: project.commit.id,
+ ref: project.default_branch,
+ status: 'success')
end
let(:build) { create(:ci_build, pipeline: pipeline) }
@@ -30,7 +32,7 @@ describe Ci::Build, models: true do
end
let(:create_from_build) { Ci::Build.create_from build }
- it 'there should be a pending task' do
+ it 'exists a pending task' do
expect(Ci::Build.pending.count(:all)).to eq 0
create_from_build
expect(Ci::Build.pending.count(:all)).to be > 0
@@ -40,7 +42,7 @@ describe Ci::Build, models: true do
describe '#ignored?' do
subject { build.ignored? }
- context 'if build is not allowed to fail' do
+ context 'when build is not allowed to fail' do
before do
build.allow_failure = false
end
@@ -62,7 +64,7 @@ describe Ci::Build, models: true do
end
end
- context 'if build is allowed to fail' do
+ context 'when build is allowed to fail' do
before do
build.allow_failure = true
end
@@ -90,7 +92,7 @@ describe Ci::Build, models: true do
it { is_expected.to be_empty }
- context 'if build.trace contains text' do
+ context 'when build.trace contains text' do
let(:text) { 'example output' }
before do
build.trace = text
@@ -100,7 +102,7 @@ describe Ci::Build, models: true do
it { expect(subject.length).to be >= text.length }
end
- context 'if build.trace hides token' do
+ context 'when build.trace hides token' do
let(:token) { 'my_secret_token' }
before do
@@ -191,93 +193,185 @@ describe Ci::Build, models: true do
end
describe '#variables' do
+ let(:container_registry_enabled) { false }
+ let(:predefined_variables) do
+ [
+ { key: 'CI', value: 'true', public: true },
+ { key: 'GITLAB_CI', value: 'true', public: true },
+ { key: 'CI_BUILD_ID', value: build.id.to_s, public: true },
+ { key: 'CI_BUILD_TOKEN', value: build.token, public: false },
+ { key: 'CI_BUILD_REF', value: build.sha, public: true },
+ { key: 'CI_BUILD_BEFORE_SHA', value: build.before_sha, public: true },
+ { key: 'CI_BUILD_REF_NAME', value: 'master', public: true },
+ { key: 'CI_BUILD_NAME', value: 'test', public: true },
+ { key: 'CI_BUILD_STAGE', value: 'test', public: true },
+ { key: 'CI_SERVER_NAME', value: 'GitLab', public: true },
+ { key: 'CI_SERVER_VERSION', value: Gitlab::VERSION, public: true },
+ { key: 'CI_SERVER_REVISION', value: Gitlab::REVISION, public: true },
+ { key: 'CI_PROJECT_ID', value: project.id.to_s, public: true },
+ { key: 'CI_PROJECT_NAME', value: project.path, public: true },
+ { key: 'CI_PROJECT_PATH', value: project.path_with_namespace, public: true },
+ { key: 'CI_PROJECT_NAMESPACE', value: project.namespace.path, public: true },
+ { key: 'CI_PROJECT_URL', value: project.web_url, public: true },
+ { key: 'CI_PIPELINE_ID', value: pipeline.id.to_s, public: true }
+ ]
+ end
+
+ before do
+ stub_container_registry_config(enabled: container_registry_enabled, host_port: 'registry.example.com')
+ end
+
+ subject { build.variables }
+
context 'returns variables' do
- subject { build.variables }
+ before do
+ build.yaml_variables = []
+ end
- let(:predefined_variables) do
- [
- { key: :CI_BUILD_NAME, value: 'test', public: true },
- { key: :CI_BUILD_STAGE, value: 'stage', public: true },
- ]
+ it { is_expected.to eq(predefined_variables) }
+ end
+
+ context 'when build is for tag' do
+ let(:tag_variable) do
+ { key: 'CI_BUILD_TAG', value: 'master', public: true }
end
- let(:yaml_variables) do
- [
- { key: :DB_NAME, value: 'postgres', public: true }
- ]
+ before do
+ build.update_attributes(tag: true)
+ end
+
+ it { is_expected.to include(tag_variable) }
+ end
+
+ context 'when secure variable is defined' do
+ let(:secure_variable) do
+ { key: 'SECRET_KEY', value: 'secret_value', public: false }
end
before do
- build.update_attributes(stage: 'stage')
+ build.project.variables << Ci::Variable.new(key: 'SECRET_KEY', value: 'secret_value')
end
- it { is_expected.to eq(predefined_variables + yaml_variables) }
+ it { is_expected.to include(secure_variable) }
+ end
- context 'for tag' do
- let(:tag_variable) do
- [
- { key: :CI_BUILD_TAG, value: 'master', public: true }
- ]
- end
+ context 'when build is for triggers' do
+ let(:trigger) { create(:ci_trigger, project: project) }
+ let(:trigger_request) { create(:ci_trigger_request_with_variables, pipeline: pipeline, trigger: trigger) }
+ let(:user_trigger_variable) do
+ { key: :TRIGGER_KEY_1, value: 'TRIGGER_VALUE_1', public: false }
+ end
+ let(:predefined_trigger_variable) do
+ { key: 'CI_BUILD_TRIGGERED', value: 'true', public: true }
+ end
- before do
- build.update_attributes(tag: true)
- end
+ before do
+ build.trigger_request = trigger_request
+ end
- it { is_expected.to eq(tag_variable + predefined_variables + yaml_variables) }
+ it { is_expected.to include(user_trigger_variable) }
+ it { is_expected.to include(predefined_trigger_variable) }
+ end
+
+ context 'when yaml_variables are undefined' do
+ before do
+ build.yaml_variables = nil
end
- context 'and secure variables' do
- let(:secure_variables) do
- [
- { key: 'SECRET_KEY', value: 'secret_value', public: false }
- ]
+ context 'use from gitlab-ci.yml' do
+ before do
+ stub_ci_pipeline_yaml_file(config)
end
- before do
- build.project.variables << Ci::Variable.new(key: 'SECRET_KEY', value: 'secret_value')
+ context 'when config is not found' do
+ let(:config) { nil }
+
+ it { is_expected.to eq(predefined_variables) }
end
- it { is_expected.to eq(predefined_variables + yaml_variables + secure_variables) }
+ context 'when config does not have a questioned job' do
+ let(:config) do
+ YAML.dump({
+ test_other: {
+ script: 'Hello World'
+ }
+ })
+ end
- context 'and trigger variables' do
- 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 }
- ]
+ it { is_expected.to eq(predefined_variables) }
+ end
+
+ context 'when config has variables' do
+ let(:config) do
+ YAML.dump({
+ test: {
+ script: 'Hello World',
+ variables: {
+ KEY: 'value'
+ }
+ }
+ })
end
- let(:predefined_trigger_variable) do
- [
- { key: :CI_BUILD_TRIGGERED, value: 'true', public: true }
- ]
+ let(:variables) do
+ [{ key: :KEY, value: 'value', public: true }]
end
- before do
- build.trigger_request = trigger_request
- end
+ it { is_expected.to eq(predefined_variables + variables) }
+ end
+ end
+ end
- it { is_expected.to eq(predefined_variables + predefined_trigger_variable + yaml_variables + secure_variables + trigger_variables) }
+ context 'when container registry is enabled' do
+ let(:container_registry_enabled) { true }
+ let(:ci_registry) do
+ { key: 'CI_REGISTRY', value: 'registry.example.com', public: true }
+ end
+ let(:ci_registry_image) do
+ { key: 'CI_REGISTRY_IMAGE', value: project.container_registry_repository_url, public: true }
+ end
+
+ context 'and is disabled for project' do
+ before do
+ project.update(container_registry_enabled: false)
end
- context 'when job variables are defined' do
- ##
- # Job-level variables are defined in gitlab_ci.yml fixture
- #
- context 'when job variables are unique' do
- let(:build) { create(:ci_build, name: 'staging') }
-
- it 'includes job variables' do
- expect(subject).to include(
- { key: :KEY1, value: 'value1', public: true },
- { key: :KEY2, value: 'value2', public: true }
- )
- end
- end
+ it { is_expected.to include(ci_registry) }
+ it { is_expected.not_to include(ci_registry_image) }
+ end
+
+ context 'and is enabled for project' do
+ before do
+ project.update(container_registry_enabled: true)
end
+
+ it { is_expected.to include(ci_registry) }
+ it { is_expected.to include(ci_registry_image) }
end
end
+
+ context 'when runner is assigned to build' do
+ let(:runner) { create(:ci_runner, description: 'description', tag_list: ['docker', 'linux']) }
+
+ before do
+ build.update(runner: runner)
+ end
+
+ it { is_expected.to include({ key: 'CI_RUNNER_ID', value: runner.id.to_s, public: true }) }
+ it { is_expected.to include({ key: 'CI_RUNNER_DESCRIPTION', value: 'description', public: true }) }
+ it { is_expected.to include({ key: 'CI_RUNNER_TAGS', value: 'docker, linux', public: true }) }
+ end
+
+ context 'returns variables in valid order' do
+ before do
+ allow(build).to receive(:predefined_variables) { ['predefined'] }
+ allow(project).to receive(:predefined_variables) { ['project'] }
+ allow(pipeline).to receive(:predefined_variables) { ['pipeline'] }
+ allow(build).to receive(:yaml_variables) { ['yaml'] }
+ allow(project).to receive(:secret_variables) { ['secret'] }
+ end
+
+ it { is_expected.to eq(%w[predefined project pipeline yaml secret]) }
+ end
end
describe '#has_tags?' do
@@ -299,7 +393,7 @@ describe Ci::Build, models: true do
it { is_expected.to be_falsey }
end
- context 'if there are runner' do
+ context 'when there are runners' do
let(:runner) { create(:ci_runner) }
before do
@@ -329,29 +423,27 @@ describe Ci::Build, models: true do
describe '#stuck?' do
subject { build.stuck? }
- %w(pending).each do |state|
- context "if commit_status.status is #{state}" do
- before do
- build.status = state
- end
-
- it { is_expected.to be_truthy }
+ context "when commit_status.status is pending" do
+ before do
+ build.status = 'pending'
+ end
- context "and there are specific runner" do
- let(:runner) { create(:ci_runner, contacted_at: 1.second.ago) }
+ it { is_expected.to be_truthy }
- before do
- build.project.runners << runner
- runner.save
- end
+ context "and there are specific runner" do
+ let(:runner) { create(:ci_runner, contacted_at: 1.second.ago) }
- it { is_expected.to be_falsey }
+ before do
+ build.project.runners << runner
+ runner.save
end
+
+ it { is_expected.to be_falsey }
end
end
- %w(success failed canceled running).each do |state|
- context "if commit_status.status is #{state}" do
+ %w[success failed canceled running].each do |state|
+ context "when commit_status.status is #{state}" do
before do
build.status = state
end
@@ -479,19 +571,19 @@ describe Ci::Build, models: true do
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
+ it 'expects to have no dependents if this is first build' do
expect(build.depends_on_builds).to be_empty
end
- it 'to have one dependent if this is test' do
+ it 'expects to have one dependent if this is test' do
expect(rspec_test.depends_on_builds.map(&:id)).to contain_exactly(build.id)
end
- it 'to have all builds from build and test stage if this is last' do
+ it 'expects to have all builds from build and test stage if this is last' do
expect(staging.depends_on_builds.map(&:id)).to contain_exactly(build.id, rspec_test.id, rubocop_test.id)
end
- it 'to have retried builds instead the original ones' do
+ it 'expects to have retried builds instead the original ones' do
retried_rspec = Ci::Build.retry(rspec_test)
expect(staging.depends_on_builds.map(&:id)).to contain_exactly(build.id, retried_rspec.id, rubocop_test.id)
end
@@ -561,23 +653,23 @@ describe Ci::Build, models: true do
describe 'build erasable' do
shared_examples 'erasable' do
- it 'should remove artifact file' do
+ it 'removes artifact file' do
expect(build.artifacts_file.exists?).to be_falsy
end
- it 'should remove artifact metadata file' do
+ it 'removes artifact metadata file' do
expect(build.artifacts_metadata.exists?).to be_falsy
end
- it 'should erase build trace in trace file' do
+ it 'erases build trace in trace file' do
expect(build.trace).to be_empty
end
- it 'should set erased to true' do
+ it 'sets erased to true' do
expect(build.erased?).to be true
end
- it 'should set erase date' do
+ it 'sets erase date' do
expect(build.erased_at).not_to be_falsy
end
end
@@ -610,7 +702,7 @@ describe Ci::Build, models: true do
include_examples 'erasable'
- it 'should record user who erased a build' do
+ it 'records user who erased a build' do
expect(build.erased_by).to eq user
end
end
@@ -620,7 +712,7 @@ describe Ci::Build, models: true do
include_examples 'erasable'
- it 'should not set user who erased a build' do
+ it 'does not set user who erased a build' do
expect(build.erased_by).to be_nil
end
end
@@ -628,7 +720,7 @@ describe Ci::Build, models: true do
describe '#erasable?' do
subject { build.erasable? }
- it { is_expected.to eq true }
+ it { is_expected.to be_truthy }
end
describe '#erased?' do
@@ -636,7 +728,7 @@ describe Ci::Build, models: true do
subject { build.erased? }
context 'build has not been erased' do
- it { is_expected.to be false }
+ it { is_expected.to be_falsey }
end
context 'build has been erased' do
@@ -644,18 +736,19 @@ describe Ci::Build, models: true do
build.erase
end
- it { is_expected.to be true }
+ it { is_expected.to be_truthy }
end
end
context 'metadata and build trace are not available' do
let!(:build) { create(:ci_build, :success, :artifacts) }
+
before do
build.remove_artifacts_metadata!
end
describe '#erase' do
- it 'should not raise error' do
+ it 'does not raise error' do
expect { build.erase }.not_to raise_error
end
end
@@ -669,11 +762,192 @@ describe Ci::Build, models: true do
end
end
+ describe '#when' do
+ subject { build.when }
+
+ context 'when `when` is undefined' do
+ before do
+ build.when = nil
+ end
+
+ context 'use from gitlab-ci.yml' do
+ before do
+ stub_ci_pipeline_yaml_file(config)
+ end
+
+ context 'when config is not found' do
+ let(:config) { nil }
+
+ it { is_expected.to eq('on_success') }
+ end
+
+ context 'when config does not have a questioned job' do
+ let(:config) do
+ YAML.dump({
+ test_other: {
+ script: 'Hello World'
+ }
+ })
+ end
+
+ it { is_expected.to eq('on_success') }
+ end
+
+ context 'when config has `when`' do
+ let(:config) do
+ YAML.dump({
+ test: {
+ script: 'Hello World',
+ when: 'always'
+ }
+ })
+ end
+
+ it { is_expected.to eq('always') }
+ end
+ end
+ end
+ end
+
+ describe '#retryable?' do
+ context 'when build is running' do
+ before do
+ build.run!
+ end
+
+ it { expect(build).not_to be_retryable }
+ end
+
+ context 'when build is finished' do
+ before do
+ build.success!
+ end
+
+ it { expect(build).to be_retryable }
+ end
+ end
+
+ describe '#manual?' do
+ before do
+ build.update(when: value)
+ end
+
+ subject { build.manual? }
+
+ context 'when is set to manual' do
+ let(:value) { 'manual' }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when set to something else' do
+ let(:value) { 'something else' }
+
+ it { is_expected.to be_falsey }
+ end
+ end
+
+ describe '#other_actions' do
+ let(:build) { create(:ci_build, :manual, pipeline: pipeline) }
+ let!(:other_build) { create(:ci_build, :manual, pipeline: pipeline, name: 'other action') }
+
+ subject { build.other_actions }
+
+ it 'returns other actions' do
+ is_expected.to contain_exactly(other_build)
+ end
+
+ context 'when build is retried' do
+ let!(:new_build) { Ci::Build.retry(build) }
+
+ it 'does not return any of them' do
+ is_expected.not_to include(build, new_build)
+ end
+ end
+
+ context 'when other build is retried' do
+ let!(:retried_build) { Ci::Build.retry(other_build) }
+
+ it 'returns a retried build' do
+ is_expected.to contain_exactly(retried_build)
+ end
+ end
+ end
+
+ describe '#play' do
+ let(:build) { create(:ci_build, :manual, pipeline: pipeline) }
+
+ subject { build.play }
+
+ it 'enqueues a build' do
+ is_expected.to be_pending
+ is_expected.to eq(build)
+ end
+
+ context 'for successful build' do
+ before do
+ build.update(status: 'success')
+ end
+
+ it 'creates a new build' do
+ is_expected.to be_pending
+ is_expected.not_to eq(build)
+ end
+ end
+ end
+
+ describe '#when' do
+ subject { build.when }
+
+ context 'when `when` is undefined' do
+ before do
+ build.when = nil
+ end
+
+ context 'use from gitlab-ci.yml' do
+ before do
+ stub_ci_pipeline_yaml_file(config)
+ end
+
+ context 'when config is not found' do
+ let(:config) { nil }
+
+ it { is_expected.to eq('on_success') }
+ end
+
+ context 'when config does not have a questioned job' do
+ let(:config) do
+ YAML.dump({
+ test_other: {
+ script: 'Hello World'
+ }
+ })
+ end
+
+ it { is_expected.to eq('on_success') }
+ end
+
+ context 'when config has when' do
+ let(:config) do
+ YAML.dump({
+ test: {
+ script: 'Hello World',
+ when: 'always'
+ }
+ })
+ end
+
+ it { is_expected.to eq('always') }
+ end
+ end
+ end
+ end
+
describe '#retryable?' do
context 'when build is running' do
before { build.run! }
- it 'should return false' do
+ it 'returns false' do
expect(build.retryable?).to be false
end
end
@@ -681,7 +955,7 @@ describe Ci::Build, models: true do
context 'when build is finished' do
before { build.success! }
- it 'should return true' do
+ it 'returns true' do
expect(build.retryable?).to be true
end
end
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index 34507cf5083..721b20e0cb2 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -2,12 +2,15 @@ require 'spec_helper'
describe Ci::Pipeline, models: true do
let(:project) { FactoryGirl.create :empty_project }
- let(:pipeline) { FactoryGirl.create :ci_pipeline, project: project }
+ let(:pipeline) { FactoryGirl.create :ci_empty_pipeline, status: 'created', project: project }
it { is_expected.to belong_to(:project) }
+ it { is_expected.to belong_to(:user) }
+
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 }
@@ -15,7 +18,9 @@ describe Ci::Pipeline, models: true do
it { is_expected.to respond_to :git_author_email }
it { is_expected.to respond_to :short_sha }
- describe :valid_commit_sha do
+ it { is_expected.to delegate_method(:stages).to(:statuses) }
+
+ describe '#valid_commit_sha' do
context 'commit.sha can not start with 00000000' do
before do
pipeline.sha = '0' * 40
@@ -26,7 +31,7 @@ describe Ci::Pipeline, models: true do
end
end
- describe :short_sha do
+ describe '#short_sha' do
subject { pipeline.short_sha }
it 'has 8 items' do
@@ -35,10 +40,7 @@ describe Ci::Pipeline, models: true do
it { expect(pipeline.sha).to start_with(subject) }
end
- describe :create_next_builds do
- end
-
- describe :retried do
+ describe '#retried' do
subject { pipeline.retried }
before do
@@ -51,366 +53,350 @@ describe Ci::Pipeline, models: true do
end
end
- describe :create_builds do
- let!(:pipeline) { FactoryGirl.create :ci_pipeline, project: project, ref: 'master', tag: false }
+ describe "coverage" do
+ let(:project) { FactoryGirl.create :empty_project, build_coverage_regex: "/.*/" }
+ let(:pipeline) { FactoryGirl.create :ci_empty_pipeline, project: project }
- def create_builds(trigger_request = nil)
- pipeline.create_builds(nil, trigger_request)
+ 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
- def create_next_builds
- pipeline.create_next_builds(pipeline.builds.order(:id).last)
+ 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 'creates builds' do
- expect(create_builds).to be_truthy
- pipeline.builds.update_all(status: "success")
- expect(pipeline.builds.count(:all)).to eq(2)
+ 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
- expect(create_next_builds).to be_truthy
- pipeline.builds.update_all(status: "success")
- expect(pipeline.builds.count(:all)).to eq(4)
+ 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
- expect(create_next_builds).to be_truthy
- pipeline.builds.update_all(status: "success")
- expect(pipeline.builds.count(:all)).to eq(5)
+ describe '#retryable?' do
+ subject { pipeline.retryable? }
- expect(create_next_builds).to be_falsey
- end
+ context 'no failed builds' do
+ before do
+ FactoryGirl.create :ci_build, name: "rspec", pipeline: pipeline, status: 'success'
+ 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',
- },
- }
+ it 'be not retryable' do
+ is_expected.to be_falsey
end
+ end
+ context 'with failed builds' do
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",
- }
- }
+ FactoryGirl.create :ci_build, name: "rspec", pipeline: pipeline, status: 'running'
+ FactoryGirl.create :ci_build, name: "rubocop", pipeline: pipeline, status: 'failed'
end
- before do
- stub_ci_pipeline_yaml_file(YAML.dump(yaml))
+ it 'be retryable' do
+ is_expected.to be_truthy
end
+ end
+ 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)
+ describe '#stages' do
+ let(:pipeline2) { FactoryGirl.create :ci_pipeline, project: project }
+ subject { CommitStatus.where(pipeline: [pipeline, pipeline2]).stages }
- 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)
+ 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
- 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)
+ describe 'state machine' do
+ let(:current) { Time.now.change(usec: 0) }
+ let(:build) { create :ci_build, name: 'build1', pipeline: pipeline }
- 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)
+ describe '#duration' do
+ before do
+ travel_to(current - 120) do
+ pipeline.run
+ end
- expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'success', 'success', 'success')
- pipeline.reload
- expect(pipeline.status).to eq('success')
+ travel_to(current) do
+ pipeline.succeed
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)
+ it 'matches sum of builds duration' do
+ expect(pipeline.reload.duration).to eq(120)
+ end
+ end
- 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)
+ describe '#started_at' do
+ it 'updates on transitioning to running' do
+ build.run
- 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.reload.started_at).not_to be_nil
+ end
- 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)
+ it 'does not update on transitioning to success' do
+ build.success
- expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'failed', 'success', 'success')
- pipeline.reload
- expect(pipeline.status).to eq('failed')
- end
+ expect(pipeline.reload.started_at).to be_nil
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)
+ describe '#finished_at' do
+ it 'updates on transitioning to success' do
+ build.success
- 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.reload.finished_at).not_to be_nil
+ end
- 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)
+ it 'does not update on transitioning to running' do
+ build.run
- 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
+ expect(pipeline.reload.finished_at).to be_nil
end
+ 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)
+ describe '#branch?' do
+ subject { pipeline.branch? }
- 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)
+ context 'is not a tag' do
+ before do
+ pipeline.tag = false
+ end
- 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)
+ it 'return true when tag is set to false' do
+ is_expected.to be_truthy
+ end
+ end
- 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)
+ context 'is not a tag' do
+ before do
+ pipeline.tag = true
+ end
- expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'success', 'failed', 'success')
- pipeline.reload
- expect(pipeline.status).to eq('failed')
- end
+ it 'return false when tag is set to true' do
+ is_expected.to be_falsey
end
+ 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)
+ describe '#manual_actions' do
+ subject { pipeline.manual_actions }
- expect(pipeline.builds.running_or_pending).not_to be_empty
+ it 'when none defined' do
+ is_expected.to be_empty
+ end
- 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)
+ context 'when action defined' do
+ let!(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'deploy') }
- expect(pipeline.builds.running_or_pending).to be_empty
- expect(pipeline.reload.status).to eq('canceled')
+ it 'returns one action' do
+ is_expected.to contain_exactly(manual)
+ end
+
+ context 'there are multiple of the same name' do
+ let!(:manual2) { create(:ci_build, :manual, pipeline: pipeline, name: 'deploy') }
+
+ it 'returns latest one' do
+ is_expected.to contain_exactly(manual2)
end
end
end
+ end
- context 'when no builds created' do
- let(:pipeline) { build(:ci_pipeline) }
+ describe '#has_warnings?' do
+ subject { pipeline.has_warnings? }
+ context 'build which is allowed to fail fails' do
before do
- stub_ci_pipeline_yaml_file(YAML.dump(before_script: ['ls']))
+ create :ci_build, :success, pipeline: pipeline, name: 'rspec'
+ create :ci_build, :allowed_to_fail, :failed, pipeline: pipeline, name: 'rubocop'
end
- it 'returns false' do
- expect(pipeline.create_builds(nil)).to be_falsey
- expect(pipeline).not_to be_persisted
+ it 'returns true' do
+ is_expected.to be_truthy
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
+ context 'build which is allowed to fail succeeds' do
+ before do
+ create :ci_build, :success, pipeline: pipeline, name: 'rspec'
+ create :ci_build, :allowed_to_fail, :success, pipeline: pipeline, name: 'rubocop'
+ end
- expect(pipeline.finished_at.to_i).to eq(build.finished_at.to_i)
+ it 'returns false' do
+ is_expected.to be_falsey
+ end
end
- it "returns nil if there is no finished build" do
- FactoryGirl.create :ci_not_started_build, pipeline: pipeline
+ context 'build is retried and succeeds' do
+ before do
+ create :ci_build, :success, pipeline: pipeline, name: 'rubocop'
+ create :ci_build, :failed, pipeline: pipeline, name: 'rspec'
+ create :ci_build, :success, pipeline: pipeline, name: 'rspec'
+ end
- expect(pipeline.finished_at).to be_nil
+ it 'returns false' do
+ is_expected.to be_falsey
+ end
end
end
- describe "coverage" do
- let(:project) { FactoryGirl.create :empty_project, build_coverage_regex: "/.*/" }
- let(:pipeline) { FactoryGirl.create :ci_pipeline, project: project }
+ describe '#status' do
+ let!(:build) { create(:ci_build, :created, pipeline: pipeline, name: 'test') }
- 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
+ subject { pipeline.reload.status }
- 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
+ context 'on queuing' do
+ before do
+ build.enqueue
+ 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")
+ it { is_expected.to eq('pending') }
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
+ context 'on run' do
+ before do
+ build.enqueue
+ build.run
+ end
- describe '#retryable?' do
- subject { pipeline.retryable? }
+ it { is_expected.to eq('running') }
+ end
- context 'no failed builds' do
+ context 'on drop' do
before do
- FactoryGirl.create :ci_build, name: "rspec", pipeline: pipeline, status: 'success'
+ build.drop
end
- it 'be not retryable' do
- is_expected.to be_falsey
- end
+ it { is_expected.to eq('failed') }
end
- context 'with failed builds' do
+ context 'on success' do
before do
- FactoryGirl.create :ci_build, name: "rspec", pipeline: pipeline, status: 'running'
- FactoryGirl.create :ci_build, name: "rubocop", pipeline: pipeline, status: 'failed'
+ build.success
end
- it 'be retryable' do
- is_expected.to be_truthy
- end
+ it { is_expected.to eq('success') }
end
- end
- describe '#stages' do
- let(:pipeline2) { FactoryGirl.create :ci_pipeline, project: project }
- subject { CommitStatus.where(pipeline: [pipeline, pipeline2]).stages }
+ context 'on cancel' do
+ before do
+ build.cancel
+ end
- before do
- FactoryGirl.create :ci_build, pipeline: pipeline2, stage: 'test', stage_idx: 1
- FactoryGirl.create :ci_build, pipeline: pipeline, stage: 'build', stage_idx: 0
+ it { is_expected.to eq('canceled') }
end
- it 'return all stages' do
- is_expected.to eq(%w(build test))
+ context 'on failure and build retry' do
+ before do
+ build.drop
+ Ci::Build.retry(build)
+ end
+
+ # We are changing a state: created > failed > running
+ # Instead of: created > failed > pending
+ # Since the pipeline already run, so it should not be pending anymore
+
+ it { is_expected.to eq('running') }
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
+ describe '#execute_hooks' do
+ let!(:build_a) { create_build('a') }
+ let!(:build_b) { create_build('b') }
- context 'dependent objects' do
- let(:commit_status) { build :commit_status, pipeline: pipeline }
+ let!(:hook) do
+ create(:project_hook, project: project, pipeline_events: enabled)
+ end
- it 'execute update_state after saving dependent object' do
- expect(pipeline).to receive(:update_state).and_return(true)
- commit_status.save
- end
+ before do
+ ProjectWebHookWorker.drain
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 }
+ context 'with pipeline hooks enabled' do
+ let(:enabled) { true }
before do
- build
+ WebMock.stub_request(:post, hook.url)
end
- [:status, :started_at, :finished_at, :duration].each do |param|
- it "update #{param}" do
- expect(pipeline.send(param)).to eq(build.send(param))
+ context 'with multiple builds' do
+ context 'when build is queued' do
+ before do
+ build_a.enqueue
+ build_b.enqueue
+ end
+
+ it 'receive a pending event once' do
+ expect(WebMock).to have_requested_pipeline_hook('pending').once
+ end
end
- end
- end
- end
- describe '#branch?' do
- subject { pipeline.branch? }
+ context 'when build is run' do
+ before do
+ build_a.enqueue
+ build_a.run
+ build_b.enqueue
+ build_b.run
+ end
+
+ it 'receive a running event once' do
+ expect(WebMock).to have_requested_pipeline_hook('running').once
+ end
+ end
- context 'is not a tag' do
- before do
- pipeline.tag = false
- end
+ context 'when all builds succeed' do
+ before do
+ build_a.success
+ build_b.success
+ end
- it 'return true when tag is set to false' do
- is_expected.to be_truthy
+ it 'receive a success event once' do
+ expect(WebMock).to have_requested_pipeline_hook('success').once
+ end
+ end
+
+ def have_requested_pipeline_hook(status)
+ have_requested(:post, hook.url).with do |req|
+ json_body = JSON.parse(req.body)
+ json_body['object_attributes']['status'] == status &&
+ json_body['builds'].length == 2
+ end
+ end
end
end
- context 'is not a tag' do
+ context 'with pipeline hooks disabled' do
+ let(:enabled) { false }
+
before do
- pipeline.tag = true
+ build_a.enqueue
+ build_b.enqueue
end
- it 'return false when tag is set to true' do
- is_expected.to be_falsey
+ it 'did not execute pipeline_hook after touched' do
+ expect(WebMock).not_to have_requested(:post, hook.url)
end
end
+
+ def create_build(name)
+ create(:ci_build, :created, pipeline: pipeline, name: name)
+ end
end
end
diff --git a/spec/models/ci/trigger_spec.rb b/spec/models/ci/trigger_spec.rb
index 474b0b1621d..3ca9231f58e 100644
--- a/spec/models/ci/trigger_spec.rb
+++ b/spec/models/ci/trigger_spec.rb
@@ -4,12 +4,12 @@ describe Ci::Trigger, models: true do
let(:project) { FactoryGirl.create :empty_project }
describe 'before_validation' do
- it 'should set an random token if none provided' do
+ it 'sets an random token if none provided' do
trigger = FactoryGirl.create :ci_trigger_without_token, project: project
expect(trigger.token).not_to be_nil
end
- it 'should not set an random token if one provided' do
+ it 'does not set an random token if one provided' do
trigger = FactoryGirl.create :ci_trigger, project: project
expect(trigger.token).to eq('token')
end
diff --git a/spec/models/ci/variable_spec.rb b/spec/models/ci/variable_spec.rb
index 98f60087cf5..4e7833c3162 100644
--- a/spec/models/ci/variable_spec.rb
+++ b/spec/models/ci/variable_spec.rb
@@ -9,7 +9,7 @@ describe Ci::Variable, models: true do
subject.value = secret_value
end
- describe :value do
+ describe '#value' do
it 'stores the encrypted value' do
expect(subject.encrypted_value).not_to be_nil
end
diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb
index ba02d5fe977..d3e6a6648cc 100644
--- a/spec/models/commit_spec.rb
+++ b/spec/models/commit_spec.rb
@@ -13,6 +13,26 @@ describe Commit, models: true do
it { is_expected.to include_module(StaticModel) }
end
+ describe '#author' do
+ it 'looks up the author in a case-insensitive way' do
+ user = create(:user, email: commit.author_email.upcase)
+ expect(commit.author).to eq(user)
+ end
+
+ it 'caches the author' do
+ user = create(:user, email: commit.author_email)
+ expect(RequestStore).to receive(:active?).twice.and_return(true)
+ expect_any_instance_of(Commit).to receive(:find_author_by_any_email).and_call_original
+
+ expect(commit.author).to eq(user)
+ key = "commit_author:#{commit.author_email}"
+ expect(RequestStore.store[key]).to eq(user)
+
+ expect(commit.author).to eq(user)
+ RequestStore.store.clear
+ end
+ end
+
describe '#to_reference' do
it 'returns a String reference to the object' do
expect(commit.to_reference).to eq commit.id
@@ -66,6 +86,27 @@ eos
end
end
+ describe '#full_title' do
+ it "returns no_commit_message when safe_message is blank" do
+ allow(commit).to receive(:safe_message).and_return('')
+ expect(commit.full_title).to eq("--no commit message")
+ end
+
+ it "returns entire message if there is no newline" do
+ message = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec sodales id felis id blandit. Vivamus egestas lacinia lacus, sed rutrum mauris.'
+
+ allow(commit).to receive(:safe_message).and_return(message)
+ expect(commit.full_title).to eq(message)
+ end
+
+ it "returns first line of message if there is a newLine" do
+ message = commit.safe_message.split(" ").first
+
+ allow(commit).to receive(:safe_message).and_return(message + "\n" + message)
+ expect(commit.full_title).to eq(message)
+ end
+ end
+
describe "delegation" do
subject { commit }
@@ -212,6 +253,7 @@ eos
it 'returns the URI type at the given path' do
expect(commit.uri_type('files/html')).to be(:tree)
expect(commit.uri_type('files/images/logo-black.png')).to be(:raw)
+ expect(project.commit('video').uri_type('files/videos/intro.mp4')).to be(:raw)
expect(commit.uri_type('files/js/application.js')).to be(:blob)
end
diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb
index 96397d7c8a9..fcfa3138ce5 100644
--- a/spec/models/commit_status_spec.rb
+++ b/spec/models/commit_status_spec.rb
@@ -24,14 +24,14 @@ describe CommitStatus, models: true do
it { is_expected.to respond_to :running? }
it { is_expected.to respond_to :pending? }
- describe :author do
+ describe '#author' do
subject { commit_status.author }
before { commit_status.author = User.new }
it { is_expected.to eq(commit_status.user) }
end
- describe :started? do
+ describe '#started?' do
subject { commit_status.started? }
context 'without started_at' do
@@ -57,7 +57,7 @@ describe CommitStatus, models: true do
end
end
- describe :active? do
+ describe '#active?' do
subject { commit_status.active? }
%w(pending running).each do |state|
@@ -77,7 +77,7 @@ describe CommitStatus, models: true do
end
end
- describe :complete? do
+ describe '#complete?' do
subject { commit_status.complete? }
%w(success failed canceled).each do |state|
@@ -97,7 +97,7 @@ describe CommitStatus, models: true do
end
end
- describe :duration do
+ describe '#duration' do
subject { commit_status.duration }
it { is_expected.to eq(120.0) }
@@ -122,7 +122,7 @@ describe CommitStatus, models: true do
end
end
- describe :latest do
+ describe '.latest' do
subject { CommitStatus.latest.order(:id) }
before do
@@ -133,12 +133,12 @@ describe CommitStatus, models: true do
@commit5 = FactoryGirl.create :commit_status, pipeline: pipeline, name: 'aa', ref: 'bb', status: 'success'
end
- it 'return unique statuses' do
+ it 'returns unique statuses' do
is_expected.to eq([@commit4, @commit5])
end
end
- describe :running_or_pending do
+ describe '.running_or_pending' do
subject { CommitStatus.running_or_pending.order(:id) }
before do
@@ -149,7 +149,7 @@ describe CommitStatus, models: true do
@commit5 = FactoryGirl.create :commit_status, pipeline: pipeline, name: 'ee', ref: nil, status: 'canceled'
end
- it 'return statuses that are running or pending' do
+ it 'returns statuses that are running or pending' do
is_expected.to eq([@commit1, @commit2])
end
end
@@ -160,7 +160,7 @@ describe CommitStatus, models: true do
context 'when no before_sha is set for pipeline' do
before { pipeline.before_sha = nil }
- it 'return blank sha' do
+ it 'returns blank sha' do
is_expected.to eq(Gitlab::Git::BLANK_SHA)
end
end
@@ -169,7 +169,7 @@ describe CommitStatus, models: true do
let(:value) { '1234' }
before { pipeline.before_sha = value }
- it 'return the set value' do
+ it 'returns the set value' do
is_expected.to eq(value)
end
end
@@ -177,30 +177,44 @@ describe CommitStatus, models: true do
describe '#stages' do
before do
- 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'
+ create :commit_status, pipeline: pipeline, stage: 'build', name: 'linux', stage_idx: 0, status: 'success'
+ create :commit_status, pipeline: pipeline, stage: 'build', name: 'mac', stage_idx: 0, status: 'failed'
+ create :commit_status, pipeline: pipeline, stage: 'deploy', name: 'staging', stage_idx: 2, status: 'running'
+ create :commit_status, pipeline: pipeline, stage: 'test', name: 'rspec', stage_idx: 1, status: 'success'
end
context 'stages list' do
subject { CommitStatus.where(pipeline: pipeline).stages }
- it 'return ordered list of stages' do
+ it 'returns ordered list of stages' do
is_expected.to eq(%w(build test deploy))
end
end
context 'stages with statuses' do
- subject { CommitStatus.where(pipeline: pipeline).stages_status }
+ subject { CommitStatus.where(pipeline: pipeline).latest.stages_status }
- it 'return list of stages with statuses' do
+ it 'returns list of stages with statuses' do
is_expected.to eq({
'build' => 'failed',
'test' => 'success',
'deploy' => 'running'
})
end
+
+ context 'when build is retried' do
+ before do
+ create :commit_status, pipeline: pipeline, stage: 'build', name: 'mac', stage_idx: 0, status: 'success'
+ end
+
+ it 'ignores a previous state' do
+ is_expected.to eq({
+ 'build' => 'success',
+ 'test' => 'success',
+ 'deploy' => 'running'
+ })
+ end
+ end
end
end
diff --git a/spec/models/compare_spec.rb b/spec/models/compare_spec.rb
new file mode 100644
index 00000000000..49ab3c4b6e9
--- /dev/null
+++ b/spec/models/compare_spec.rb
@@ -0,0 +1,77 @@
+require 'spec_helper'
+
+describe Compare, models: true do
+ include RepoHelpers
+
+ let(:project) { create(:project, :public) }
+ let(:commit) { project.commit }
+
+ let(:start_commit) { sample_image_commit }
+ let(:head_commit) { sample_commit }
+
+ let(:raw_compare) { Gitlab::Git::Compare.new(project.repository.raw_repository, start_commit.id, head_commit.id) }
+
+ subject { described_class.new(raw_compare, project) }
+
+ describe '#start_commit' do
+ it 'returns raw compare base commit' do
+ expect(subject.start_commit.id).to eq(start_commit.id)
+ end
+
+ it 'returns nil if compare base commit is nil' do
+ expect(raw_compare).to receive(:base).and_return(nil)
+
+ expect(subject.start_commit).to eq(nil)
+ end
+ end
+
+ describe '#commit' do
+ it 'returns raw compare head commit' do
+ expect(subject.commit.id).to eq(head_commit.id)
+ end
+
+ it 'returns nil if compare head commit is nil' do
+ expect(raw_compare).to receive(:head).and_return(nil)
+
+ expect(subject.commit).to eq(nil)
+ end
+ end
+
+ describe '#base_commit' do
+ let(:base_commit) { Commit.new(another_sample_commit, project) }
+
+ it 'returns project merge base commit' do
+ expect(project).to receive(:merge_base_commit).with(start_commit.id, head_commit.id).and_return(base_commit)
+
+ expect(subject.base_commit).to eq(base_commit)
+ end
+
+ it 'returns nil if there is no start_commit' do
+ expect(subject).to receive(:start_commit).and_return(nil)
+
+ expect(subject.base_commit).to eq(nil)
+ end
+
+ it 'returns nil if there is no head commit' do
+ expect(subject).to receive(:head_commit).and_return(nil)
+
+ expect(subject.base_commit).to eq(nil)
+ end
+ end
+
+ describe '#diff_refs' do
+ it 'uses base_commit sha as base_sha' do
+ expect(subject).to receive(:base_commit).at_least(:once).and_call_original
+
+ expect(subject.diff_refs.base_sha).to eq(subject.base_commit.id)
+ end
+
+ it 'uses start_commit sha as start_sha' do
+ expect(subject.diff_refs.start_sha).to eq(start_commit.id)
+ end
+
+ it 'uses commit sha as head sha' do
+ expect(subject.diff_refs.head_sha).to eq(head_commit.id)
+ end
+ end
+end
diff --git a/spec/models/concerns/faster_cache_keys_spec.rb b/spec/models/concerns/faster_cache_keys_spec.rb
new file mode 100644
index 00000000000..8d3f94267fa
--- /dev/null
+++ b/spec/models/concerns/faster_cache_keys_spec.rb
@@ -0,0 +1,17 @@
+require 'spec_helper'
+
+describe FasterCacheKeys do
+ describe '#cache_key' do
+ it 'returns a String' do
+ # We're using a fixed string here so it's easier to set an expectation for
+ # the resulting cache key.
+ time = '2016-08-08 16:39:00+02'
+ issue = build(:issue, updated_at: time)
+ issue.extend(described_class)
+
+ expect(issue).to receive(:id).and_return(1)
+
+ expect(issue.cache_key).to eq("issues/1-#{time}")
+ end
+ end
+end
diff --git a/spec/models/concerns/statuseable_spec.rb b/spec/models/concerns/has_status_spec.rb
index 8e0a2a2cbde..e118432d098 100644
--- a/spec/models/concerns/statuseable_spec.rb
+++ b/spec/models/concerns/has_status_spec.rb
@@ -1,9 +1,9 @@
require 'spec_helper'
-describe Statuseable do
+describe HasStatus do
before do
@object = Object.new
- @object.extend(Statuseable::ClassMethods)
+ @object.extend(HasStatus::ClassMethods)
end
describe '.status' do
@@ -12,7 +12,7 @@ describe Statuseable do
end
subject { @object.status }
-
+
shared_examples 'build status summary' do
context 'all successful' do
let(:statuses) { Array.new(2) { create(type, status: :success) } }
diff --git a/spec/models/concerns/mentionable_spec.rb b/spec/models/concerns/mentionable_spec.rb
index 0344dae8b5d..549b0042038 100644
--- a/spec/models/concerns/mentionable_spec.rb
+++ b/spec/models/concerns/mentionable_spec.rb
@@ -7,7 +7,7 @@ describe Mentionable do
nil
end
- describe :references do
+ describe 'references' do
let(:project) { create(:project) }
it 'excludes JIRA references' do
@@ -68,7 +68,7 @@ describe Issue, "Mentionable" do
describe '#create_cross_references!' do
let(:project) { create(:project) }
- let(:author) { double('author') }
+ let(:author) { build(:user) }
let(:commit) { project.commit }
let(:commit2) { project.commit }
@@ -88,6 +88,10 @@ describe Issue, "Mentionable" do
let(:author) { create(:author) }
let(:issues) { create_list(:issue, 2, project: project, author: author) }
+ before do
+ project.team << [author, Gitlab::Access::DEVELOPER]
+ end
+
context 'before changes are persisted' do
it 'ignores pre-existing references' do
issue = create_issue(description: issues[0].to_reference)
diff --git a/spec/models/concerns/milestoneish_spec.rb b/spec/models/concerns/milestoneish_spec.rb
index 7e9ab8940cf..b7e973798a3 100644
--- a/spec/models/concerns/milestoneish_spec.rb
+++ b/spec/models/concerns/milestoneish_spec.rb
@@ -26,53 +26,53 @@ describe Milestone, 'Milestoneish' do
end
describe '#closed_items_count' do
- it 'should not count confidential issues for non project members' do
+ it 'does not count confidential issues for non project members' 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
+ it 'does 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
+ it 'counts confidential issues for author' do
expect(milestone.closed_items_count(author)).to eq 4
end
- it 'should count confidential issues for assignee' do
+ it 'counts confidential issues for assignee' do
expect(milestone.closed_items_count(assignee)).to eq 4
end
- it 'should count confidential issues for project members' do
+ it 'counts confidential issues for project members' do
expect(milestone.closed_items_count(member)).to eq 6
end
- it 'should count all issues for admin' do
+ it 'counts all issues for admin' do
expect(milestone.closed_items_count(admin)).to eq 6
end
end
describe '#total_items_count' do
- it 'should not count confidential issues for non project members' do
+ it 'does not count confidential issues for non project members' 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
+ it 'does 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
+ it 'counts confidential issues for author' do
expect(milestone.total_items_count(author)).to eq 7
end
- it 'should count confidential issues for assignee' do
+ it 'counts confidential issues for assignee' do
expect(milestone.total_items_count(assignee)).to eq 7
end
- it 'should count confidential issues for project members' do
+ it 'counts confidential issues for project members' do
expect(milestone.total_items_count(member)).to eq 10
end
- it 'should count all issues for admin' do
+ it 'counts all issues for admin' do
expect(milestone.total_items_count(admin)).to eq 10
end
end
@@ -91,27 +91,27 @@ describe Milestone, 'Milestoneish' do
end
describe '#percent_complete' do
- it 'should not count confidential issues for non project members' do
+ it 'does not count confidential issues for non project members' do
expect(milestone.percent_complete(non_member)).to eq 50
end
- it 'should not count confidential issues for project members with guest role' do
+ it 'does 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
+ it 'counts confidential issues for author' do
expect(milestone.percent_complete(author)).to eq 57
end
- it 'should count confidential issues for assignee' do
+ it 'counts confidential issues for assignee' do
expect(milestone.percent_complete(assignee)).to eq 57
end
- it 'should count confidential issues for project members' do
+ it 'counts confidential issues for project members' do
expect(milestone.percent_complete(member)).to eq 60
end
- it 'should count confidential issues for admin' do
+ it 'counts confidential issues for admin' do
expect(milestone.percent_complete(admin)).to eq 60
end
end
diff --git a/spec/models/concerns/spammable_spec.rb b/spec/models/concerns/spammable_spec.rb
new file mode 100644
index 00000000000..32935bc0b09
--- /dev/null
+++ b/spec/models/concerns/spammable_spec.rb
@@ -0,0 +1,33 @@
+require 'spec_helper'
+
+describe Issue, 'Spammable' do
+ let(:issue) { create(:issue, description: 'Test Desc.') }
+
+ describe 'Associations' do
+ it { is_expected.to have_one(:user_agent_detail).dependent(:destroy) }
+ end
+
+ describe 'ClassMethods' do
+ it 'should return correct attr_spammable' do
+ expect(issue.spammable_text).to eq("#{issue.title}\n#{issue.description}")
+ end
+ end
+
+ describe 'InstanceMethods' do
+ it 'should be invalid if spam' do
+ issue = build(:issue, spam: true)
+ expect(issue.valid?).to be_falsey
+ end
+
+ describe '#check_for_spam?' do
+ it 'returns true for public project' do
+ issue.project.update_attribute(:visibility_level, Gitlab::VisibilityLevel::PUBLIC)
+ expect(issue.check_for_spam?).to eq(true)
+ end
+
+ it 'returns false for other visibility levels' do
+ expect(issue.check_for_spam?).to eq(false)
+ end
+ end
+ end
+end
diff --git a/spec/models/concerns/token_authenticatable_spec.rb b/spec/models/concerns/token_authenticatable_spec.rb
index 9e8ebc56a31..eb64f3d0c83 100644
--- a/spec/models/concerns/token_authenticatable_spec.rb
+++ b/spec/models/concerns/token_authenticatable_spec.rb
@@ -41,7 +41,7 @@ describe ApplicationSetting, 'TokenAuthenticatable' do
describe 'ensured! token' do
subject { described_class.new.send("ensure_#{token_field}!") }
- it 'should persist new token' do
+ it 'persists new token' do
expect(subject).to eq described_class.current[token_field]
end
end
diff --git a/spec/models/deployment_spec.rb b/spec/models/deployment_spec.rb
index b273018707f..bfff639ad78 100644
--- a/spec/models/deployment_spec.rb
+++ b/spec/models/deployment_spec.rb
@@ -11,7 +11,32 @@ describe Deployment, models: true do
it { is_expected.to delegate_method(:name).to(:environment).with_prefix }
it { is_expected.to delegate_method(:commit).to(:project) }
it { is_expected.to delegate_method(:commit_title).to(:commit).as(:try) }
+ it { is_expected.to delegate_method(:manual_actions).to(:deployable).as(:try) }
it { is_expected.to validate_presence_of(:ref) }
it { is_expected.to validate_presence_of(:sha) }
+
+ describe '#includes_commit?' do
+ let(:project) { create(:project) }
+ let(:environment) { create(:environment, project: project) }
+ let(:deployment) do
+ create(:deployment, environment: environment, sha: project.commit.id)
+ end
+
+ context 'when there is no project commit' do
+ it 'returns false' do
+ commit = project.commit('feature')
+
+ expect(deployment.includes_commit?(commit)).to be false
+ end
+ end
+
+ context 'when they share the same tree branch' do
+ it 'returns true' do
+ commit = project.commit
+
+ expect(deployment.includes_commit?(commit)).to be true
+ end
+ end
+ end
end
diff --git a/spec/models/diff_note_spec.rb b/spec/models/diff_note_spec.rb
index af8e890ca95..6a640474cfe 100644
--- a/spec/models/diff_note_spec.rb
+++ b/spec/models/diff_note_spec.rb
@@ -103,7 +103,7 @@ describe DiffNote, models: true do
describe "#active?" do
context "when noteable is a commit" do
- subject { create(:diff_note_on_commit, project: project, position: position) }
+ subject { build(:diff_note_on_commit, project: project, position: position) }
it "returns true" do
expect(subject.active?).to be true
@@ -119,7 +119,7 @@ describe DiffNote, models: true do
context "when the merge request's diff refs don't match that of the diff note" do
before do
- allow(subject.noteable).to receive(:diff_refs).and_return(commit.diff_refs)
+ allow(subject.noteable).to receive(:diff_sha_refs).and_return(commit.diff_refs)
end
it "returns false" do
@@ -168,7 +168,7 @@ describe DiffNote, models: true do
context "when the note is outdated" do
before do
- allow(merge_request).to receive(:diff_refs).and_return(commit.diff_refs)
+ allow(merge_request).to receive(:diff_sha_refs).and_return(commit.diff_refs)
end
it "uses the DiffPositionUpdateService" do
@@ -188,4 +188,300 @@ describe DiffNote, models: true do
end
end
end
+
+ describe "#resolvable?" do
+ context "when noteable is a commit" do
+ subject { create(:diff_note_on_commit, project: project, position: position) }
+
+ it "returns false" do
+ expect(subject.resolvable?).to be false
+ end
+ end
+
+ context "when noteable is a merge request" do
+ context "when a system note" do
+ before do
+ subject.system = true
+ end
+
+ it "returns false" do
+ expect(subject.resolvable?).to be false
+ end
+ end
+
+ context "when a regular note" do
+ it "returns true" do
+ expect(subject.resolvable?).to be true
+ end
+ end
+ end
+ end
+
+ describe "#to_be_resolved?" do
+ context "when not resolvable" do
+ before do
+ allow(subject).to receive(:resolvable?).and_return(false)
+ end
+
+ it "returns false" do
+ expect(subject.to_be_resolved?).to be false
+ end
+ end
+
+ context "when resolvable" do
+ before do
+ allow(subject).to receive(:resolvable?).and_return(true)
+ end
+
+ context "when resolved" do
+ before do
+ allow(subject).to receive(:resolved?).and_return(true)
+ end
+
+ it "returns false" do
+ expect(subject.to_be_resolved?).to be false
+ end
+ end
+
+ context "when not resolved" do
+ before do
+ allow(subject).to receive(:resolved?).and_return(false)
+ end
+
+ it "returns true" do
+ expect(subject.to_be_resolved?).to be true
+ end
+ end
+ end
+ end
+
+ describe "#resolve!" do
+ let(:current_user) { create(:user) }
+
+ context "when not resolvable" do
+ before do
+ allow(subject).to receive(:resolvable?).and_return(false)
+ end
+
+ it "returns nil" do
+ expect(subject.resolve!(current_user)).to be_nil
+ end
+
+ it "doesn't set resolved_at" do
+ subject.resolve!(current_user)
+
+ expect(subject.resolved_at).to be_nil
+ end
+
+ it "doesn't set resolved_by" do
+ subject.resolve!(current_user)
+
+ expect(subject.resolved_by).to be_nil
+ end
+
+ it "doesn't mark as resolved" do
+ subject.resolve!(current_user)
+
+ expect(subject.resolved?).to be false
+ end
+ end
+
+ context "when resolvable" do
+ before do
+ allow(subject).to receive(:resolvable?).and_return(true)
+ end
+
+ context "when already resolved" do
+ let(:user) { create(:user) }
+
+ before do
+ subject.resolve!(user)
+ end
+
+ it "returns nil" do
+ expect(subject.resolve!(current_user)).to be_nil
+ end
+
+ it "doesn't change resolved_at" do
+ expect(subject.resolved_at).not_to be_nil
+
+ expect { subject.resolve!(current_user) }.not_to change { subject.resolved_at }
+ end
+
+ it "doesn't change resolved_by" do
+ expect(subject.resolved_by).to eq(user)
+
+ expect { subject.resolve!(current_user) }.not_to change { subject.resolved_by }
+ end
+
+ it "doesn't change resolved status" do
+ expect(subject.resolved?).to be true
+
+ expect { subject.resolve!(current_user) }.not_to change { subject.resolved? }
+ end
+ end
+
+ context "when not yet resolved" do
+ it "returns true" do
+ expect(subject.resolve!(current_user)).to be true
+ end
+
+ it "sets resolved_at" do
+ subject.resolve!(current_user)
+
+ expect(subject.resolved_at).not_to be_nil
+ end
+
+ it "sets resolved_by" do
+ subject.resolve!(current_user)
+
+ expect(subject.resolved_by).to eq(current_user)
+ end
+
+ it "marks as resolved" do
+ subject.resolve!(current_user)
+
+ expect(subject.resolved?).to be true
+ end
+ end
+ end
+ end
+
+ describe "#unresolve!" do
+ context "when not resolvable" do
+ before do
+ allow(subject).to receive(:resolvable?).and_return(false)
+ end
+
+ it "returns nil" do
+ expect(subject.unresolve!).to be_nil
+ end
+ end
+
+ context "when resolvable" do
+ before do
+ allow(subject).to receive(:resolvable?).and_return(true)
+ end
+
+ context "when resolved" do
+ let(:user) { create(:user) }
+
+ before do
+ subject.resolve!(user)
+ end
+
+ it "returns true" do
+ expect(subject.unresolve!).to be true
+ end
+
+ it "unsets resolved_at" do
+ subject.unresolve!
+
+ expect(subject.resolved_at).to be_nil
+ end
+
+ it "unsets resolved_by" do
+ subject.unresolve!
+
+ expect(subject.resolved_by).to be_nil
+ end
+
+ it "unmarks as resolved" do
+ subject.unresolve!
+
+ expect(subject.resolved?).to be false
+ end
+ end
+
+ context "when not resolved" do
+ it "returns nil" do
+ expect(subject.unresolve!).to be_nil
+ end
+ end
+ end
+ end
+
+ describe "#discussion" do
+ context "when not resolvable" do
+ before do
+ allow(subject).to receive(:resolvable?).and_return(false)
+ end
+
+ it "returns nil" do
+ expect(subject.discussion).to be_nil
+ end
+ end
+
+ context "when resolvable" do
+ let!(:diff_note2) { create(:diff_note_on_merge_request, project: project, noteable: merge_request, position: subject.position) }
+ let!(:diff_note3) { create(:diff_note_on_merge_request, project: project, noteable: merge_request, position: active_position2) }
+
+ let(:active_position2) do
+ Gitlab::Diff::Position.new(
+ old_path: "files/ruby/popen.rb",
+ new_path: "files/ruby/popen.rb",
+ old_line: 16,
+ new_line: 22,
+ diff_refs: merge_request.diff_refs
+ )
+ end
+
+ it "returns the discussion this note is in" do
+ discussion = subject.discussion
+
+ expect(discussion.id).to eq(subject.discussion_id)
+ expect(discussion.notes).to eq([subject, diff_note2])
+ end
+ end
+ end
+
+ describe "#discussion_id" do
+ let(:note) { create(:diff_note_on_merge_request) }
+
+ context "when it is newly created" do
+ it "has a discussion id" do
+ expect(note.discussion_id).not_to be_nil
+ expect(note.discussion_id).to match(/\A\h{40}\z/)
+ end
+ end
+
+ context "when it didn't store a discussion id before" do
+ before do
+ note.update_column(:discussion_id, nil)
+ end
+
+ it "has a discussion id" do
+ # The discussion_id is set in `after_initialize`, so `reload` won't work
+ reloaded_note = Note.find(note.id)
+
+ expect(reloaded_note.discussion_id).not_to be_nil
+ expect(reloaded_note.discussion_id).to match(/\A\h{40}\z/)
+ end
+ end
+ end
+
+ describe "#original_discussion_id" do
+ let(:note) { create(:diff_note_on_merge_request) }
+
+ context "when it is newly created" do
+ it "has a discussion id" do
+ expect(note.original_discussion_id).not_to be_nil
+ expect(note.original_discussion_id).to match(/\A\h{40}\z/)
+ end
+ end
+
+ context "when it didn't store a discussion id before" do
+ before do
+ note.update_column(:original_discussion_id, nil)
+ end
+
+ it "has a discussion id" do
+ # The original_discussion_id is set in `after_initialize`, so `reload` won't work
+ reloaded_note = Note.find(note.id)
+
+ expect(reloaded_note.original_discussion_id).not_to be_nil
+ expect(reloaded_note.original_discussion_id).to match(/\A\h{40}\z/)
+ end
+ end
+ end
end
diff --git a/spec/models/discussion_spec.rb b/spec/models/discussion_spec.rb
new file mode 100644
index 00000000000..179f2e73662
--- /dev/null
+++ b/spec/models/discussion_spec.rb
@@ -0,0 +1,615 @@
+require 'spec_helper'
+
+describe Discussion, model: true do
+ subject { described_class.new([first_note, second_note, third_note]) }
+
+ let(:first_note) { create(:diff_note_on_merge_request) }
+ let(:second_note) { create(:diff_note_on_merge_request) }
+ let(:third_note) { create(:diff_note_on_merge_request) }
+
+ describe "#resolvable?" do
+ context "when a diff discussion" do
+ before do
+ allow(subject).to receive(:diff_discussion?).and_return(true)
+ end
+
+ context "when all notes are unresolvable" do
+ before do
+ allow(first_note).to receive(:resolvable?).and_return(false)
+ allow(second_note).to receive(:resolvable?).and_return(false)
+ allow(third_note).to receive(:resolvable?).and_return(false)
+ end
+
+ it "returns false" do
+ expect(subject.resolvable?).to be false
+ end
+ end
+
+ context "when some notes are unresolvable and some notes are resolvable" do
+ before do
+ allow(first_note).to receive(:resolvable?).and_return(true)
+ allow(second_note).to receive(:resolvable?).and_return(false)
+ allow(third_note).to receive(:resolvable?).and_return(true)
+ end
+
+ it "returns true" do
+ expect(subject.resolvable?).to be true
+ end
+ end
+
+ context "when all notes are resolvable" do
+ before do
+ allow(first_note).to receive(:resolvable?).and_return(true)
+ allow(second_note).to receive(:resolvable?).and_return(true)
+ allow(third_note).to receive(:resolvable?).and_return(true)
+ end
+
+ it "returns true" do
+ expect(subject.resolvable?).to be true
+ end
+ end
+ end
+
+ context "when not a diff discussion" do
+ before do
+ allow(subject).to receive(:diff_discussion?).and_return(false)
+ end
+
+ it "returns false" do
+ expect(subject.resolvable?).to be false
+ end
+ end
+ end
+
+ describe "#resolved?" do
+ context "when not resolvable" do
+ before do
+ allow(subject).to receive(:resolvable?).and_return(false)
+ end
+
+ it "returns false" do
+ expect(subject.resolved?).to be false
+ end
+ end
+
+ context "when resolvable" do
+ before do
+ allow(subject).to receive(:resolvable?).and_return(true)
+
+ allow(first_note).to receive(:resolvable?).and_return(true)
+ allow(second_note).to receive(:resolvable?).and_return(false)
+ allow(third_note).to receive(:resolvable?).and_return(true)
+ end
+
+ context "when all resolvable notes are resolved" do
+ before do
+ allow(first_note).to receive(:resolved?).and_return(true)
+ allow(third_note).to receive(:resolved?).and_return(true)
+ end
+
+ it "returns true" do
+ expect(subject.resolved?).to be true
+ end
+ end
+
+ context "when some resolvable notes are not resolved" do
+ before do
+ allow(first_note).to receive(:resolved?).and_return(true)
+ allow(third_note).to receive(:resolved?).and_return(false)
+ end
+
+ it "returns false" do
+ expect(subject.resolved?).to be false
+ end
+ end
+ end
+ end
+
+ describe "#to_be_resolved?" do
+ context "when not resolvable" do
+ before do
+ allow(subject).to receive(:resolvable?).and_return(false)
+ end
+
+ it "returns false" do
+ expect(subject.to_be_resolved?).to be false
+ end
+ end
+
+ context "when resolvable" do
+ before do
+ allow(subject).to receive(:resolvable?).and_return(true)
+
+ allow(first_note).to receive(:resolvable?).and_return(true)
+ allow(second_note).to receive(:resolvable?).and_return(false)
+ allow(third_note).to receive(:resolvable?).and_return(true)
+ end
+
+ context "when all resolvable notes are resolved" do
+ before do
+ allow(first_note).to receive(:resolved?).and_return(true)
+ allow(third_note).to receive(:resolved?).and_return(true)
+ end
+
+ it "returns false" do
+ expect(subject.to_be_resolved?).to be false
+ end
+ end
+
+ context "when some resolvable notes are not resolved" do
+ before do
+ allow(first_note).to receive(:resolved?).and_return(true)
+ allow(third_note).to receive(:resolved?).and_return(false)
+ end
+
+ it "returns true" do
+ expect(subject.to_be_resolved?).to be true
+ end
+ end
+ end
+ end
+
+ describe "#can_resolve?" do
+ let(:current_user) { create(:user) }
+
+ context "when not resolvable" do
+ before do
+ allow(subject).to receive(:resolvable?).and_return(false)
+ end
+
+ it "returns false" do
+ expect(subject.can_resolve?(current_user)).to be false
+ end
+ end
+
+ context "when resolvable" do
+ before do
+ allow(subject).to receive(:resolvable?).and_return(true)
+ end
+
+ context "when not signed in" do
+ let(:current_user) { nil }
+
+ it "returns false" do
+ expect(subject.can_resolve?(current_user)).to be false
+ end
+ end
+
+ context "when signed in" do
+ context "when the signed in user is the noteable author" do
+ before do
+ subject.noteable.author = current_user
+ end
+
+ it "returns true" do
+ expect(subject.can_resolve?(current_user)).to be true
+ end
+ end
+
+ context "when the signed in user can push to the project" do
+ before do
+ subject.project.team << [current_user, :master]
+ end
+
+ it "returns true" do
+ expect(subject.can_resolve?(current_user)).to be true
+ end
+ end
+
+ context "when the signed in user is a random user" do
+ it "returns false" do
+ expect(subject.can_resolve?(current_user)).to be false
+ end
+ end
+ end
+ end
+ end
+
+ describe "#resolve!" do
+ let(:current_user) { create(:user) }
+
+ context "when not resolvable" do
+ before do
+ allow(subject).to receive(:resolvable?).and_return(false)
+ end
+
+ it "returns nil" do
+ expect(subject.resolve!(current_user)).to be_nil
+ end
+
+ it "doesn't set resolved_at" do
+ subject.resolve!(current_user)
+
+ expect(subject.resolved_at).to be_nil
+ end
+
+ it "doesn't set resolved_by" do
+ subject.resolve!(current_user)
+
+ expect(subject.resolved_by).to be_nil
+ end
+
+ it "doesn't mark as resolved" do
+ subject.resolve!(current_user)
+
+ expect(subject.resolved?).to be false
+ end
+ end
+
+ context "when resolvable" do
+ let(:user) { create(:user) }
+
+ before do
+ allow(subject).to receive(:resolvable?).and_return(true)
+
+ allow(first_note).to receive(:resolvable?).and_return(true)
+ allow(second_note).to receive(:resolvable?).and_return(false)
+ allow(third_note).to receive(:resolvable?).and_return(true)
+ end
+
+ context "when all resolvable notes are resolved" do
+ before do
+ first_note.resolve!(user)
+ third_note.resolve!(user)
+ end
+
+ it "calls resolve! on every resolvable note" do
+ expect(first_note).to receive(:resolve!).with(current_user)
+ expect(second_note).not_to receive(:resolve!)
+ expect(third_note).to receive(:resolve!).with(current_user)
+
+ subject.resolve!(current_user)
+ end
+
+ it "doesn't change resolved_at on the resolved notes" do
+ expect(first_note.resolved_at).not_to be_nil
+ expect(third_note.resolved_at).not_to be_nil
+
+ expect { subject.resolve!(current_user) }.not_to change { first_note.resolved_at }
+ expect { subject.resolve!(current_user) }.not_to change { third_note.resolved_at }
+ end
+
+ it "doesn't change resolved_by on the resolved notes" do
+ expect(first_note.resolved_by).to eq(user)
+ expect(third_note.resolved_by).to eq(user)
+
+ expect { subject.resolve!(current_user) }.not_to change { first_note.resolved_by }
+ expect { subject.resolve!(current_user) }.not_to change { third_note.resolved_by }
+ end
+
+ it "doesn't change the resolved state on the resolved notes" do
+ expect(first_note.resolved?).to be true
+ expect(third_note.resolved?).to be true
+
+ expect { subject.resolve!(current_user) }.not_to change { first_note.resolved? }
+ expect { subject.resolve!(current_user) }.not_to change { third_note.resolved? }
+ end
+
+ it "doesn't change resolved_at" do
+ expect(subject.resolved_at).not_to be_nil
+
+ expect { subject.resolve!(current_user) }.not_to change { subject.resolved_at }
+ end
+
+ it "doesn't change resolved_by" do
+ expect(subject.resolved_by).to eq(user)
+
+ expect { subject.resolve!(current_user) }.not_to change { subject.resolved_by }
+ end
+
+ it "doesn't change resolved state" do
+ expect(subject.resolved?).to be true
+
+ expect { subject.resolve!(current_user) }.not_to change { subject.resolved? }
+ end
+ end
+
+ context "when some resolvable notes are resolved" do
+ before do
+ first_note.resolve!(user)
+ end
+
+ it "calls resolve! on every resolvable note" do
+ expect(first_note).to receive(:resolve!).with(current_user)
+ expect(second_note).not_to receive(:resolve!)
+ expect(third_note).to receive(:resolve!).with(current_user)
+
+ subject.resolve!(current_user)
+ end
+
+ it "doesn't change resolved_at on the resolved note" do
+ expect(first_note.resolved_at).not_to be_nil
+
+ expect { subject.resolve!(current_user) }.not_to change { first_note.resolved_at }
+ end
+
+ it "doesn't change resolved_by on the resolved note" do
+ expect(first_note.resolved_by).to eq(user)
+
+ expect { subject.resolve!(current_user) }.not_to change { first_note.resolved_by }
+ end
+
+ it "doesn't change the resolved state on the resolved note" do
+ expect(first_note.resolved?).to be true
+
+ expect { subject.resolve!(current_user) }.not_to change { first_note.resolved? }
+ end
+
+ it "sets resolved_at on the unresolved note" do
+ subject.resolve!(current_user)
+
+ expect(third_note.resolved_at).not_to be_nil
+ end
+
+ it "sets resolved_by on the unresolved note" do
+ subject.resolve!(current_user)
+
+ expect(third_note.resolved_by).to eq(current_user)
+ end
+
+ it "marks the unresolved note as resolved" do
+ subject.resolve!(current_user)
+
+ expect(third_note.resolved?).to be true
+ end
+
+ it "sets resolved_at" do
+ subject.resolve!(current_user)
+
+ expect(subject.resolved_at).not_to be_nil
+ end
+
+ it "sets resolved_by" do
+ subject.resolve!(current_user)
+
+ expect(subject.resolved_by).to eq(current_user)
+ end
+
+ it "marks as resolved" do
+ subject.resolve!(current_user)
+
+ expect(subject.resolved?).to be true
+ end
+ end
+
+ context "when no resolvable notes are resolved" do
+ it "calls resolve! on every resolvable note" do
+ expect(first_note).to receive(:resolve!).with(current_user)
+ expect(second_note).not_to receive(:resolve!)
+ expect(third_note).to receive(:resolve!).with(current_user)
+
+ subject.resolve!(current_user)
+ end
+
+ it "sets resolved_at on the unresolved notes" do
+ subject.resolve!(current_user)
+
+ expect(first_note.resolved_at).not_to be_nil
+ expect(third_note.resolved_at).not_to be_nil
+ end
+
+ it "sets resolved_by on the unresolved notes" do
+ subject.resolve!(current_user)
+
+ expect(first_note.resolved_by).to eq(current_user)
+ expect(third_note.resolved_by).to eq(current_user)
+ end
+
+ it "marks the unresolved notes as resolved" do
+ subject.resolve!(current_user)
+
+ expect(first_note.resolved?).to be true
+ expect(third_note.resolved?).to be true
+ end
+
+ it "sets resolved_at" do
+ subject.resolve!(current_user)
+
+ expect(subject.resolved_at).not_to be_nil
+ end
+
+ it "sets resolved_by" do
+ subject.resolve!(current_user)
+
+ expect(subject.resolved_by).to eq(current_user)
+ end
+
+ it "marks as resolved" do
+ subject.resolve!(current_user)
+
+ expect(subject.resolved?).to be true
+ end
+ end
+ end
+ end
+
+ describe "#unresolve!" do
+ context "when not resolvable" do
+ before do
+ allow(subject).to receive(:resolvable?).and_return(false)
+ end
+
+ it "returns nil" do
+ expect(subject.unresolve!).to be_nil
+ end
+ end
+
+ context "when resolvable" do
+ let(:user) { create(:user) }
+
+ before do
+ allow(subject).to receive(:resolvable?).and_return(true)
+
+ allow(first_note).to receive(:resolvable?).and_return(true)
+ allow(second_note).to receive(:resolvable?).and_return(false)
+ allow(third_note).to receive(:resolvable?).and_return(true)
+ end
+
+ context "when all resolvable notes are resolved" do
+ before do
+ first_note.resolve!(user)
+ third_note.resolve!(user)
+ end
+
+ it "calls unresolve! on every resolvable note" do
+ expect(first_note).to receive(:unresolve!)
+ expect(second_note).not_to receive(:unresolve!)
+ expect(third_note).to receive(:unresolve!)
+
+ subject.unresolve!
+ end
+
+ it "unsets resolved_at on the resolved notes" do
+ subject.unresolve!
+
+ expect(first_note.resolved_at).to be_nil
+ expect(third_note.resolved_at).to be_nil
+ end
+
+ it "unsets resolved_by on the resolved notes" do
+ subject.unresolve!
+
+ expect(first_note.resolved_by).to be_nil
+ expect(third_note.resolved_by).to be_nil
+ end
+
+ it "unmarks the resolved notes as resolved" do
+ subject.unresolve!
+
+ expect(first_note.resolved?).to be false
+ expect(third_note.resolved?).to be false
+ end
+
+ it "unsets resolved_at" do
+ subject.unresolve!
+
+ expect(subject.resolved_at).to be_nil
+ end
+
+ it "unsets resolved_by" do
+ subject.unresolve!
+
+ expect(subject.resolved_by).to be_nil
+ end
+
+ it "unmarks as resolved" do
+ subject.unresolve!
+
+ expect(subject.resolved?).to be false
+ end
+ end
+
+ context "when some resolvable notes are resolved" do
+ before do
+ first_note.resolve!(user)
+ end
+
+ it "calls unresolve! on every resolvable note" do
+ expect(first_note).to receive(:unresolve!)
+ expect(second_note).not_to receive(:unresolve!)
+ expect(third_note).to receive(:unresolve!)
+
+ subject.unresolve!
+ end
+
+ it "unsets resolved_at on the resolved note" do
+ subject.unresolve!
+
+ expect(first_note.resolved_at).to be_nil
+ end
+
+ it "unsets resolved_by on the resolved note" do
+ subject.unresolve!
+
+ expect(first_note.resolved_by).to be_nil
+ end
+
+ it "unmarks the resolved note as resolved" do
+ subject.unresolve!
+
+ expect(first_note.resolved?).to be false
+ end
+ end
+
+ context "when no resolvable notes are resolved" do
+ it "calls unresolve! on every resolvable note" do
+ expect(first_note).to receive(:unresolve!)
+ expect(second_note).not_to receive(:unresolve!)
+ expect(third_note).to receive(:unresolve!)
+
+ subject.unresolve!
+ end
+ end
+ end
+ end
+
+ describe "#collapsed?" do
+ context "when a diff discussion" do
+ before do
+ allow(subject).to receive(:diff_discussion?).and_return(true)
+ end
+
+ context "when resolvable" do
+ before do
+ allow(subject).to receive(:resolvable?).and_return(true)
+ end
+
+ context "when resolved" do
+ before do
+ allow(subject).to receive(:resolved?).and_return(true)
+ end
+
+ it "returns true" do
+ expect(subject.collapsed?).to be true
+ end
+ end
+
+ context "when not resolved" do
+ before do
+ allow(subject).to receive(:resolved?).and_return(false)
+ end
+
+ it "returns false" do
+ expect(subject.collapsed?).to be false
+ end
+ end
+ end
+
+ context "when not resolvable" do
+ before do
+ allow(subject).to receive(:resolvable?).and_return(false)
+ end
+
+ context "when active" do
+ before do
+ allow(subject).to receive(:active?).and_return(true)
+ end
+
+ it "returns false" do
+ expect(subject.collapsed?).to be false
+ end
+ end
+
+ context "when outdated" do
+ before do
+ allow(subject).to receive(:active?).and_return(false)
+ end
+
+ it "returns true" do
+ expect(subject.collapsed?).to be true
+ end
+ end
+ end
+ end
+
+ context "when not a diff discussion" do
+ before do
+ allow(subject).to receive(:diff_discussion?).and_return(false)
+ end
+
+ it "returns false" do
+ expect(subject.collapsed?).to be false
+ end
+ end
+ end
+end
diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb
index 7629af6a570..c881897926e 100644
--- a/spec/models/environment_spec.rb
+++ b/spec/models/environment_spec.rb
@@ -11,4 +11,56 @@ describe Environment, models: true do
it { is_expected.to validate_presence_of(:name) }
it { is_expected.to validate_uniqueness_of(:name).scoped_to(:project_id) }
it { is_expected.to validate_length_of(:name).is_within(0..255) }
+
+ it { is_expected.to validate_length_of(:external_url).is_within(0..255) }
+
+ # To circumvent a not null violation of the name column:
+ # https://github.com/thoughtbot/shoulda-matchers/issues/336
+ it 'validates uniqueness of :external_url' do
+ create(:environment)
+
+ is_expected.to validate_uniqueness_of(:external_url).scoped_to(:project_id)
+ end
+
+ describe '#nullify_external_url' do
+ it 'replaces a blank url with nil' do
+ env = build(:environment, external_url: "")
+
+ expect(env.save).to be true
+ expect(env.external_url).to be_nil
+ end
+ end
+
+ describe '#includes_commit?' do
+ context 'without a last deployment' do
+ it "returns false" do
+ expect(environment.includes_commit?('HEAD')).to be false
+ end
+ end
+
+ context 'with a last deployment' do
+ let(:project) { create(:project) }
+ let(:environment) { create(:environment, project: project) }
+
+ let!(:deployment) do
+ create(:deployment, environment: environment, sha: project.commit('master').id)
+ end
+
+ context 'in the same branch' do
+ it 'returns true' do
+ expect(environment.includes_commit?(RepoHelpers.sample_commit)).to be true
+ end
+ end
+
+ context 'not in the same branch' do
+ before do
+ deployment.update(sha: project.commit('feature').id)
+ end
+
+ it 'returns false' do
+ expect(environment.includes_commit?(RepoHelpers.sample_commit)).to be false
+ end
+ end
+ end
+ end
end
diff --git a/spec/models/forked_project_link_spec.rb b/spec/models/forked_project_link_spec.rb
index fa1a0d4e0c7..9c81d159cdf 100644
--- a/spec/models/forked_project_link_spec.rb
+++ b/spec/models/forked_project_link_spec.rb
@@ -9,16 +9,16 @@ describe ForkedProjectLink, "add link on fork" do
@project_to = fork_project(project_from, user)
end
- it "project_to should know it is forked" do
+ it "project_to knows it is forked" do
expect(@project_to.forked?).to be_truthy
end
- it "project should know who it is forked from" do
+ it "project knows who it is forked from" do
expect(@project_to.forked_from_project).to eq(project_from)
end
end
-describe :forked_from_project do
+describe '#forked?' do
let(:forked_project_link) { build(:forked_project_link) }
let(:project_from) { create(:project) }
let(:project_to) { create(:project, forked_project_link: forked_project_link) }
@@ -29,15 +29,15 @@ describe :forked_from_project do
forked_project_link.save!
end
- it "project_to should know it is forked" do
+ it "project_to knows it is forked" do
expect(project_to.forked?).to be_truthy
end
- it "project_from should not be forked" do
+ it "project_from is not forked" do
expect(project_from.forked?).to be_falsey
end
- it "project_to.destroy should destroy fork_link" do
+ it "project_to.destroy destroys fork_link" do
expect(forked_project_link).to receive(:destroy)
project_to.destroy
end
diff --git a/spec/models/generic_commit_status_spec.rb b/spec/models/generic_commit_status_spec.rb
index c4e781dd1dc..615cfe3142b 100644
--- a/spec/models/generic_commit_status_spec.rb
+++ b/spec/models/generic_commit_status_spec.rb
@@ -4,33 +4,33 @@ describe GenericCommitStatus, models: true do
let(:pipeline) { FactoryGirl.create :ci_pipeline }
let(:generic_commit_status) { FactoryGirl.create :generic_commit_status, pipeline: pipeline }
- describe :context do
+ describe '#context' do
subject { generic_commit_status.context }
before { generic_commit_status.context = 'my_context' }
it { is_expected.to eq(generic_commit_status.name) }
end
- describe :tags do
+ describe '#tags' do
subject { generic_commit_status.tags }
it { is_expected.to eq([:external]) }
end
- describe :set_default_values do
+ describe 'set_default_values' do
before do
generic_commit_status.context = nil
generic_commit_status.stage = nil
generic_commit_status.save
end
- describe :context do
+ describe '#context' do
subject { generic_commit_status.context }
it { is_expected.not_to be_nil }
end
- describe :stage do
+ describe '#stage' do
subject { generic_commit_status.stage }
it { is_expected.not_to be_nil }
diff --git a/spec/models/global_milestone_spec.rb b/spec/models/global_milestone_spec.rb
index 197c99cd007..92e0f7f27ce 100644
--- a/spec/models/global_milestone_spec.rb
+++ b/spec/models/global_milestone_spec.rb
@@ -14,7 +14,7 @@ describe GlobalMilestone, models: true do
let(:milestone2_project2) { create(:milestone, title: "VD-123", project: project2) }
let(:milestone2_project3) { create(:milestone, title: "VD-123", project: project3) }
- describe :build_collection do
+ describe '.build_collection' do
before do
milestones =
[
@@ -29,20 +29,20 @@ describe GlobalMilestone, models: true do
@global_milestones = GlobalMilestone.build_collection(milestones)
end
- it 'should have all project milestones' do
+ it 'has all project milestones' do
expect(@global_milestones.count).to eq(2)
end
- it 'should have all project milestones titles' do
+ it 'has all project milestones titles' do
expect(@global_milestones.map(&:title)).to match_array(['Milestone v1.2', 'VD-123'])
end
- it 'should have all project milestones' do
+ it 'has all project milestones' do
expect(@global_milestones.map { |group_milestone| group_milestone.milestones.count }.sum).to eq(6)
end
end
- describe :initialize do
+ describe '#initialize' do
before do
milestones =
[
@@ -54,19 +54,19 @@ describe GlobalMilestone, models: true do
@global_milestone = GlobalMilestone.new(milestone1_project1.title, milestones)
end
- it 'should have exactly one group milestone' do
+ it 'has exactly one group milestone' do
expect(@global_milestone.title).to eq('Milestone v1.2')
end
- it 'should have all project milestones with the same title' do
+ it 'has all project milestones with the same title' do
expect(@global_milestone.milestones.count).to eq(3)
end
end
- describe :safe_title do
+ describe '#safe_title' do
let(:milestone) { create(:milestone, title: "git / test", project: project1) }
- it 'should strip out slashes and spaces' do
+ it 'strips out slashes and spaces' do
global_milestone = GlobalMilestone.new(milestone.title, [milestone])
expect(global_milestone.safe_title).to eq('git-test')
diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb
index a878ff1b227..ea4b59c26b1 100644
--- a/spec/models/group_spec.rb
+++ b/spec/models/group_spec.rb
@@ -97,26 +97,26 @@ describe Group, models: true do
end
end
- describe :users do
+ describe '#users' do
it { expect(group.users).to eq(group.owners) }
end
- describe :human_name do
+ describe '#human_name' do
it { expect(group.human_name).to eq(group.name) }
end
- describe :add_users do
+ describe '#add_user' do
let(:user) { create(:user) }
before { group.add_user(user, GroupMember::MASTER) }
it { expect(group.group_members.masters.map(&:user)).to include(user) }
end
- describe :add_users do
+ describe '#add_users' do
let(:user) { create(:user) }
before { group.add_users([user.id], GroupMember::GUEST) }
- it "should update the group permission" do
+ it "updates the group permission" do
expect(group.group_members.guests.map(&:user)).to include(user)
group.add_users([user.id], GroupMember::DEVELOPER)
expect(group.group_members.developers.map(&:user)).to include(user)
@@ -124,16 +124,16 @@ describe Group, models: true do
end
end
- describe :avatar_type do
+ describe '#avatar_type' do
let(:user) { create(:user) }
before { group.add_user(user, GroupMember::MASTER) }
- it "should be true if avatar is image" do
+ it "is true if avatar is image" do
group.update_attribute(:avatar, 'uploads/avatar.png')
expect(group.avatar_type).to be_truthy
end
- it "should be false if avatar is html page" do
+ it "is false if avatar is html page" do
group.update_attribute(:avatar, 'uploads/avatar.html')
expect(group.avatar_type).to eq(["only images allowed"])
end
diff --git a/spec/models/hooks/project_hook_spec.rb b/spec/models/hooks/project_hook_spec.rb
index 983848392b7..4a457997a4f 100644
--- a/spec/models/hooks/project_hook_spec.rb
+++ b/spec/models/hooks/project_hook_spec.rb
@@ -24,7 +24,7 @@ describe ProjectHook, models: true do
end
describe '.push_hooks' do
- it 'should return hooks for push events only' do
+ it 'returns hooks for push events only' do
hook = create(:project_hook, push_events: true)
create(:project_hook, push_events: false)
expect(ProjectHook.push_hooks).to eq([hook])
@@ -32,7 +32,7 @@ describe ProjectHook, models: true do
end
describe '.tag_push_hooks' do
- it 'should return hooks for tag push events only' do
+ it 'returns hooks for tag push events only' do
hook = create(:project_hook, tag_push_events: true)
create(:project_hook, tag_push_events: false)
expect(ProjectHook.tag_push_hooks).to eq([hook])
diff --git a/spec/models/hooks/system_hook_spec.rb b/spec/models/hooks/system_hook_spec.rb
index 4078b9e4ff5..cbdf7eec082 100644
--- a/spec/models/hooks/system_hook_spec.rb
+++ b/spec/models/hooks/system_hook_spec.rb
@@ -38,7 +38,7 @@ describe SystemHook, models: true do
end
it "project_destroy hook" do
- Projects::DestroyService.new(project, user, {}).pending_delete!
+ Projects::DestroyService.new(project, user, {}).async_execute
expect(WebMock).to have_requested(:post, system_hook.url).with(
body: /project_destroy/,
diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb
index b87d68283e6..3259f795296 100644
--- a/spec/models/issue_spec.rb
+++ b/spec/models/issue_spec.rb
@@ -22,6 +22,26 @@ describe Issue, models: true do
it { is_expected.to have_db_index(:deleted_at) }
end
+ describe 'visible_to_user' do
+ let(:user) { create(:user) }
+ let(:authorized_user) { create(:user) }
+ let(:project) { create(:project, namespace: authorized_user.namespace) }
+ let!(:public_issue) { create(:issue, project: project) }
+ let!(:confidential_issue) { create(:issue, project: project, confidential: true) }
+
+ it 'returns non confidential issues for nil user' do
+ expect(Issue.visible_to_user(nil).count).to be(1)
+ end
+
+ it 'returns non confidential issues for user not authorized for the issues projects' do
+ expect(Issue.visible_to_user(user).count).to be(1)
+ end
+
+ it 'returns all issues for user authorized for the issues projects' do
+ expect(Issue.visible_to_user(authorized_user).count).to be(2)
+ end
+ end
+
describe '#to_reference' do
it 'returns a String reference to the object' do
expect(subject.to_reference).to eq "##{subject.iid}"
@@ -286,4 +306,257 @@ describe Issue, models: true do
expect(user2.assigned_open_issues_count).to eq(1)
end
end
+
+ describe '#visible_to_user?' do
+ context 'with a user' do
+ let(:user) { build(:user) }
+ let(:issue) { build(:issue) }
+
+ it 'returns true when the issue is readable' do
+ expect(issue).to receive(:readable_by?).with(user).and_return(true)
+
+ expect(issue.visible_to_user?(user)).to eq(true)
+ end
+
+ it 'returns false when the issue is not readable' do
+ expect(issue).to receive(:readable_by?).with(user).and_return(false)
+
+ expect(issue.visible_to_user?(user)).to eq(false)
+ end
+ end
+
+ context 'without a user' do
+ let(:issue) { build(:issue) }
+
+ it 'returns true when the issue is publicly visible' do
+ expect(issue).to receive(:publicly_visible?).and_return(true)
+
+ expect(issue.visible_to_user?).to eq(true)
+ end
+
+ it 'returns false when the issue is not publicly visible' do
+ expect(issue).to receive(:publicly_visible?).and_return(false)
+
+ expect(issue.visible_to_user?).to eq(false)
+ end
+ end
+ end
+
+ describe '#readable_by?' do
+ describe 'with a regular user that is not a team member' do
+ let(:user) { create(:user) }
+
+ context 'using a public project' do
+ let(:project) { create(:empty_project, :public) }
+
+ it 'returns true for a regular issue' do
+ issue = build(:issue, project: project)
+
+ expect(issue).to be_readable_by(user)
+ end
+
+ it 'returns false for a confidential issue' do
+ issue = build(:issue, project: project, confidential: true)
+
+ expect(issue).not_to be_readable_by(user)
+ end
+ end
+
+ context 'using an internal project' do
+ let(:project) { create(:empty_project, :internal) }
+
+ context 'using an internal user' do
+ it 'returns true for a regular issue' do
+ issue = build(:issue, project: project)
+
+ expect(issue).to be_readable_by(user)
+ end
+
+ it 'returns false for a confidential issue' do
+ issue = build(:issue, :confidential, project: project)
+
+ expect(issue).not_to be_readable_by(user)
+ end
+ end
+
+ context 'using an external user' do
+ before do
+ allow(user).to receive(:external?).and_return(true)
+ end
+
+ it 'returns false for a regular issue' do
+ issue = build(:issue, project: project)
+
+ expect(issue).not_to be_readable_by(user)
+ end
+
+ it 'returns false for a confidential issue' do
+ issue = build(:issue, :confidential, project: project)
+
+ expect(issue).not_to be_readable_by(user)
+ end
+ end
+ end
+
+ context 'using a private project' do
+ let(:project) { create(:empty_project, :private) }
+
+ it 'returns false for a regular issue' do
+ issue = build(:issue, project: project)
+
+ expect(issue).not_to be_readable_by(user)
+ end
+
+ it 'returns false for a confidential issue' do
+ issue = build(:issue, :confidential, project: project)
+
+ expect(issue).not_to be_readable_by(user)
+ end
+
+ context 'when the user is the project owner' do
+ it 'returns true for a regular issue' do
+ issue = build(:issue, project: project)
+
+ expect(issue).not_to be_readable_by(user)
+ end
+
+ it 'returns true for a confidential issue' do
+ issue = build(:issue, :confidential, project: project)
+
+ expect(issue).not_to be_readable_by(user)
+ end
+ end
+ end
+ end
+
+ context 'with a regular user that is a team member' do
+ let(:user) { create(:user) }
+ let(:project) { create(:empty_project, :public) }
+
+ context 'using a public project' do
+ before do
+ project.team << [user, Gitlab::Access::DEVELOPER]
+ end
+
+ it 'returns true for a regular issue' do
+ issue = build(:issue, project: project)
+
+ expect(issue).to be_readable_by(user)
+ end
+
+ it 'returns true for a confidential issue' do
+ issue = build(:issue, :confidential, project: project)
+
+ expect(issue).to be_readable_by(user)
+ end
+ end
+
+ context 'using an internal project' do
+ let(:project) { create(:empty_project, :internal) }
+
+ before do
+ project.team << [user, Gitlab::Access::DEVELOPER]
+ end
+
+ it 'returns true for a regular issue' do
+ issue = build(:issue, project: project)
+
+ expect(issue).to be_readable_by(user)
+ end
+
+ it 'returns true for a confidential issue' do
+ issue = build(:issue, :confidential, project: project)
+
+ expect(issue).to be_readable_by(user)
+ end
+ end
+
+ context 'using a private project' do
+ let(:project) { create(:empty_project, :private) }
+
+ before do
+ project.team << [user, Gitlab::Access::DEVELOPER]
+ end
+
+ it 'returns true for a regular issue' do
+ issue = build(:issue, project: project)
+
+ expect(issue).to be_readable_by(user)
+ end
+
+ it 'returns true for a confidential issue' do
+ issue = build(:issue, :confidential, project: project)
+
+ expect(issue).to be_readable_by(user)
+ end
+ end
+ end
+
+ context 'with an admin user' do
+ let(:project) { create(:empty_project) }
+ let(:user) { create(:user, admin: true) }
+
+ it 'returns true for a regular issue' do
+ issue = build(:issue, project: project)
+
+ expect(issue).to be_readable_by(user)
+ end
+
+ it 'returns true for a confidential issue' do
+ issue = build(:issue, :confidential, project: project)
+
+ expect(issue).to be_readable_by(user)
+ end
+ end
+ end
+
+ describe '#publicly_visible?' do
+ context 'using a public project' do
+ let(:project) { create(:empty_project, :public) }
+
+ it 'returns true for a regular issue' do
+ issue = build(:issue, project: project)
+
+ expect(issue).to be_publicly_visible
+ end
+
+ it 'returns false for a confidential issue' do
+ issue = build(:issue, :confidential, project: project)
+
+ expect(issue).not_to be_publicly_visible
+ end
+ end
+
+ context 'using an internal project' do
+ let(:project) { create(:empty_project, :internal) }
+
+ it 'returns false for a regular issue' do
+ issue = build(:issue, project: project)
+
+ expect(issue).not_to be_publicly_visible
+ end
+
+ it 'returns false for a confidential issue' do
+ issue = build(:issue, :confidential, project: project)
+
+ expect(issue).not_to be_publicly_visible
+ end
+ end
+
+ context 'using a private project' do
+ let(:project) { create(:empty_project, :private) }
+
+ it 'returns false for a regular issue' do
+ issue = build(:issue, project: project)
+
+ expect(issue).not_to be_publicly_visible
+ end
+
+ it 'returns false for a confidential issue' do
+ issue = build(:issue, :confidential, project: project)
+
+ expect(issue).not_to be_publicly_visible
+ end
+ end
+ end
end
diff --git a/spec/models/key_spec.rb b/spec/models/key_spec.rb
index 49cf3d8633a..fd4a2beff58 100644
--- a/spec/models/key_spec.rb
+++ b/spec/models/key_spec.rb
@@ -16,12 +16,13 @@ describe Key, models: true do
end
describe "Methods" do
+ let(:user) { create(:user) }
it { is_expected.to respond_to :projects }
it { is_expected.to respond_to :publishable_key }
describe "#publishable_keys" do
- it 'strips all personal information' do
- expect(build(:key).publishable_key).not_to match(/dummy@gitlab/)
+ it 'replaces SSH key comment with simple identifier of username + hostname' do
+ expect(build(:key, user: user).publishable_key).to include("#{user.name} (localhost)")
end
end
end
@@ -72,13 +73,13 @@ describe Key, models: true do
end
context 'callbacks' do
- it 'should add new key to authorized_file' do
+ it 'adds new key to authorized_file' do
@key = build(:personal_key, id: 7)
expect(GitlabShellWorker).to receive(:perform_async).with(:add_key, @key.shell_id, @key.key)
@key.save
end
- it 'should remove key from authorized_file' do
+ it 'removes key from authorized_file' do
@key = create(:personal_key)
expect(GitlabShellWorker).to receive(:perform_async).with(:remove_key, @key.shell_id, @key.key)
@key.destroy
diff --git a/spec/models/label_spec.rb b/spec/models/label_spec.rb
index f37f44a608e..5a5d1a5d60c 100644
--- a/spec/models/label_spec.rb
+++ b/spec/models/label_spec.rb
@@ -5,8 +5,10 @@ describe Label, models: true do
describe 'associations' do
it { is_expected.to belong_to(:project) }
+
it { is_expected.to have_many(:label_links).dependent(:destroy) }
it { is_expected.to have_many(:issues).through(:label_links).source(:target) }
+ it { is_expected.to have_many(:lists).dependent(:destroy) }
end
describe 'modules' do
@@ -18,7 +20,7 @@ describe Label, models: true do
describe 'validation' do
it { is_expected.to validate_presence_of(:project) }
- it 'should validate color code' do
+ it 'validates color code' do
expect(label).not_to allow_value('G-ITLAB').for(:color)
expect(label).not_to allow_value('AABBCC').for(:color)
expect(label).not_to allow_value('#AABBCCEE').for(:color)
@@ -30,7 +32,7 @@ describe Label, models: true do
expect(label).to allow_value('#abcdef').for(:color)
end
- it 'should validate title' do
+ it 'validates title' do
expect(label).not_to allow_value('G,ITLAB').for(:title)
expect(label).not_to allow_value('').for(:title)
diff --git a/spec/models/legacy_diff_note_spec.rb b/spec/models/legacy_diff_note_spec.rb
index d64d89edbd3..81517a18b74 100644
--- a/spec/models/legacy_diff_note_spec.rb
+++ b/spec/models/legacy_diff_note_spec.rb
@@ -5,21 +5,21 @@ describe LegacyDiffNote, models: true do
let!(:note) { create(:legacy_diff_note_on_commit, note: "+1 from me") }
let!(:commit) { note.noteable }
- it "should save a valid note" do
+ it "saves a valid note" do
expect(note.commit_id).to eq(commit.id)
expect(note.noteable.id).to eq(commit.id)
end
- it "should be recognized by #legacy_diff_note?" do
+ it "is recognized by #legacy_diff_note?" do
expect(note).to be_legacy_diff_note
end
end
describe '#active?' do
- it 'is always true when the note has no associated diff' do
+ it 'is always true when the note has no associated diff line' do
note = build(:legacy_diff_note_on_merge_request)
- expect(note).to receive(:diff).and_return(nil)
+ expect(note).to receive(:diff_line).and_return(nil)
expect(note).to be_active
end
@@ -27,7 +27,7 @@ describe LegacyDiffNote, models: true do
it 'is never true when the note has no noteable associated' do
note = build(:legacy_diff_note_on_merge_request)
- expect(note).to receive(:diff).and_return(double)
+ expect(note).to receive(:diff_line).and_return(double)
expect(note).to receive(:noteable).and_return(nil)
expect(note).not_to be_active
@@ -47,7 +47,7 @@ describe LegacyDiffNote, models: true do
merge = build_stubbed(:merge_request, :simple)
note = build(:legacy_diff_note_on_merge_request, noteable: merge)
- allow(note).to receive(:diff).and_return(double)
+ allow(note).to receive(:diff_line).and_return(double)
expect(note).to receive(:find_noteable_diff).and_return(nil)
expect(note).not_to be_active
@@ -58,7 +58,7 @@ describe LegacyDiffNote, models: true do
# Generate a real line_code value so we know it will match. We use a
# random line from a random diff just for funsies.
- diff = merge.diffs.to_a.sample
+ diff = merge.raw_diffs.to_a.sample
line = Gitlab::Diff::Parser.new.parse(diff.diff.each_line).to_a.sample
code = Gitlab::Diff::LineCode.generate(diff.new_path, line.new_pos, line.old_pos)
@@ -73,4 +73,29 @@ describe LegacyDiffNote, models: true do
end
end
end
+
+ describe "#discussion_id" do
+ let(:note) { create(:note) }
+
+ context "when it is newly created" do
+ it "has a discussion id" do
+ expect(note.discussion_id).not_to be_nil
+ expect(note.discussion_id).to match(/\A\h{40}\z/)
+ end
+ end
+
+ context "when it didn't store a discussion id before" do
+ before do
+ note.update_column(:discussion_id, nil)
+ end
+
+ it "has a discussion id" do
+ # The discussion_id is set in `after_initialize`, so `reload` won't work
+ reloaded_note = Note.find(note.id)
+
+ expect(reloaded_note.discussion_id).not_to be_nil
+ expect(reloaded_note.discussion_id).to match(/\A\h{40}\z/)
+ end
+ end
+ end
end
diff --git a/spec/models/list_spec.rb b/spec/models/list_spec.rb
new file mode 100644
index 00000000000..9e1a52011c3
--- /dev/null
+++ b/spec/models/list_spec.rb
@@ -0,0 +1,117 @@
+require 'rails_helper'
+
+describe List do
+ describe 'relationships' do
+ it { is_expected.to belong_to(:board) }
+ it { is_expected.to belong_to(:label) }
+ end
+
+ describe 'validations' do
+ it { is_expected.to validate_presence_of(:board) }
+ it { is_expected.to validate_presence_of(:label) }
+ it { is_expected.to validate_presence_of(:list_type) }
+ it { is_expected.to validate_presence_of(:position) }
+ it { is_expected.to validate_numericality_of(:position).only_integer.is_greater_than_or_equal_to(0) }
+
+ it 'validates uniqueness of label scoped to board_id' do
+ create(:list)
+
+ expect(subject).to validate_uniqueness_of(:label_id).scoped_to(:board_id)
+ end
+
+ context 'when list_type is set to backlog' do
+ subject { described_class.new(list_type: :backlog) }
+
+ it { is_expected.not_to validate_presence_of(:label) }
+ it { is_expected.not_to validate_presence_of(:position) }
+ end
+
+ context 'when list_type is set to done' do
+ subject { described_class.new(list_type: :done) }
+
+ it { is_expected.not_to validate_presence_of(:label) }
+ it { is_expected.not_to validate_presence_of(:position) }
+ end
+ end
+
+ describe '#destroy' do
+ it 'can be destroyed when when list_type is set to label' do
+ subject = create(:list)
+
+ expect(subject.destroy).to be_truthy
+ end
+
+ it 'can not be destroyed when list_type is set to backlog' do
+ subject = create(:backlog_list)
+
+ expect(subject.destroy).to be_falsey
+ end
+
+ it 'can not be destroyed when when list_type is set to done' do
+ subject = create(:done_list)
+
+ expect(subject.destroy).to be_falsey
+ end
+ end
+
+ describe '#destroyable?' do
+ it 'retruns true when list_type is set to label' do
+ subject.list_type = :label
+
+ expect(subject).to be_destroyable
+ end
+
+ it 'retruns false when list_type is set to backlog' do
+ subject.list_type = :backlog
+
+ expect(subject).not_to be_destroyable
+ end
+
+ it 'retruns false when list_type is set to done' do
+ subject.list_type = :done
+
+ expect(subject).not_to be_destroyable
+ end
+ end
+
+ describe '#movable?' do
+ it 'retruns true when list_type is set to label' do
+ subject.list_type = :label
+
+ expect(subject).to be_movable
+ end
+
+ it 'retruns false when list_type is set to backlog' do
+ subject.list_type = :backlog
+
+ expect(subject).not_to be_movable
+ end
+
+ it 'retruns false when list_type is set to done' do
+ subject.list_type = :done
+
+ expect(subject).not_to be_movable
+ end
+ end
+
+ describe '#title' do
+ it 'returns label name when list_type is set to label' do
+ subject.list_type = :label
+ subject.label = Label.new(name: 'Development')
+
+ expect(subject.title).to eq 'Development'
+ end
+
+ it 'returns Backlog when list_type is set to backlog' do
+ subject.list_type = :backlog
+
+ expect(subject.title).to eq 'Backlog'
+ end
+
+ it 'returns Done when list_type is set to done' do
+ subject.list_type = :done
+
+ expect(subject.title).to eq 'Done'
+ end
+ end
+end
diff --git a/spec/models/member_spec.rb b/spec/models/member_spec.rb
index 40181a8b906..fef90d9b5cb 100644
--- a/spec/models/member_spec.rb
+++ b/spec/models/member_spec.rb
@@ -10,7 +10,7 @@ describe Member, models: true do
it { is_expected.to validate_presence_of(:user) }
it { is_expected.to validate_presence_of(:source) }
- it { is_expected.to validate_inclusion_of(:access_level).in_array(Gitlab::Access.values) }
+ it { is_expected.to validate_inclusion_of(:access_level).in_array(Gitlab::Access.all_values) }
it_behaves_like 'an object with email-formated attributes', :invite_email do
subject { build(:project_member) }
@@ -65,11 +65,21 @@ describe Member, models: true do
@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)
+ Member.add_user(
+ project.members,
+ 'toto1@example.com',
+ Gitlab::Access::DEVELOPER,
+ current_user: @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)
+ Member.add_user(
+ project.members,
+ 'toto2@example.com',
+ Gitlab::Access::DEVELOPER,
+ current_user: @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) }
@@ -79,6 +89,18 @@ describe Member, models: true do
@accepted_request_member = project.requesters.find_by(user_id: accepted_request_user.id).tap { |m| m.accept_request }
end
+ describe '.access_for_user_ids' do
+ it 'returns the right access levels' do
+ users = [@owner_user.id, @master_user.id]
+ expected = {
+ @owner_user.id => Gitlab::Access::OWNER,
+ @master_user.id => Gitlab::Access::MASTER
+ }
+
+ expect(described_class.access_for_user_ids(users)).to eq(expected)
+ end
+ end
+
describe '.invite' do
it { expect(described_class.invite).not_to include @master }
it { expect(described_class.invite).to include @invited_member }
diff --git a/spec/models/members/group_member_spec.rb b/spec/models/members/group_member_spec.rb
index 18439cac2a4..4f875fd257a 100644
--- a/spec/models/members/group_member_spec.rb
+++ b/spec/models/members/group_member_spec.rb
@@ -22,7 +22,7 @@ require 'spec_helper'
describe GroupMember, models: true do
describe 'notifications' do
describe "#after_create" do
- it "should send email to user" do
+ it "sends email to user" do
membership = build(:group_member)
allow(membership).to receive(:notification_service).
@@ -40,7 +40,7 @@ describe GroupMember, models: true do
and_return(double('NotificationService').as_null_object)
end
- it "should send email to user" do
+ it "sends email to user" do
expect(@group_member).to receive(:notification_service)
@group_member.update_attribute(:access_level, GroupMember::MASTER)
end
diff --git a/spec/models/members/project_member_spec.rb b/spec/models/members/project_member_spec.rb
index 4c103462433..913d74645a7 100644
--- a/spec/models/members/project_member_spec.rb
+++ b/spec/models/members/project_member_spec.rb
@@ -27,6 +27,7 @@ describe ProjectMember, models: true do
describe 'validations' do
it { is_expected.to allow_value('Project').for(:source_type) }
it { is_expected.not_to allow_value('project').for(:source_type) }
+ it { is_expected.to validate_inclusion_of(:access_level).in_array(Gitlab::Access.values) }
end
describe 'modules' do
@@ -40,7 +41,7 @@ describe ProjectMember, models: true do
end
describe "#destroy" do
- let(:owner) { create(:project_member, access_level: ProjectMember::OWNER) }
+ let(:owner) { create(:project_member, access_level: ProjectMember::MASTER) }
let(:project) { owner.project }
let(:master) { create(:project_member, project: project) }
@@ -52,7 +53,7 @@ describe ProjectMember, models: true do
master_todos
end
- it "destroy itself and delete associated todos" do
+ it "destroys 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)
@@ -101,7 +102,7 @@ describe ProjectMember, models: true do
end
end
- describe :add_users_into_projects do
+ describe '.add_users_to_projects' do
before do
@project_1 = create :project
@project_2 = create :project
@@ -109,7 +110,7 @@ describe ProjectMember, models: true do
@user_1 = create :user
@user_2 = create :user
- ProjectMember.add_users_into_projects(
+ ProjectMember.add_users_to_projects(
[@project_1.id, @project_2.id],
[@user_1.id, @user_2.id],
ProjectMember::MASTER
@@ -123,7 +124,7 @@ describe ProjectMember, models: true do
it { expect(@project_2.users).to include(@user_2) }
end
- describe :truncate_teams do
+ describe '.truncate_teams' do
before do
@project_1 = create :project
@project_2 = create :project
diff --git a/spec/models/merge_request_diff_spec.rb b/spec/models/merge_request_diff_spec.rb
new file mode 100644
index 00000000000..e5b185dc3f6
--- /dev/null
+++ b/spec/models/merge_request_diff_spec.rb
@@ -0,0 +1,68 @@
+require 'spec_helper'
+
+describe MergeRequestDiff, models: true do
+ describe 'create new record' do
+ subject { create(:merge_request).merge_request_diff }
+
+ it { expect(subject).to be_valid }
+ it { expect(subject).to be_persisted }
+ it { expect(subject.commits.count).to eq(5) }
+ it { expect(subject.diffs.count).to eq(8) }
+ it { expect(subject.head_commit_sha).to eq('5937ac0a7beb003549fc5fd26fc247adbce4a52e') }
+ it { expect(subject.base_commit_sha).to eq('ae73cb07c9eeaf35924a10f713b364d32b2dd34f') }
+ it { expect(subject.start_commit_sha).to eq('0b4bc9a49b562e85de7cc9e834518ea6828729b9') }
+ end
+
+ describe '#latest' do
+ let!(:mr) { create(:merge_request, :with_diffs) }
+ let!(:first_diff) { mr.merge_request_diff }
+ let!(:last_diff) { mr.create_merge_request_diff }
+
+ it { expect(last_diff.latest?).to be_truthy }
+ it { expect(first_diff.latest?).to be_falsey }
+ end
+
+ describe '#diffs' do
+ let(:mr) { create(:merge_request, :with_diffs) }
+ let(:mr_diff) { mr.merge_request_diff }
+
+ context 'when the :ignore_whitespace_change option is set' do
+ it 'creates a new compare object instead of loading from the DB' do
+ expect(mr_diff).not_to receive(:load_diffs)
+ expect(Gitlab::Git::Compare).to receive(:new).and_call_original
+
+ mr_diff.raw_diffs(ignore_whitespace_change: true)
+ end
+ end
+
+ context 'when the raw diffs are empty' do
+ before { mr_diff.update_attributes(st_diffs: '') }
+
+ it 'returns an empty DiffCollection' do
+ expect(mr_diff.raw_diffs).to be_a(Gitlab::Git::DiffCollection)
+ expect(mr_diff.raw_diffs).to be_empty
+ end
+ end
+
+ context 'when the raw diffs exist' do
+ it 'returns the diffs' do
+ expect(mr_diff.raw_diffs).to be_a(Gitlab::Git::DiffCollection)
+ expect(mr_diff.raw_diffs).not_to be_empty
+ end
+
+ context 'when the :paths option is set' do
+ let(:diffs) { mr_diff.raw_diffs(paths: ['files/ruby/popen.rb', 'files/ruby/popen.rb']) }
+
+ it 'only returns diffs that match the (old path, new path) given' do
+ expect(diffs.map(&:new_path)).to contain_exactly('files/ruby/popen.rb')
+ end
+
+ it 'uses the diffs from the DB' do
+ expect(mr_diff).to receive(:load_diffs)
+
+ diffs
+ end
+ end
+ end
+ end
+end
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index a4b6ff8f8ad..d67f71bbb9c 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -9,7 +9,7 @@ describe MergeRequest, models: true do
it { is_expected.to belong_to(:target_project).with_foreign_key(:target_project_id).class_name('Project') }
it { is_expected.to belong_to(:source_project).with_foreign_key(:source_project_id).class_name('Project') }
it { is_expected.to belong_to(:merge_user).class_name("User") }
- it { is_expected.to have_one(:merge_request_diff).dependent(:destroy) }
+ it { is_expected.to have_many(:merge_request_diffs).dependent(:destroy) }
end
describe 'modules' do
@@ -65,11 +65,11 @@ describe MergeRequest, models: true do
end
describe '#target_branch_sha' do
- context 'when the target branch does not exist anymore' do
- let(:project) { create(:project) }
+ let(:project) { create(:project) }
- subject { create(:merge_request, source_project: project, target_project: project) }
+ subject { create(:merge_request, source_project: project, target_project: project) }
+ context 'when the target branch does not exist' do
before do
project.repository.raw_repository.delete_branch(subject.target_branch)
end
@@ -78,6 +78,12 @@ describe MergeRequest, models: true do
expect(subject.target_branch_sha).to be_nil
end
end
+
+ it 'returns memoized value' do
+ subject.target_branch_sha = '8ffb3c15a5475e59ae909384297fede4badcb4c7'
+
+ expect(subject.target_branch_sha).to eq '8ffb3c15a5475e59ae909384297fede4badcb4c7'
+ end
end
describe '#source_branch_sha' do
@@ -103,6 +109,12 @@ describe MergeRequest, models: true do
expect(subject.source_branch_sha).to be_nil
end
end
+
+ it 'returns memoized value' do
+ subject.source_branch_sha = '2e5d3239642f9161dcbbc4b70a211a68e5e45e2b'
+
+ expect(subject.source_branch_sha).to eq '2e5d3239642f9161dcbbc4b70a211a68e5e45e2b'
+ end
end
describe '#to_reference' do
@@ -116,6 +128,56 @@ describe MergeRequest, models: true do
end
end
+ describe '#raw_diffs' do
+ let(:merge_request) { build(:merge_request) }
+ let(:options) { { paths: ['a/b', 'b/a', 'c/*'] } }
+
+ context 'when there are MR diffs' do
+ it 'delegates to the MR diffs' do
+ merge_request.merge_request_diff = MergeRequestDiff.new
+
+ expect(merge_request.merge_request_diff).to receive(:raw_diffs).with(options)
+
+ merge_request.raw_diffs(options)
+ end
+ end
+
+ context 'when there are no MR diffs' do
+ it 'delegates to the compare object' do
+ merge_request.compare = double(:compare)
+
+ expect(merge_request.compare).to receive(:raw_diffs).with(options)
+
+ merge_request.raw_diffs(options)
+ end
+ end
+ end
+
+ describe '#diffs' do
+ let(:merge_request) { build(:merge_request) }
+ let(:options) { { paths: ['a/b', 'b/a', 'c/*'] } }
+
+ context 'when there are MR diffs' do
+ it 'delegates to the MR diffs' do
+ merge_request.save
+
+ expect(merge_request.merge_request_diff).to receive(:raw_diffs).with(hash_including(options))
+
+ merge_request.diffs(options)
+ end
+ end
+
+ context 'when there are no MR diffs' do
+ it 'delegates to the compare object' do
+ merge_request.compare = double(:compare)
+
+ expect(merge_request.compare).to receive(:diffs).with(options)
+
+ merge_request.diffs(options)
+ end
+ end
+ end
+
describe "#mr_and_commit_notes" do
let!(:merge_request) { create(:merge_request) }
@@ -126,12 +188,12 @@ describe MergeRequest, models: true do
create(:note, noteable: merge_request, project: merge_request.project)
end
- it "should include notes for commits" do
+ it "includes notes for commits" do
expect(merge_request.commits).not_to be_empty
expect(merge_request.mr_and_commit_notes.count).to eq(2)
end
- it "should include notes for commits from target project as well" do
+ it "includes notes for commits from target project as well" do
create(:note_on_commit, commit_id: merge_request.commits.first.id,
project: merge_request.target_project)
@@ -242,7 +304,7 @@ describe MergeRequest, models: true do
expect(subject.can_remove_source_branch?(user)).to be_falsey
end
- it "cant remove a root ref" do
+ it "can't remove a root ref" do
subject.source_branch = "master"
subject.target_branch = "feature"
@@ -254,7 +316,7 @@ describe MergeRequest, models: true do
end
it "can be removed if the last commit is the head of the source branch" do
- allow(subject.source_project).to receive(:commit).and_return(subject.diff_head_commit)
+ allow(subject).to receive(:source_branch_head).and_return(subject.diff_head_commit)
expect(subject.can_remove_source_branch?(user)).to be_truthy
end
@@ -394,6 +456,20 @@ describe MergeRequest, models: true do
subject { create :merge_request, :simple }
end
+ describe '#commits_sha' do
+ let(:commit0) { double('commit0', sha: 'sha1') }
+ let(:commit1) { double('commit1', sha: 'sha2') }
+ let(:commit2) { double('commit2', sha: 'sha3') }
+
+ before do
+ allow(subject.merge_request_diff).to receive(:commits).and_return([commit0, commit1, commit2])
+ end
+
+ it 'returns sha of commits' do
+ expect(subject.commits_sha).to contain_exactly('sha1', 'sha2', 'sha3')
+ end
+ end
+
describe '#pipeline' do
describe 'when the source project exists' do
it 'returns the latest pipeline' do
@@ -418,6 +494,19 @@ describe MergeRequest, models: true do
end
end
+ describe '#all_pipelines' do
+ let!(:pipelines) do
+ subject.merge_request_diff.commits.map do |commit|
+ create(:ci_empty_pipeline, project: subject.source_project, sha: commit.id, ref: subject.source_branch)
+ end
+ end
+
+ it 'returns a pipelines from source projects with proper ordering' do
+ expect(subject.all_pipelines).not_to be_empty
+ expect(subject.all_pipelines).to eq(pipelines.reverse)
+ end
+ end
+
describe '#participants' do
let(:project) { create(:project, :public) }
@@ -612,13 +701,37 @@ describe MergeRequest, models: true do
end
end
+ describe "#environments" do
+ let(:project) { create(:project) }
+ let!(:environment) { create(:environment, project: project) }
+ let!(:environment1) { create(:environment, project: project) }
+ let!(:environment2) { create(:environment, project: project) }
+ let(:merge_request) { create(:merge_request, source_project: project) }
+
+ it 'selects deployed environments' do
+ create(:deployment, environment: environment, sha: project.commit('master').id)
+ create(:deployment, environment: environment1, sha: project.commit('feature').id)
+
+ expect(merge_request.environments).to eq [environment]
+ end
+ end
+
describe "#reload_diff" do
let(:note) { create(:diff_note_on_merge_request, project: subject.project, noteable: subject) }
let(:commit) { subject.project.commit(sample_commit.id) }
- it "reloads the diff content" do
- expect(subject.merge_request_diff).to receive(:reload_content)
+ it "does not change existing merge request diff" do
+ expect(subject.merge_request_diff).not_to receive(:save_git_content)
+ subject.reload_diff
+ end
+
+ it "creates new merge request diff" do
+ expect { subject.reload_diff }.to change { subject.merge_request_diffs.count }.by(1)
+ end
+
+ it "executs diff cache service" do
+ expect_any_instance_of(MergeRequests::MergeRequestDiffCacheService).to receive(:execute).with(subject)
subject.reload_diff
end
@@ -626,13 +739,15 @@ describe MergeRequest, models: true do
it "updates diff note positions" do
old_diff_refs = subject.diff_refs
- merge_request_diff = subject.merge_request_diff
-
# Update merge_request_diff so that #diff_refs will return commit.diff_refs
- allow(merge_request_diff).to receive(:reload_content) do
- merge_request_diff.base_commit_sha = commit.parent_id
- merge_request_diff.start_commit_sha = commit.parent_id
- merge_request_diff.head_commit_sha = commit.sha
+ allow(subject).to receive(:create_merge_request_diff) do
+ subject.merge_request_diffs.create(
+ base_commit_sha: commit.parent_id,
+ start_commit_sha: commit.parent_id,
+ head_commit_sha: commit.sha
+ )
+
+ subject.merge_request_diff(true)
end
expect(Notes::DiffPositionUpdateService).to receive(:new).with(
@@ -642,11 +757,209 @@ describe MergeRequest, models: true do
new_diff_refs: commit.diff_refs,
paths: note.position.paths
).and_call_original
- expect_any_instance_of(Notes::DiffPositionUpdateService).to receive(:execute).with(note)
+ expect_any_instance_of(Notes::DiffPositionUpdateService).to receive(:execute).with(note)
expect_any_instance_of(DiffNote).to receive(:save).once
subject.reload_diff
end
end
+
+ describe '#branch_merge_base_commit' do
+ context 'source and target branch exist' do
+ it { expect(subject.branch_merge_base_commit.sha).to eq('ae73cb07c9eeaf35924a10f713b364d32b2dd34f') }
+ it { expect(subject.branch_merge_base_commit).to be_a(Commit) }
+ end
+
+ context 'when the target branch does not exist' do
+ before do
+ subject.project.repository.raw_repository.delete_branch(subject.target_branch)
+ end
+
+ it 'returns nil' do
+ expect(subject.branch_merge_base_commit).to be_nil
+ end
+ end
+ end
+
+ describe "#diff_sha_refs" do
+ context "with diffs" do
+ subject { create(:merge_request, :with_diffs) }
+
+ it "does not touch the repository" do
+ subject # Instantiate the object
+
+ expect_any_instance_of(Repository).not_to receive(:commit)
+
+ subject.diff_sha_refs
+ end
+
+ it "returns expected diff_refs" do
+ expected_diff_refs = Gitlab::Diff::DiffRefs.new(
+ base_sha: subject.merge_request_diff.base_commit_sha,
+ start_sha: subject.merge_request_diff.start_commit_sha,
+ head_sha: subject.merge_request_diff.head_commit_sha
+ )
+
+ expect(subject.diff_sha_refs).to eq(expected_diff_refs)
+ end
+ end
+ end
+
+ context "discussion status" do
+ let(:first_discussion) { Discussion.new([create(:diff_note_on_merge_request)]) }
+ let(:second_discussion) { Discussion.new([create(:diff_note_on_merge_request)]) }
+ let(:third_discussion) { Discussion.new([create(:diff_note_on_merge_request)]) }
+
+ before do
+ allow(subject).to receive(:diff_discussions).and_return([first_discussion, second_discussion, third_discussion])
+ end
+
+ describe "#discussions_resolvable?" do
+ context "when all discussions are unresolvable" do
+ before do
+ allow(first_discussion).to receive(:resolvable?).and_return(false)
+ allow(second_discussion).to receive(:resolvable?).and_return(false)
+ allow(third_discussion).to receive(:resolvable?).and_return(false)
+ end
+
+ it "returns false" do
+ expect(subject.discussions_resolvable?).to be false
+ end
+ end
+
+ context "when some discussions are unresolvable and some discussions are resolvable" do
+ before do
+ allow(first_discussion).to receive(:resolvable?).and_return(true)
+ allow(second_discussion).to receive(:resolvable?).and_return(false)
+ allow(third_discussion).to receive(:resolvable?).and_return(true)
+ end
+
+ it "returns true" do
+ expect(subject.discussions_resolvable?).to be true
+ end
+ end
+
+ context "when all discussions are resolvable" do
+ before do
+ allow(first_discussion).to receive(:resolvable?).and_return(true)
+ allow(second_discussion).to receive(:resolvable?).and_return(true)
+ allow(third_discussion).to receive(:resolvable?).and_return(true)
+ end
+
+ it "returns true" do
+ expect(subject.discussions_resolvable?).to be true
+ end
+ end
+ end
+
+ describe "#discussions_resolved?" do
+ context "when discussions are not resolvable" do
+ before do
+ allow(subject).to receive(:discussions_resolvable?).and_return(false)
+ end
+
+ it "returns false" do
+ expect(subject.discussions_resolved?).to be false
+ end
+ end
+
+ context "when discussions are resolvable" do
+ before do
+ allow(subject).to receive(:discussions_resolvable?).and_return(true)
+
+ allow(first_discussion).to receive(:resolvable?).and_return(true)
+ allow(second_discussion).to receive(:resolvable?).and_return(false)
+ allow(third_discussion).to receive(:resolvable?).and_return(true)
+ end
+
+ context "when all resolvable discussions are resolved" do
+ before do
+ allow(first_discussion).to receive(:resolved?).and_return(true)
+ allow(third_discussion).to receive(:resolved?).and_return(true)
+ end
+
+ it "returns true" do
+ expect(subject.discussions_resolved?).to be true
+ end
+ end
+
+ context "when some resolvable discussions are not resolved" do
+ before do
+ allow(first_discussion).to receive(:resolved?).and_return(true)
+ allow(third_discussion).to receive(:resolved?).and_return(false)
+ end
+
+ it "returns false" do
+ expect(subject.discussions_resolved?).to be false
+ end
+ end
+ end
+ end
+ end
+
+ describe '#conflicts_can_be_resolved_in_ui?' do
+ def create_merge_request(source_branch)
+ create(:merge_request, source_branch: source_branch, target_branch: 'conflict-start') do |mr|
+ mr.mark_as_unmergeable
+ end
+ end
+
+ it 'returns a falsey value when the MR can be merged without conflicts' do
+ merge_request = create_merge_request('master')
+ merge_request.mark_as_mergeable
+
+ expect(merge_request.conflicts_can_be_resolved_in_ui?).to be_falsey
+ end
+
+ it 'returns a falsey value when the MR is marked as having conflicts, but has none' do
+ merge_request = create_merge_request('master')
+
+ expect(merge_request.conflicts_can_be_resolved_in_ui?).to be_falsey
+ end
+
+ it 'returns a falsey value when the MR has a missing ref after a force push' do
+ merge_request = create_merge_request('conflict-resolvable')
+ allow(merge_request.conflicts).to receive(:merge_index).and_raise(Rugged::OdbError)
+
+ expect(merge_request.conflicts_can_be_resolved_in_ui?).to be_falsey
+ end
+
+ it 'returns a falsey value when the MR does not support new diff notes' do
+ merge_request = create_merge_request('conflict-resolvable')
+ merge_request.merge_request_diff.update_attributes(start_commit_sha: nil)
+
+ expect(merge_request.conflicts_can_be_resolved_in_ui?).to be_falsey
+ end
+
+ it 'returns a falsey value when the conflicts contain a large file' do
+ merge_request = create_merge_request('conflict-too-large')
+
+ expect(merge_request.conflicts_can_be_resolved_in_ui?).to be_falsey
+ end
+
+ it 'returns a falsey value when the conflicts contain a binary file' do
+ merge_request = create_merge_request('conflict-binary-file')
+
+ expect(merge_request.conflicts_can_be_resolved_in_ui?).to be_falsey
+ end
+
+ it 'returns a falsey value when the conflicts contain a file with ambiguous conflict markers' do
+ merge_request = create_merge_request('conflict-contains-conflict-markers')
+
+ expect(merge_request.conflicts_can_be_resolved_in_ui?).to be_falsey
+ end
+
+ it 'returns a falsey value when the conflicts contain a file edited in one branch and deleted in another' do
+ merge_request = create_merge_request('conflict-missing-side')
+
+ expect(merge_request.conflicts_can_be_resolved_in_ui?).to be_falsey
+ end
+
+ it 'returns a truthy value when the conflicts are resolvable in the UI' do
+ merge_request = create_merge_request('conflict-resolvable')
+
+ expect(merge_request.conflicts_can_be_resolved_in_ui?).to be_truthy
+ end
+ end
end
diff --git a/spec/models/milestone_spec.rb b/spec/models/milestone_spec.rb
index 1e18c788b50..d64d6cde2b5 100644
--- a/spec/models/milestone_spec.rb
+++ b/spec/models/milestone_spec.rb
@@ -28,12 +28,12 @@ describe Milestone, models: true do
end
describe "unique milestone title per project" do
- it "shouldn't accept the same title in a project twice" do
+ it "does not accept the same title in a project twice" do
new_milestone = Milestone.new(project: milestone.project, title: milestone.title)
expect(new_milestone).not_to be_valid
end
- it "should accept the same title in another project" do
+ it "accepts the same title in another project" do
project = build(:project)
new_milestone = Milestone.new(project: project, title: milestone.title)
@@ -42,35 +42,35 @@ describe Milestone, models: true do
end
describe "#percent_complete" do
- it "should not count open issues" do
+ it "does not count open issues" do
milestone.issues << issue
expect(milestone.percent_complete(user)).to eq(0)
end
- it "should count closed issues" do
+ it "counts closed issues" do
issue.close
milestone.issues << issue
expect(milestone.percent_complete(user)).to eq(100)
end
- it "should recover from dividing by zero" do
+ it "recovers from dividing by zero" do
expect(milestone.percent_complete(user)).to eq(0)
end
end
describe "#expires_at" do
- it "should be nil when due_date is unset" do
+ it "is nil when due_date is unset" do
milestone.update_attributes(due_date: nil)
expect(milestone.expires_at).to be_nil
end
- it "should not be nil when due_date is set" do
+ it "is not nil when due_date is set" do
milestone.update_attributes(due_date: Date.tomorrow)
expect(milestone.expires_at).to be_present
end
end
- describe :expired? do
+ describe '#expired?' do
context "expired" do
before do
allow(milestone).to receive(:due_date).and_return(Date.today.prev_year)
@@ -88,7 +88,7 @@ describe Milestone, models: true do
end
end
- describe :percent_complete do
+ describe '#percent_complete' do
before do
allow(milestone).to receive_messages(
closed_items_count: 3,
@@ -111,22 +111,22 @@ describe Milestone, models: true do
it { expect(milestone.is_empty?(user)).to be_falsey }
end
- describe :can_be_closed? do
+ describe '#can_be_closed?' do
it { expect(milestone.can_be_closed?).to be_truthy }
end
- describe :total_items_count do
+ describe '#total_items_count' do
before do
create :closed_issue, milestone: milestone
create :merge_request, milestone: milestone
end
- it 'Should return total count of issues and merge requests assigned to milestone' do
+ it 'returns total count of issues and merge requests assigned to milestone' do
expect(milestone.total_items_count(user)).to eq 2
end
end
- describe :can_be_closed? do
+ describe '#can_be_closed?' do
before do
milestone = create :milestone
create :closed_issue, milestone: milestone
@@ -134,11 +134,11 @@ describe Milestone, models: true do
create :issue
end
- it 'should be true if milestone active and all nested issues closed' do
+ it 'returns true if milestone active and all nested issues closed' do
expect(milestone.can_be_closed?).to be_truthy
end
- it 'should be false if milestone active and not all nested issues closed' do
+ it 'returns false if milestone active and not all nested issues closed' do
issue.milestone = milestone
issue.save
diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb
index 5f68cd2b066..544920d1824 100644
--- a/spec/models/namespace_spec.rb
+++ b/spec/models/namespace_spec.rb
@@ -18,11 +18,11 @@ describe Namespace, models: true do
it { is_expected.to respond_to(:to_param) }
end
- describe :to_param do
+ describe '#to_param' do
it { expect(namespace.to_param).to eq(namespace.path) }
end
- describe :human_name do
+ describe '#human_name' do
it { expect(namespace.human_name).to eq(namespace.owner_name) }
end
@@ -54,18 +54,18 @@ describe Namespace, models: true do
end
end
- describe :move_dir do
+ describe '#move_dir' do
before do
@namespace = create :namespace
@project = create :project, namespace: @namespace
allow(@namespace).to receive(:path_changed?).and_return(true)
end
- it "should raise error when directory exists" do
+ it "raises error when directory exists" do
expect { @namespace.move_dir }.to raise_error("namespace directory cannot be moved")
end
- it "should move dir if path changed" do
+ it "moves dir if path changed" do
new_path = @namespace.path + "_new"
allow(@namespace).to receive(:path_was).and_return(@namespace.path)
allow(@namespace).to receive(:path).and_return(new_path)
@@ -93,12 +93,12 @@ describe Namespace, models: true do
before { namespace.destroy }
- it "should remove its dirs when deleted" do
+ it "removes its dirs when deleted" do
expect(File.exist?(path)).to be(false)
end
end
- describe :find_by_path_or_name do
+ describe '.find_by_path_or_name' do
before do
@namespace = create(:namespace, name: 'WoW', path: 'woW')
end
diff --git a/spec/models/network/graph_spec.rb b/spec/models/network/graph_spec.rb
new file mode 100644
index 00000000000..b76513d2a3c
--- /dev/null
+++ b/spec/models/network/graph_spec.rb
@@ -0,0 +1,12 @@
+require 'spec_helper'
+
+describe Network::Graph, models: true do
+ let(:project) { create(:project) }
+ let!(:note_on_commit) { create(:note_on_commit, project: project) }
+
+ it '#initialize' do
+ graph = described_class.new(project, 'refs/heads/master', project.repository.commit, nil)
+
+ expect(graph.notes).to eq( { note_on_commit.commit_id => 1 } )
+ end
+end
diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb
index 6549791f675..9e8ae07e0b2 100644
--- a/spec/models/note_spec.rb
+++ b/spec/models/note_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe Note, models: true do
+ include RepoHelpers
+
describe 'associations' do
it { is_expected.to belong_to(:project) }
it { is_expected.to belong_to(:noteable).touch(true) }
@@ -56,18 +58,18 @@ describe Note, models: true do
let!(:note) { create(:note_on_commit, note: "+1 from me") }
let!(:commit) { note.noteable }
- it "should be accessible through #noteable" do
+ it "is accessible through #noteable" do
expect(note.commit_id).to eq(commit.id)
expect(note.noteable).to be_a(Commit)
expect(note.noteable).to eq(commit)
end
- it "should save a valid note" do
+ it "saves a valid note" do
expect(note.commit_id).to eq(commit.id)
note.noteable == commit
end
- it "should be recognized by #for_commit?" do
+ it "is recognized by #for_commit?" do
expect(note).to be_for_commit
end
@@ -135,22 +137,30 @@ describe Note, models: true do
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,
- 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
+ expect(Banzai::Renderer).to receive(:cache_collection_render).
+ with([{
+ text: note1.note,
+ context: {
+ pipeline: :note,
+ cache_key: [note1, "note"],
+ project: note1.project,
+ author: note1.author
+ }
+ }]).and_call_original
+
+ expect(Banzai::Renderer).to receive(:cache_collection_render).
+ with([{
+ text: note2.note,
+ context: {
+ pipeline: :note,
+ cache_key: [note2, "note"],
+ project: note2.project,
+ author: note2.author
+ }
+ }]).and_call_original
+
+ note1.all_references.users
+ note2.all_references.users
end
end
@@ -215,7 +225,7 @@ describe Note, models: true do
let(:note) do
create :note,
noteable: ext_issue, project: ext_proj,
- note: "mentioned in issue #{private_issue.to_reference(ext_proj)}",
+ note: "Mentioned in issue #{private_issue.to_reference(ext_proj)}",
system: true
end
@@ -226,6 +236,20 @@ describe Note, models: true do
it "returns false" do
expect(note.cross_reference_not_visible_for?(private_user)).to be_falsy
end
+
+ it "returns false if user visible reference count set" do
+ note.user_visible_reference_count = 1
+
+ expect(note).not_to receive(:reference_mentionables)
+ expect(note.cross_reference_not_visible_for?(ext_issue.author)).to be_falsy
+ end
+
+ it "returns true if ref count is 0" do
+ note.user_visible_reference_count = 0
+
+ expect(note).not_to receive(:reference_mentionables)
+ expect(note.cross_reference_not_visible_for?(ext_issue.author)).to be_truthy
+ end
end
describe 'clear_blank_line_code!' do
@@ -245,4 +269,81 @@ describe Note, models: true do
expect(note.participants).to include(note.author)
end
end
+
+ describe ".grouped_diff_discussions" do
+ let!(:merge_request) { create(:merge_request) }
+ let(:project) { merge_request.project }
+ let!(:active_diff_note1) { create(:diff_note_on_merge_request, project: project, noteable: merge_request) }
+ let!(:active_diff_note2) { create(:diff_note_on_merge_request, project: project, noteable: merge_request) }
+ let!(:active_diff_note3) { create(:diff_note_on_merge_request, project: project, noteable: merge_request, position: active_position2) }
+ let!(:outdated_diff_note1) { create(:diff_note_on_merge_request, project: project, noteable: merge_request, position: outdated_position) }
+ let!(:outdated_diff_note2) { create(:diff_note_on_merge_request, project: project, noteable: merge_request, position: outdated_position) }
+
+ let(:active_position2) do
+ Gitlab::Diff::Position.new(
+ old_path: "files/ruby/popen.rb",
+ new_path: "files/ruby/popen.rb",
+ old_line: 16,
+ new_line: 22,
+ diff_refs: merge_request.diff_refs
+ )
+ end
+
+ let(:outdated_position) do
+ Gitlab::Diff::Position.new(
+ old_path: "files/ruby/popen.rb",
+ new_path: "files/ruby/popen.rb",
+ old_line: nil,
+ new_line: 9,
+ diff_refs: project.commit("874797c3a73b60d2187ed6e2fcabd289ff75171e").diff_refs
+ )
+ end
+
+ subject { merge_request.notes.grouped_diff_discussions }
+
+ it "includes active discussions" do
+ discussions = subject.values
+
+ expect(discussions.count).to eq(2)
+ expect(discussions.map(&:id)).to eq([active_diff_note1.discussion_id, active_diff_note3.discussion_id])
+ expect(discussions.all?(&:active?)).to be true
+
+ expect(discussions.first.notes).to eq([active_diff_note1, active_diff_note2])
+ expect(discussions.last.notes).to eq([active_diff_note3])
+ end
+
+ it "doesn't include outdated discussions" do
+ expect(subject.values.map(&:id)).not_to include(outdated_diff_note1.discussion_id)
+ end
+
+ it "groups the discussions by line code" do
+ expect(subject[active_diff_note1.line_code].id).to eq(active_diff_note1.discussion_id)
+ expect(subject[active_diff_note3.line_code].id).to eq(active_diff_note3.discussion_id)
+ end
+ end
+
+ describe "#discussion_id" do
+ let(:note) { create(:note) }
+
+ context "when it is newly created" do
+ it "has a discussion id" do
+ expect(note.discussion_id).not_to be_nil
+ expect(note.discussion_id).to match(/\A\h{40}\z/)
+ end
+ end
+
+ context "when it didn't store a discussion id before" do
+ before do
+ note.update_column(:discussion_id, nil)
+ end
+
+ it "has a discussion id" do
+ # The discussion_id is set in `after_initialize`, so `reload` won't work
+ reloaded_note = Note.find(note.id)
+
+ expect(reloaded_note.discussion_id).not_to be_nil
+ expect(reloaded_note.discussion_id).to match(/\A\h{40}\z/)
+ end
+ end
+ end
end
diff --git a/spec/models/project_security_spec.rb b/spec/models/project_security_spec.rb
index e12258c0874..36379074ea0 100644
--- a/spec/models/project_security_spec.rb
+++ b/spec/models/project_security_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe Project, models: true do
- describe :authorization do
+ describe 'authorization' do
before do
@p1 = create(:project)
@@ -21,7 +21,7 @@ describe Project, models: true do
let(:owner_actions) { Ability.project_owner_rules }
describe "Non member rules" do
- it "should deny for non-project users any actions" do
+ it "denies for non-project users any actions" do
owner_actions.each do |action|
expect(@abilities.allowed?(@u1, action, @p1)).to be_falsey
end
@@ -33,7 +33,7 @@ describe Project, models: true do
@p1.project_members.create(project: @p1, user: @u2, access_level: ProjectMember::GUEST)
end
- it "should allow for project user any guest actions" do
+ it "allows for project user any guest actions" do
guest_actions.each do |action|
expect(@abilities.allowed?(@u2, action, @p1)).to be_truthy
end
@@ -45,7 +45,7 @@ describe Project, models: true do
@p1.project_members.create(project: @p1, user: @u2, access_level: ProjectMember::REPORTER)
end
- it "should allow for project user any report actions" do
+ it "allows for project user any report actions" do
report_actions.each do |action|
expect(@abilities.allowed?(@u2, action, @p1)).to be_truthy
end
@@ -58,13 +58,13 @@ describe Project, models: true do
@p1.project_members.create(project: @p1, user: @u3, access_level: ProjectMember::DEVELOPER)
end
- it "should deny for developer master-specific actions" do
+ it "denies for developer master-specific actions" do
[dev_actions - report_actions].each do |action|
expect(@abilities.allowed?(@u2, action, @p1)).to be_falsey
end
end
- it "should allow for project user any dev actions" do
+ it "allows for project user any dev actions" do
dev_actions.each do |action|
expect(@abilities.allowed?(@u3, action, @p1)).to be_truthy
end
@@ -77,13 +77,13 @@ describe Project, models: true do
@p1.project_members.create(project: @p1, user: @u3, access_level: ProjectMember::MASTER)
end
- it "should deny for developer master-specific actions" do
+ it "denies for developer master-specific actions" do
[master_actions - dev_actions].each do |action|
expect(@abilities.allowed?(@u2, action, @p1)).to be_falsey
end
end
- it "should allow for project user any master actions" do
+ it "allows for project user any master actions" do
master_actions.each do |action|
expect(@abilities.allowed?(@u3, action, @p1)).to be_truthy
end
@@ -96,13 +96,13 @@ describe Project, models: true do
@p1.project_members.create(project: @p1, user: @u3, access_level: ProjectMember::MASTER)
end
- it "should deny for masters admin-specific actions" do
+ it "denies for masters admin-specific actions" do
[owner_actions - master_actions].each do |action|
expect(@abilities.allowed?(@u2, action, @p1)).to be_falsey
end
end
- it "should allow for project owner any admin actions" do
+ it "allows for project owner any admin actions" do
owner_actions.each do |action|
expect(@abilities.allowed?(@u4, action, @p1)).to be_truthy
end
diff --git a/spec/models/project_services/asana_service_spec.rb b/spec/models/project_services/asana_service_spec.rb
index f3d15f3c1ea..dc702cfc42c 100644
--- a/spec/models/project_services/asana_service_spec.rb
+++ b/spec/models/project_services/asana_service_spec.rb
@@ -65,7 +65,7 @@ describe AsanaService, models: true do
)
end
- it 'should call Asana service to create a story' do
+ it 'calls Asana service to create a story' do
data = create_data_for_commits('Message from commit. related to #123456')
expected_message = "#{data[:user_name]} pushed to branch #{data[:ref]} of #{project.name_with_namespace} ( #{data[:commits][0][:url]} ): #{data[:commits][0][:message]}"
@@ -76,7 +76,7 @@ describe AsanaService, models: true do
@asana.execute(data)
end
- it 'should call Asana service to create a story and close a task' do
+ it 'calls Asana service to create a story and close a task' do
data = create_data_for_commits('fix #456789')
d1 = double('Asana::Task')
expect(d1).to receive(:add_comment)
@@ -86,7 +86,7 @@ describe AsanaService, models: true do
@asana.execute(data)
end
- it 'should be able to close via url' do
+ it 'is able to close via url' do
data = create_data_for_commits('closes https://app.asana.com/19292/956299/42')
d1 = double('Asana::Task')
expect(d1).to receive(:add_comment)
@@ -96,7 +96,7 @@ describe AsanaService, models: true do
@asana.execute(data)
end
- it 'should allow multiple matches per line' do
+ it 'allows multiple matches per line' do
message = <<-EOF
minor bigfix, refactoring, fixed #123 and Closes #456 work on #789
ref https://app.asana.com/19292/956299/42 and closing https://app.asana.com/19292/956299/12
diff --git a/spec/models/project_services/assembla_service_spec.rb b/spec/models/project_services/assembla_service_spec.rb
index 17e9361dd5c..d672d80156c 100644
--- a/spec/models/project_services/assembla_service_spec.rb
+++ b/spec/models/project_services/assembla_service_spec.rb
@@ -39,12 +39,12 @@ describe AssemblaService, models: true do
token: 'verySecret',
subdomain: 'project_name'
)
- @sample_data = Gitlab::PushDataBuilder.build_sample(project, user)
+ @sample_data = Gitlab::DataBuilder::Push.build_sample(project, user)
@api_url = 'https://atlas.assembla.com/spaces/project_name/github_tool?secret_key=verySecret'
WebMock.stub_request(:post, @api_url)
end
- it "should call Assembla API" do
+ it "calls Assembla API" do
@assembla_service.execute(@sample_data)
expect(WebMock).to have_requested(:post, @api_url).with(
body: /#{@sample_data[:before]}.*#{@sample_data[:after]}.*#{project.path}/
diff --git a/spec/models/project_services/buildkite_service_spec.rb b/spec/models/project_services/buildkite_service_spec.rb
index 60364df2015..0866e1532dd 100644
--- a/spec/models/project_services/buildkite_service_spec.rb
+++ b/spec/models/project_services/buildkite_service_spec.rb
@@ -57,7 +57,7 @@ describe BuildkiteService, models: true do
)
end
- describe :webhook_url do
+ describe '#webhook_url' do
it 'returns the webhook url' do
expect(@service.webhook_url).to eq(
'https://webhook.buildkite.com/deliver/secret-sauce-webhook-token'
@@ -65,7 +65,7 @@ describe BuildkiteService, models: true do
end
end
- describe :commit_status_path do
+ describe '#commit_status_path' do
it 'returns the correct status page' do
expect(@service.commit_status_path('2ab7834c')).to eq(
'https://gitlab.buildkite.com/status/secret-sauce-status-token.json?commit=2ab7834c'
@@ -73,7 +73,7 @@ describe BuildkiteService, models: true do
end
end
- describe :build_page do
+ describe '#build_page' do
it 'returns the correct build page' do
expect(@service.build_page('2ab7834c', nil)).to eq(
'https://buildkite.com/account-name/example-project/builds?commit=2ab7834c'
diff --git a/spec/models/project_services/builds_email_service_spec.rb b/spec/models/project_services/builds_email_service_spec.rb
index 236df8f047d..0194f9e2563 100644
--- a/spec/models/project_services/builds_email_service_spec.rb
+++ b/spec/models/project_services/builds_email_service_spec.rb
@@ -1,7 +1,9 @@
require 'spec_helper'
describe BuildsEmailService do
- let(:data) { Gitlab::BuildDataBuilder.build(create(:ci_build)) }
+ let(:data) do
+ Gitlab::DataBuilder::Build.build(create(:ci_build))
+ end
describe 'Validations' do
context 'when service is active' do
@@ -23,6 +25,44 @@ describe BuildsEmailService do
end
end
+ describe '#test_data' do
+ let(:build) { create(:ci_build) }
+ let(:project) { build.project }
+ let(:user) { create(:user) }
+
+ before { project.team << [user, :developer] }
+
+ it 'builds test data' do
+ data = subject.test_data(project)
+
+ expect(data[:object_kind]).to eq("build")
+ end
+ end
+
+ describe '#test' do
+ it 'sends email' do
+ data = Gitlab::DataBuilder::Build.build(create(:ci_build))
+ subject.recipients = 'test@gitlab.com'
+
+ expect(BuildEmailWorker).to receive(:perform_async)
+
+ subject.test(data)
+ end
+
+ context 'notify only failed builds is true' do
+ it 'sends email' do
+ data = Gitlab::DataBuilder::Build.build(create(:ci_build))
+ data[:build_status] = "success"
+ subject.recipients = 'test@gitlab.com'
+
+ expect(subject).not_to receive(:notify_only_broken_builds)
+ expect(BuildEmailWorker).to receive(:perform_async)
+
+ subject.test(data)
+ end
+ end
+ end
+
describe '#execute' do
it 'sends email' do
subject.recipients = 'test@gitlab.com'
diff --git a/spec/models/project_services/campfire_service_spec.rb b/spec/models/project_services/campfire_service_spec.rb
index 3e6da42803b..c76ae21421b 100644
--- a/spec/models/project_services/campfire_service_spec.rb
+++ b/spec/models/project_services/campfire_service_spec.rb
@@ -39,4 +39,62 @@ describe CampfireService, models: true do
it { is_expected.not_to validate_presence_of(:token) }
end
end
+
+ describe "#execute" do
+ let(:user) { create(:user) }
+ let(:project) { create(:project) }
+
+ before do
+ @campfire_service = CampfireService.new
+ allow(@campfire_service).to receive_messages(
+ project_id: project.id,
+ project: project,
+ service_hook: true,
+ token: 'verySecret',
+ subdomain: 'project-name',
+ room: 'test-room'
+ )
+ @sample_data = Gitlab::DataBuilder::Push.build_sample(project, user)
+ @rooms_url = 'https://verySecret:X@project-name.campfirenow.com/rooms.json'
+ @headers = { 'Content-Type' => 'application/json; charset=utf-8' }
+ end
+
+ it "calls Campfire API to get a list of rooms and speak in a room" do
+ # make sure a valid list of rooms is returned
+ body = File.read(Rails.root + 'spec/fixtures/project_services/campfire/rooms.json')
+ WebMock.stub_request(:get, @rooms_url).to_return(
+ body: body,
+ status: 200,
+ headers: @headers
+ )
+ # stub the speak request with the room id found in the previous request's response
+ speak_url = 'https://verySecret:X@project-name.campfirenow.com/room/123/speak.json'
+ WebMock.stub_request(:post, speak_url)
+
+ @campfire_service.execute(@sample_data)
+
+ expect(WebMock).to have_requested(:get, @rooms_url).once
+ expect(WebMock).to have_requested(:post, speak_url).with(
+ body: /#{project.path}.*#{@sample_data[:before]}.*#{@sample_data[:after]}/
+ ).once
+ end
+
+ it "calls Campfire API to get a list of rooms but shouldn't speak in a room" do
+ # return a list of rooms that do not contain a room named 'test-room'
+ body = File.read(Rails.root + 'spec/fixtures/project_services/campfire/rooms2.json')
+ WebMock.stub_request(:get, @rooms_url).to_return(
+ body: body,
+ status: 200,
+ headers: @headers
+ )
+ # we want to make sure no request is sent to the /speak endpoint, here is a basic
+ # regexp that matches this endpoint
+ speak_url = 'https://verySecret:X@project-name.campfirenow.com/room/.*/speak.json'
+
+ @campfire_service.execute(@sample_data)
+
+ expect(WebMock).to have_requested(:get, @rooms_url).once
+ expect(WebMock).not_to have_requested(:post, /#{speak_url}/)
+ end
+ end
end
diff --git a/spec/models/project_services/drone_ci_service_spec.rb b/spec/models/project_services/drone_ci_service_spec.rb
index 3a8e67438fc..8ef892259f2 100644
--- a/spec/models/project_services/drone_ci_service_spec.rb
+++ b/spec/models/project_services/drone_ci_service_spec.rb
@@ -84,7 +84,9 @@ describe DroneCiService, models: true do
include_context :drone_ci_service
let(:user) { create(:user, username: 'username') }
- let(:push_sample_data) { Gitlab::PushDataBuilder.build_sample(project, user) }
+ let(:push_sample_data) do
+ Gitlab::DataBuilder::Push.build_sample(project, user)
+ end
it do
service_hook = double
diff --git a/spec/models/project_services/external_wiki_service_spec.rb b/spec/models/project_services/external_wiki_service_spec.rb
index 5fe5ea7d2df..d7c5ea95d71 100644
--- a/spec/models/project_services/external_wiki_service_spec.rb
+++ b/spec/models/project_services/external_wiki_service_spec.rb
@@ -56,7 +56,7 @@ describe ExternalWikiService, models: true do
@service.destroy!
end
- it 'should replace the wiki url' do
+ it 'replaces the wiki url' do
wiki_path = get_project_wiki_path(project)
expect(wiki_path).to match('https://gitlab.com')
end
diff --git a/spec/models/project_services/flowdock_service_spec.rb b/spec/models/project_services/flowdock_service_spec.rb
index b7e627e6518..d2557019756 100644
--- a/spec/models/project_services/flowdock_service_spec.rb
+++ b/spec/models/project_services/flowdock_service_spec.rb
@@ -52,12 +52,12 @@ describe FlowdockService, models: true do
service_hook: true,
token: 'verySecret'
)
- @sample_data = Gitlab::PushDataBuilder.build_sample(project, user)
+ @sample_data = Gitlab::DataBuilder::Push.build_sample(project, user)
@api_url = 'https://api.flowdock.com/v1/messages'
WebMock.stub_request(:post, @api_url)
end
- it "should call FlowDock API" do
+ it "calls FlowDock API" do
@flowdock_service.execute(@sample_data)
@sample_data[:commits].each do |commit|
# One request to Flowdock per new commit
diff --git a/spec/models/project_services/gemnasium_service_spec.rb b/spec/models/project_services/gemnasium_service_spec.rb
index a08f1ac229f..3d0b6c9816b 100644
--- a/spec/models/project_services/gemnasium_service_spec.rb
+++ b/spec/models/project_services/gemnasium_service_spec.rb
@@ -55,9 +55,9 @@ describe GemnasiumService, models: true do
token: 'verySecret',
api_key: 'GemnasiumUserApiKey'
)
- @sample_data = Gitlab::PushDataBuilder.build_sample(project, user)
+ @sample_data = Gitlab::DataBuilder::Push.build_sample(project, user)
end
- it "should call Gemnasium service" do
+ it "calls Gemnasium service" do
expect(Gemnasium::GitlabService).to receive(:execute).with(an_instance_of(Hash)).once
@gemnasium_service.execute(@sample_data)
end
diff --git a/spec/models/project_services/gitlab_issue_tracker_service_spec.rb b/spec/models/project_services/gitlab_issue_tracker_service_spec.rb
index 7a1f106d6e3..8ef79a17d50 100644
--- a/spec/models/project_services/gitlab_issue_tracker_service_spec.rb
+++ b/spec/models/project_services/gitlab_issue_tracker_service_spec.rb
@@ -54,7 +54,7 @@ describe GitlabIssueTrackerService, models: true do
@service.destroy!
end
- it 'should give the correct path' do
+ it 'gives the correct path' do
expect(@service.project_url).to eq("http://localhost/gitlab/root/#{project.path_with_namespace}/issues")
expect(@service.new_issue_url).to eq("http://localhost/gitlab/root/#{project.path_with_namespace}/issues/new")
expect(@service.issue_url(432)).to eq("http://localhost/gitlab/root/#{project.path_with_namespace}/issues/432")
@@ -71,7 +71,7 @@ describe GitlabIssueTrackerService, models: true do
@service.destroy!
end
- it 'should give the correct path' do
+ it 'gives the correct path' do
expect(@service.project_path).to eq("/gitlab/root/#{project.path_with_namespace}/issues")
expect(@service.new_issue_path).to eq("/gitlab/root/#{project.path_with_namespace}/issues/new")
expect(@service.issue_path(432)).to eq("/gitlab/root/#{project.path_with_namespace}/issues/432")
diff --git a/spec/models/project_services/hipchat_service_spec.rb b/spec/models/project_services/hipchat_service_spec.rb
index 5f618322aab..34eafbe555d 100644
--- a/spec/models/project_services/hipchat_service_spec.rb
+++ b/spec/models/project_services/hipchat_service_spec.rb
@@ -48,7 +48,9 @@ describe HipchatService, models: true do
let(:project_name) { project.name_with_namespace.gsub(/\s/, '') }
let(:token) { 'verySecret' }
let(:server_url) { 'https://hipchat.example.com'}
- let(:push_sample_data) { Gitlab::PushDataBuilder.build_sample(project, user) }
+ let(:push_sample_data) do
+ Gitlab::DataBuilder::Push.build_sample(project, user)
+ end
before(:each) do
allow(hipchat).to receive_messages(
@@ -61,7 +63,7 @@ describe HipchatService, models: true do
WebMock.stub_request(:post, api_url)
end
- it 'should test and return errors' do
+ it 'tests and return errors' do
allow(hipchat).to receive(:execute).and_raise(StandardError, 'no such room')
result = hipchat.test(push_sample_data)
@@ -69,7 +71,7 @@ describe HipchatService, models: true do
expect(result[:result].to_s).to eq('no such room')
end
- it 'should use v1 if version is provided' do
+ it 'uses v1 if version is provided' do
allow(hipchat).to receive(:api_version).and_return('v1')
expect(HipChat::Client).to receive(:new).with(
token,
@@ -79,7 +81,7 @@ describe HipchatService, models: true do
hipchat.execute(push_sample_data)
end
- it 'should use v2 as the version when nothing is provided' do
+ it 'uses v2 as the version when nothing is provided' do
allow(hipchat).to receive(:api_version).and_return('')
expect(HipChat::Client).to receive(:new).with(
token,
@@ -90,13 +92,13 @@ describe HipchatService, models: true do
end
context 'push events' do
- it "should call Hipchat API for push events" do
+ it "calls Hipchat API for push events" do
hipchat.execute(push_sample_data)
expect(WebMock).to have_requested(:post, api_url).once
end
- it "should create a push message" do
+ it "creates a push message" do
message = hipchat.send(:create_push_message, push_sample_data)
push_sample_data[:object_attributes]
@@ -108,15 +110,23 @@ describe HipchatService, models: true do
end
context 'tag_push events' do
- let(:push_sample_data) { Gitlab::PushDataBuilder.build(project, user, Gitlab::Git::BLANK_SHA, '1' * 40, 'refs/tags/test', []) }
+ let(:push_sample_data) do
+ Gitlab::DataBuilder::Push.build(
+ project,
+ user,
+ Gitlab::Git::BLANK_SHA,
+ '1' * 40,
+ 'refs/tags/test',
+ [])
+ end
- it "should call Hipchat API for tag push events" do
+ it "calls Hipchat API for tag push events" do
hipchat.execute(push_sample_data)
expect(WebMock).to have_requested(:post, api_url).once
end
- it "should create a tag push message" do
+ it "creates a tag push message" do
message = hipchat.send(:create_push_message, push_sample_data)
push_sample_data[:object_attributes]
@@ -131,13 +141,13 @@ describe HipchatService, models: true do
let(:issue_service) { Issues::CreateService.new(project, user) }
let(:issues_sample_data) { issue_service.hook_data(issue, 'open') }
- it "should call Hipchat API for issue events" do
+ it "calls Hipchat API for issue events" do
hipchat.execute(issues_sample_data)
expect(WebMock).to have_requested(:post, api_url).once
end
- it "should create an issue message" do
+ it "creates an issue message" do
message = hipchat.send(:create_issue_message, issues_sample_data)
obj_attr = issues_sample_data[:object_attributes]
@@ -154,13 +164,13 @@ describe HipchatService, models: true do
let(:merge_service) { MergeRequests::CreateService.new(project, user) }
let(:merge_sample_data) { merge_service.hook_data(merge_request, 'open') }
- it "should call Hipchat API for merge requests events" do
+ it "calls Hipchat API for merge requests events" do
hipchat.execute(merge_sample_data)
expect(WebMock).to have_requested(:post, api_url).once
end
- it "should create a merge request message" do
+ it "creates a merge request message" do
message = hipchat.send(:create_merge_request_message,
merge_sample_data)
@@ -184,8 +194,8 @@ describe HipchatService, models: true do
note: 'a comment on a commit')
end
- it "should call Hipchat API for commit comment events" do
- data = Gitlab::NoteDataBuilder.build(commit_note, user)
+ it "calls Hipchat API for commit comment events" do
+ data = Gitlab::DataBuilder::Note.build(commit_note, user)
hipchat.execute(data)
expect(WebMock).to have_requested(:post, api_url).once
@@ -216,8 +226,8 @@ describe HipchatService, models: true do
note: "merge request note")
end
- it "should call Hipchat API for merge request comment events" do
- data = Gitlab::NoteDataBuilder.build(merge_request_note, user)
+ it "calls Hipchat API for merge request comment events" do
+ data = Gitlab::DataBuilder::Note.build(merge_request_note, user)
hipchat.execute(data)
expect(WebMock).to have_requested(:post, api_url).once
@@ -243,8 +253,8 @@ describe HipchatService, models: true do
note: "issue note")
end
- it "should call Hipchat API for issue comment events" do
- data = Gitlab::NoteDataBuilder.build(issue_note, user)
+ it "calls Hipchat API for issue comment events" do
+ data = Gitlab::DataBuilder::Note.build(issue_note, user)
hipchat.execute(data)
message = hipchat.send(:create_message, data)
@@ -269,8 +279,8 @@ describe HipchatService, models: true do
note: "snippet note")
end
- it "should call Hipchat API for snippet comment events" do
- data = Gitlab::NoteDataBuilder.build(snippet_note, user)
+ it "calls Hipchat API for snippet comment events" do
+ data = Gitlab::DataBuilder::Note.build(snippet_note, user)
hipchat.execute(data)
expect(WebMock).to have_requested(:post, api_url).once
@@ -291,19 +301,20 @@ describe HipchatService, models: true do
end
context 'build events' do
- let(:build) { create(:ci_build) }
- let(:data) { Gitlab::BuildDataBuilder.build(build) }
+ let(:pipeline) { create(:ci_empty_pipeline) }
+ let(:build) { create(:ci_build, pipeline: pipeline) }
+ let(:data) { Gitlab::DataBuilder::Build.build(build) }
context 'for failed' do
before { build.drop }
- it "should call Hipchat API" do
+ it "calls Hipchat API" do
hipchat.execute(data)
expect(WebMock).to have_requested(:post, api_url).once
end
- it "should create a build message" do
+ it "creates a build message" do
message = hipchat.send(:create_build_message, data)
project_url = project.web_url
@@ -325,13 +336,13 @@ describe HipchatService, models: true do
build.success
end
- it "should call Hipchat API" do
+ it "calls Hipchat API" do
hipchat.notify_only_broken_builds = false
hipchat.execute(data)
expect(WebMock).to have_requested(:post, api_url).once
end
- it "should notify only broken" do
+ it "notifies only broken" do
hipchat.notify_only_broken_builds = true
hipchat.execute(data)
expect(WebMock).not_to have_requested(:post, api_url).once
@@ -340,18 +351,36 @@ describe HipchatService, models: true do
end
context "#message_options" do
- it "should be set to the defaults" do
- expect(hipchat.send(:message_options)).to eq({ notify: false, color: 'yellow' })
+ it "is set to the defaults" do
+ expect(hipchat.__send__(:message_options)).to eq({ notify: false, color: 'yellow' })
end
- it "should set notfiy to true" do
+ it "sets notify to true" do
allow(hipchat).to receive(:notify).and_return('1')
- expect(hipchat.send(:message_options)).to eq({ notify: true, color: 'yellow' })
+
+ expect(hipchat.__send__(:message_options)).to eq({ notify: true, color: 'yellow' })
end
- it "should set the color" do
+ it "sets the color" do
allow(hipchat).to receive(:color).and_return('red')
- expect(hipchat.send(:message_options)).to eq({ notify: false, color: 'red' })
+
+ expect(hipchat.__send__(:message_options)).to eq({ notify: false, color: 'red' })
+ end
+
+ context 'with a successful build' do
+ it 'uses the green color' do
+ build_data = { object_kind: 'build', commit: { status: 'success' } }
+
+ expect(hipchat.__send__(:message_options, build_data)).to eq({ notify: false, color: 'green' })
+ end
+ end
+
+ context 'with a failed build' do
+ it 'uses the red color' do
+ build_data = { object_kind: 'build', commit: { status: 'failed' } }
+
+ expect(hipchat.__send__(:message_options, build_data)).to eq({ notify: false, color: 'red' })
+ end
end
end
end
diff --git a/spec/models/project_services/irker_service_spec.rb b/spec/models/project_services/irker_service_spec.rb
index 4ee022a5171..ffb17fd3259 100644
--- a/spec/models/project_services/irker_service_spec.rb
+++ b/spec/models/project_services/irker_service_spec.rb
@@ -46,32 +46,35 @@ describe IrkerService, models: true do
let(:irker) { IrkerService.new }
let(:user) { create(:user) }
let(:project) { create(:project) }
- let(:sample_data) { Gitlab::PushDataBuilder.build_sample(project, user) }
+ let(:sample_data) do
+ Gitlab::DataBuilder::Push.build_sample(project, user)
+ end
let(:recipients) { '#commits irc://test.net/#test ftp://bad' }
let(:colorize_messages) { '1' }
before do
+ @irker_server = TCPServer.new 'localhost', 0
+
allow(irker).to receive_messages(
active: true,
project: project,
project_id: project.id,
service_hook: true,
- server_host: 'localhost',
- server_port: 6659,
+ server_host: @irker_server.addr[2],
+ server_port: @irker_server.addr[1],
default_irc_uri: 'irc://chat.freenode.net/',
recipients: recipients,
colorize_messages: colorize_messages)
irker.valid?
- @irker_server = TCPServer.new 'localhost', 6659
end
after do
@irker_server.close
end
- it 'should send valid JSON messages to an Irker listener' do
+ it 'sends valid JSON messages to an Irker listener' do
irker.execute(sample_data)
conn = @irker_server.accept
diff --git a/spec/models/project_services/jira_service_spec.rb b/spec/models/project_services/jira_service_spec.rb
index 5a97cf370da..9037ca5cc20 100644
--- a/spec/models/project_services/jira_service_spec.rb
+++ b/spec/models/project_services/jira_service_spec.rb
@@ -66,7 +66,7 @@ describe JiraService, models: true do
password: 'gitlab_jira_password'
)
@jira_service.save # will build API URL, as api_url was not specified above
- @sample_data = Gitlab::PushDataBuilder.build_sample(project, user)
+ @sample_data = Gitlab::DataBuilder::Push.build_sample(project, user)
# https://github.com/bblimke/webmock#request-with-basic-authentication
@api_url = 'http://gitlab_jira_username:gitlab_jira_password@jira.example.com/rest/api/2/issue/JIRA-123/transitions'
@comment_url = 'http://gitlab_jira_username:gitlab_jira_password@jira.example.com/rest/api/2/issue/JIRA-123/comment'
@@ -75,7 +75,7 @@ describe JiraService, models: true do
WebMock.stub_request(:post, @comment_url)
end
- it "should call JIRA API" do
+ it "calls JIRA API" do
@jira_service.execute(merge_request,
ExternalIssue.new("JIRA-123", project))
expect(WebMock).to have_requested(:post, @comment_url).with(
@@ -128,7 +128,7 @@ describe JiraService, models: true do
expect(@jira_service.api_url).to eq("http://jira_edited.example.com/rest/api/2")
end
- it "should reset password if url changed, even if setter called multiple times" do
+ it "resets password if url changed, even if setter called multiple times" do
@jira_service.api_url = 'http://jira1.example.com/rest/api/2'
@jira_service.api_url = 'http://jira1.example.com/rest/api/2'
@jira_service.save
@@ -181,7 +181,7 @@ describe JiraService, models: true do
@service.destroy!
end
- it 'should be initialized' do
+ it 'is initialized' do
expect(@service.title).to eq('JIRA')
expect(@service.description).to eq("Jira issue tracker")
end
@@ -197,7 +197,7 @@ describe JiraService, models: true do
@service.destroy!
end
- it "should be correct" do
+ it "is correct" do
expect(@service.title).to eq('Jira One')
expect(@service.description).to eq('Jira One issue tracker')
end
@@ -225,7 +225,7 @@ describe JiraService, models: true do
@service.destroy!
end
- it 'should be prepopulated with the settings' do
+ it 'is prepopulated with the settings' do
expect(@service.properties["project_url"]).to eq('http://jira.sample/projects/project_a')
expect(@service.properties["issues_url"]).to eq("http://jira.sample/issues/:id")
expect(@service.properties["new_issue_url"]).to eq("http://jira.sample/projects/project_a/issues/new")
diff --git a/spec/models/project_services/pivotaltracker_service_spec.rb b/spec/models/project_services/pivotaltracker_service_spec.rb
index f37edd4d970..d098d988521 100644
--- a/spec/models/project_services/pivotaltracker_service_spec.rb
+++ b/spec/models/project_services/pivotaltracker_service_spec.rb
@@ -39,4 +39,75 @@ describe PivotaltrackerService, models: true do
it { is_expected.not_to validate_presence_of(:token) }
end
end
+
+ describe 'Execute' do
+ let(:service) do
+ PivotaltrackerService.new.tap do |service|
+ service.token = 'secret_api_token'
+ end
+ end
+
+ let(:url) { PivotaltrackerService::API_ENDPOINT }
+
+ def push_data(branch: 'master')
+ {
+ object_kind: 'push',
+ ref: "refs/heads/#{branch}",
+ commits: [
+ {
+ id: '21c12ea',
+ author: {
+ name: 'Some User'
+ },
+ url: 'https://example.com/commit',
+ message: 'commit message',
+ }
+ ]
+ }
+ end
+
+ before do
+ WebMock.stub_request(:post, url)
+ end
+
+ it 'should post correct message' do
+ service.execute(push_data)
+ expect(WebMock).to have_requested(:post, url).with(
+ body: {
+ 'source_commit' => {
+ 'commit_id' => '21c12ea',
+ 'author' => 'Some User',
+ 'url' => 'https://example.com/commit',
+ 'message' => 'commit message'
+ }
+ },
+ headers: {
+ 'Content-Type' => 'application/json',
+ 'X-TrackerToken' => 'secret_api_token'
+ }
+ ).once
+ end
+
+ context 'when allowed branches is specified' do
+ let(:service) do
+ super().tap do |service|
+ service.restrict_to_branch = 'master,v10'
+ end
+ end
+
+ it 'should post message if branch is in the list' do
+ service.execute(push_data(branch: 'master'))
+ service.execute(push_data(branch: 'v10'))
+
+ expect(WebMock).to have_requested(:post, url).twice
+ end
+
+ it 'should not post message if branch is not in the list' do
+ service.execute(push_data(branch: 'mas'))
+ service.execute(push_data(branch: 'v11'))
+
+ expect(WebMock).not_to have_requested(:post, url)
+ end
+ end
+ end
end
diff --git a/spec/models/project_services/pushover_service_spec.rb b/spec/models/project_services/pushover_service_spec.rb
index 555d9757b47..5959c81577d 100644
--- a/spec/models/project_services/pushover_service_spec.rb
+++ b/spec/models/project_services/pushover_service_spec.rb
@@ -48,7 +48,9 @@ describe PushoverService, models: true do
let(:pushover) { PushoverService.new }
let(:user) { create(:user) }
let(:project) { create(:project) }
- let(:sample_data) { Gitlab::PushDataBuilder.build_sample(project, user) }
+ let(:sample_data) do
+ Gitlab::DataBuilder::Push.build_sample(project, user)
+ end
let(:api_key) { 'verySecret' }
let(:user_key) { 'verySecret' }
@@ -72,7 +74,7 @@ describe PushoverService, models: true do
WebMock.stub_request(:post, api_url)
end
- it 'should call Pushover API' do
+ it 'calls Pushover API' do
pushover.execute(sample_data)
expect(WebMock).to have_requested(:post, api_url).once
diff --git a/spec/models/project_services/slack_service/note_message_spec.rb b/spec/models/project_services/slack_service/note_message_spec.rb
index 379c3e1219c..41b93f08050 100644
--- a/spec/models/project_services/slack_service/note_message_spec.rb
+++ b/spec/models/project_services/slack_service/note_message_spec.rb
@@ -60,6 +60,7 @@ describe SlackService::NoteMessage, models: true do
title: "merge request title\ndetails\n"
}
end
+
it 'returns a message regarding notes on a merge request' do
message = SlackService::NoteMessage.new(@args)
expect(message.pretext).to eq("Test User commented on " \
diff --git a/spec/models/project_services/slack_service/wiki_page_message_spec.rb b/spec/models/project_services/slack_service/wiki_page_message_spec.rb
index 46dedb66c7c..13aea0b0600 100644
--- a/spec/models/project_services/slack_service/wiki_page_message_spec.rb
+++ b/spec/models/project_services/slack_service/wiki_page_message_spec.rb
@@ -47,7 +47,7 @@ describe SlackService::WikiPageMessage, models: true do
context 'when :action == "create"' do
before { args[:object_attributes][:action] = 'create' }
- it 'it returns the attachment for a new wiki page' do
+ it 'returns the attachment for a new wiki page' do
expect(subject.attachments).to eq([
{
text: "Wiki page description",
@@ -60,7 +60,7 @@ describe SlackService::WikiPageMessage, models: true do
context 'when :action == "update"' do
before { args[:object_attributes][:action] = 'update' }
- it 'it returns the attachment for an updated wiki page' do
+ it 'returns the attachment for an updated wiki page' do
expect(subject.attachments).to eq([
{
text: "Wiki page description",
diff --git a/spec/models/project_services/slack_service_spec.rb b/spec/models/project_services/slack_service_spec.rb
index 155f3e74e0d..28af68d13b4 100644
--- a/spec/models/project_services/slack_service_spec.rb
+++ b/spec/models/project_services/slack_service_spec.rb
@@ -45,7 +45,9 @@ describe SlackService, models: true do
let(:slack) { SlackService.new }
let(:user) { create(:user) }
let(:project) { create(:project) }
- let(:push_sample_data) { Gitlab::PushDataBuilder.build_sample(project, user) }
+ let(:push_sample_data) do
+ Gitlab::DataBuilder::Push.build_sample(project, user)
+ end
let(:webhook_url) { 'https://hooks.slack.com/services/SVRWFV0VVAR97N/B02R25XN3/ZBqu7xMupaEEICInN685' }
let(:username) { 'slack_username' }
let(:channel) { 'slack_channel' }
@@ -93,41 +95,42 @@ describe SlackService, models: true do
@wiki_page_sample_data = wiki_page_service.hook_data(@wiki_page, 'create')
end
- it "should call Slack API for push events" do
+ it "calls Slack API for push events" do
slack.execute(push_sample_data)
expect(WebMock).to have_requested(:post, webhook_url).once
end
- it "should call Slack API for issue events" do
+ it "calls Slack API for issue events" do
slack.execute(@issues_sample_data)
expect(WebMock).to have_requested(:post, webhook_url).once
end
- it "should call Slack API for merge requests events" do
+ it "calls Slack API for merge requests events" do
slack.execute(@merge_sample_data)
expect(WebMock).to have_requested(:post, webhook_url).once
end
- it "should call Slack API for wiki page events" do
+ it "calls Slack API for wiki page events" do
slack.execute(@wiki_page_sample_data)
expect(WebMock).to have_requested(:post, webhook_url).once
end
- it 'should use the username as an option for slack when configured' do
+ it 'uses the username as an option for slack when configured' do
allow(slack).to receive(:username).and_return(username)
expect(Slack::Notifier).to receive(:new).
with(webhook_url, username: username).
and_return(
double(:slack_service).as_null_object
)
+
slack.execute(push_sample_data)
end
- it 'should use the channel as an option when it is configured' do
+ it 'uses the channel as an option when it is configured' do
allow(slack).to receive(:channel).and_return(channel)
expect(Slack::Notifier).to receive(:new).
with(webhook_url, channel: channel).
@@ -136,6 +139,76 @@ describe SlackService, models: true do
)
slack.execute(push_sample_data)
end
+
+ context "event channels" do
+ it "uses the right channel for push event" do
+ slack.update_attributes(push_channel: "random")
+
+ expect(Slack::Notifier).to receive(:new).
+ with(webhook_url, channel: "random").
+ and_return(
+ double(:slack_service).as_null_object
+ )
+
+ slack.execute(push_sample_data)
+ end
+
+ it "uses the right channel for merge request event" do
+ slack.update_attributes(merge_request_channel: "random")
+
+ expect(Slack::Notifier).to receive(:new).
+ with(webhook_url, channel: "random").
+ and_return(
+ double(:slack_service).as_null_object
+ )
+
+ slack.execute(@merge_sample_data)
+ end
+
+ it "uses the right channel for issue event" do
+ slack.update_attributes(issue_channel: "random")
+
+ expect(Slack::Notifier).to receive(:new).
+ with(webhook_url, channel: "random").
+ and_return(
+ double(:slack_service).as_null_object
+ )
+
+ slack.execute(@issues_sample_data)
+ end
+
+ it "uses the right channel for wiki event" do
+ slack.update_attributes(wiki_page_channel: "random")
+
+ expect(Slack::Notifier).to receive(:new).
+ with(webhook_url, channel: "random").
+ and_return(
+ double(:slack_service).as_null_object
+ )
+
+ slack.execute(@wiki_page_sample_data)
+ end
+
+ context "note event" do
+ let(:issue_note) do
+ create(:note_on_issue, project: project, note: "issue note")
+ end
+
+ it "uses the right channel" do
+ slack.update_attributes(note_channel: "random")
+
+ note_data = Gitlab::DataBuilder::Note.build(issue_note, user)
+
+ expect(Slack::Notifier).to receive(:new).
+ with(webhook_url, channel: "random").
+ and_return(
+ double(:slack_service).as_null_object
+ )
+
+ slack.execute(note_data)
+ end
+ end
+ end
end
describe "Note events" do
@@ -163,8 +236,8 @@ describe SlackService, models: true do
note: 'a comment on a commit')
end
- it "should call Slack API for commit comment events" do
- data = Gitlab::NoteDataBuilder.build(commit_note, user)
+ it "calls Slack API for commit comment events" do
+ data = Gitlab::DataBuilder::Note.build(commit_note, user)
slack.execute(data)
expect(WebMock).to have_requested(:post, webhook_url).once
@@ -177,8 +250,8 @@ describe SlackService, models: true do
note: "merge request note")
end
- it "should call Slack API for merge request comment events" do
- data = Gitlab::NoteDataBuilder.build(merge_request_note, user)
+ it "calls Slack API for merge request comment events" do
+ data = Gitlab::DataBuilder::Note.build(merge_request_note, user)
slack.execute(data)
expect(WebMock).to have_requested(:post, webhook_url).once
@@ -190,8 +263,8 @@ describe SlackService, models: true do
create(:note_on_issue, project: project, note: "issue note")
end
- it "should call Slack API for issue comment events" do
- data = Gitlab::NoteDataBuilder.build(issue_note, user)
+ it "calls Slack API for issue comment events" do
+ data = Gitlab::DataBuilder::Note.build(issue_note, user)
slack.execute(data)
expect(WebMock).to have_requested(:post, webhook_url).once
@@ -204,8 +277,8 @@ describe SlackService, models: true do
note: "snippet note")
end
- it "should call Slack API for snippet comment events" do
- data = Gitlab::NoteDataBuilder.build(snippet_note, user)
+ it "calls Slack API for snippet comment events" do
+ data = Gitlab::DataBuilder::Note.build(snippet_note, user)
slack.execute(data)
expect(WebMock).to have_requested(:post, webhook_url).once
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 5a27ccbab0a..b2baeeb31bb 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -23,6 +23,7 @@ describe Project, models: true do
it { is_expected.to have_one(:slack_service).dependent(:destroy) }
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_one(:board).dependent(:destroy) }
it { is_expected.to have_many(:commit_statuses) }
it { is_expected.to have_many(:pipelines) }
it { is_expected.to have_many(:builds) }
@@ -69,6 +70,7 @@ describe Project, models: true do
it { is_expected.to include_module(Gitlab::ConfigHelper) }
it { is_expected.to include_module(Gitlab::ShellAdapter) }
it { is_expected.to include_module(Gitlab::VisibilityLevel) }
+ it { is_expected.to include_module(Gitlab::CurrentSettings) }
it { is_expected.to include_module(Referable) }
it { is_expected.to include_module(Sortable) }
end
@@ -88,7 +90,7 @@ describe Project, models: true do
it { is_expected.to validate_presence_of(:namespace) }
it { is_expected.to validate_presence_of(:repository_storage) }
- it 'should not allow new projects beyond user limits' do
+ it 'does 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
@@ -97,7 +99,7 @@ describe Project, models: true do
describe 'wiki path conflict' do
context "when the new path has been used by the wiki of other Project" do
- it 'should have an error on the name attribute' do
+ it 'has an error on the name attribute' do
new_project = build_stubbed(:project, namespace_id: project.namespace_id, path: "#{project.path}.wiki")
expect(new_project).not_to be_valid
@@ -106,7 +108,7 @@ describe Project, models: true do
end
context "when the new wiki path has been used by the path of other Project" do
- it 'should have an error on the name attribute' do
+ it 'has an error on the name attribute' do
project_with_wiki_suffix = create(:project, path: 'foo.wiki')
new_project = build_stubbed(:project, namespace_id: project_with_wiki_suffix.namespace_id, path: 'foo')
@@ -124,23 +126,41 @@ describe Project, models: true do
allow(Gitlab.config.repositories).to receive(:storages).and_return(storages)
end
- it "should not allow repository storages that don't match a label in the configuration" do
+ it "does not allow repository storages that don't match a label in the configuration" do
expect(project2).not_to be_valid
expect(project2.errors[:repository_storage].first).to match(/is not included in the list/)
end
end
- it 'should not allow an invalid URI as import_url' do
+ it 'does not allow an invalid URI as import_url' do
project2 = build(:project, import_url: 'invalid://')
expect(project2).not_to be_valid
end
- it 'should allow a valid URI as import_url' do
+ it 'does allow a valid URI as import_url' do
project2 = build(:project, import_url: 'ssh://test@gitlab.com/project.git')
expect(project2).to be_valid
end
+
+ it 'allows an empty URI' do
+ project2 = build(:project, import_url: '')
+
+ expect(project2).to be_valid
+ end
+
+ it 'does not produce import data on an empty URI' do
+ project2 = build(:project, import_url: '')
+
+ expect(project2.import_data).to be_nil
+ end
+
+ it 'does not produce import data on an invalid URI' do
+ project2 = build(:project, import_url: 'test://')
+
+ expect(project2.import_data).to be_nil
+ end
end
describe 'default_scope' do
@@ -153,12 +173,12 @@ describe Project, models: true do
end
describe 'project token' do
- it 'should set an random token if none provided' do
+ it 'sets an random token if none provided' do
project = FactoryGirl.create :empty_project, runners_token: ''
expect(project.runners_token).not_to eq('')
end
- it 'should not set an random toke if one provided' do
+ it 'does not set an random toke if one provided' do
project = FactoryGirl.create :empty_project, runners_token: 'my-token'
expect(project.runners_token).to eq('my-token')
end
@@ -206,7 +226,7 @@ describe Project, models: true do
end
end
- it 'should return valid url to repo' do
+ it 'returns valid url to repo' do
project = Project.new(path: 'somewhere')
expect(project.url_to_repo).to eq(Gitlab.config.gitlab_shell.ssh_path_prefix + 'somewhere.git')
end
@@ -227,12 +247,40 @@ describe Project, models: true do
end
end
+ xdescribe "#new_issue_address" do
+ let(:project) { create(:empty_project, path: "somewhere") }
+ let(:user) { create(:user) }
+
+ context 'incoming email enabled' do
+ before do
+ stub_incoming_email_setting(enabled: true, address: "p+%{key}@gl.ab")
+ end
+
+ it 'returns the address to create a new issue' do
+ token = user.authentication_token
+ address = "p+#{project.namespace.path}/#{project.path}+#{token}@gl.ab"
+
+ expect(project.new_issue_address(user)).to eq(address)
+ end
+ end
+
+ context 'incoming email disabled' do
+ before do
+ stub_incoming_email_setting(enabled: false)
+ end
+
+ it 'returns nil' do
+ expect(project.new_issue_address(user)).to be_nil
+ end
+ end
+ end
+
describe 'last_activity methods' do
let(:project) { create(:project) }
let(:last_event) { double(created_at: Time.now) }
describe 'last_activity' do
- it 'should alias last_activity to last_event' do
+ it 'alias last_activity to last_event' do
allow(project).to receive(:last_event).and_return(last_event)
expect(project.last_activity).to eq(last_event)
end
@@ -296,20 +344,20 @@ describe Project, models: true do
end
end
- describe :update_merge_requests do
+ describe '#update_merge_requests' do
let(:project) { create(:project) }
let(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
let(:key) { create(:key, user_id: project.owner.id) }
let(:prev_commit_id) { merge_request.commits.last.id }
let(:commit_id) { merge_request.commits.first.id }
- it 'should close merge request if last commit from source branch was pushed to target branch' do
+ it 'closes merge request if last commit from source branch was pushed to target branch' do
project.update_merge_requests(prev_commit_id, commit_id, "refs/heads/#{merge_request.target_branch}", key.user)
merge_request.reload
expect(merge_request.merged?).to be_truthy
end
- it 'should update merge request commits with new one if pushed to source branch' do
+ it 'updates merge request commits with new one if pushed to source branch' do
project.update_merge_requests(prev_commit_id, commit_id, "refs/heads/#{merge_request.source_branch}", key.user)
merge_request.reload
expect(merge_request.diff_head_sha).to eq(commit_id)
@@ -345,7 +393,7 @@ describe Project, models: true do
end
end
- describe :to_param do
+ describe '#to_param' do
context 'with namespace' do
before do
@group = create :group, name: 'gitlab'
@@ -354,30 +402,48 @@ describe Project, models: true do
it { expect(@project.to_param).to eq('gitlabhq') }
end
+
+ context 'with invalid path' do
+ it 'returns previous path to keep project suitable for use in URLs when persisted' do
+ project = create(:empty_project, path: 'gitlab')
+ project.path = 'foo&bar'
+
+ expect(project).not_to be_valid
+ expect(project.to_param).to eq 'gitlab'
+ end
+
+ it 'returns current path when new record' do
+ project = build(:empty_project, path: 'gitlab')
+ project.path = 'foo&bar'
+
+ expect(project).not_to be_valid
+ expect(project.to_param).to eq 'foo&bar'
+ end
+ end
end
- describe :repository do
+ describe '#repository' do
let(:project) { create(:project) }
- it 'should return valid repo' do
+ it 'returns valid repo' do
expect(project.repository).to be_kind_of(Repository)
end
end
- describe :default_issues_tracker? do
+ describe '#default_issues_tracker?' do
let(:project) { create(:project) }
let(:ext_project) { create(:redmine_project) }
- it "should be true if used internal tracker" do
+ it "is true if used internal tracker" do
expect(project.default_issues_tracker?).to be_truthy
end
- it "should be false if used other tracker" do
+ it "is false if used other tracker" do
expect(ext_project.default_issues_tracker?).to be_falsey
end
end
- describe :external_issue_tracker do
+ describe '#external_issue_tracker' do
let(:project) { create(:project) }
let(:ext_project) { create(:redmine_project) }
@@ -418,7 +484,7 @@ describe Project, models: true do
end
end
- describe :cache_has_external_issue_tracker do
+ describe '#cache_has_external_issue_tracker' do
let(:project) { create(:project) }
it 'stores true if there is any external_issue_tracker' do
@@ -440,7 +506,58 @@ describe Project, models: true do
end
end
- describe :open_branches do
+ describe '#external_wiki' do
+ let(:project) { create(:project) }
+
+ context 'with an active external wiki' do
+ before do
+ create(:service, project: project, type: 'ExternalWikiService', active: true)
+ project.external_wiki
+ end
+
+ it 'sets :has_external_wiki as true' do
+ expect(project.has_external_wiki).to be(true)
+ end
+
+ it 'sets :has_external_wiki as false if an external wiki service is destroyed later' do
+ expect(project.has_external_wiki).to be(true)
+
+ project.services.external_wikis.first.destroy
+
+ expect(project.has_external_wiki).to be(false)
+ end
+ end
+
+ context 'with an inactive external wiki' do
+ before do
+ create(:service, project: project, type: 'ExternalWikiService', active: false)
+ end
+
+ it 'sets :has_external_wiki as false' do
+ expect(project.has_external_wiki).to be(false)
+ end
+ end
+
+ context 'with no external wiki' do
+ before do
+ project.external_wiki
+ end
+
+ it 'sets :has_external_wiki as false' do
+ expect(project.has_external_wiki).to be(false)
+ end
+
+ it 'sets :has_external_wiki as true if an external wiki service is created later' do
+ expect(project.has_external_wiki).to be(false)
+
+ create(:service, project: project, type: 'ExternalWikiService', active: true)
+
+ expect(project.has_external_wiki).to be(true)
+ end
+ end
+ end
+
+ describe '#open_branches' do
let(:project) { create(:project) }
before do
@@ -517,21 +634,21 @@ describe Project, models: true do
end
end
- describe :avatar_type do
+ describe '#avatar_type' do
let(:project) { create(:project) }
- it 'should be true if avatar is image' do
+ it 'is true if avatar is image' do
project.update_attribute(:avatar, 'uploads/avatar.png')
expect(project.avatar_type).to be_truthy
end
- it 'should be false if avatar is html page' do
+ it 'is false if avatar is html page' do
project.update_attribute(:avatar, 'uploads/avatar.html')
expect(project.avatar_type).to eq(['only images allowed'])
end
end
- describe :avatar_url do
+ describe '#avatar_url' do
subject { project.avatar_url }
let(:project) { create(:project) }
@@ -568,7 +685,7 @@ describe Project, models: true do
end
end
- describe :pipeline do
+ describe '#pipeline' do
let(:project) { create :project }
let(:pipeline) { create :ci_pipeline, project: project, ref: 'master' }
@@ -588,7 +705,7 @@ describe Project, models: true do
end
end
- describe :builds_enabled do
+ describe '#builds_enabled' do
let(:project) { create :project }
before { project.builds_enabled = true }
@@ -598,6 +715,20 @@ describe Project, models: true do
it { expect(project.builds_enabled?).to be_truthy }
end
+ describe '.cached_count', caching: true do
+ let(:group) { create(:group, :public) }
+ let!(:project1) { create(:empty_project, :public, group: group) }
+ let!(:project2) { create(:empty_project, :public, group: group) }
+
+ it 'returns total project count' do
+ expect(Project).to receive(:count).once.and_call_original
+
+ 3.times do
+ expect(Project.cached_count).to eq(2)
+ end
+ end
+ end
+
describe '.trending' do
let(:group) { create(:group, :public) }
let(:project1) { create(:empty_project, :public, group: group) }
@@ -690,7 +821,7 @@ describe Project, models: true do
end
end
- describe :any_runners do
+ describe '#any_runners' do
let(:project) { create(:empty_project, shared_runners_enabled: shared_runners_enabled) }
let(:specific_runner) { create(:ci_runner) }
let(:shared_runner) { create(:ci_runner, :shared) }
@@ -698,16 +829,16 @@ describe Project, models: true do
context 'for shared runners disabled' do
let(:shared_runners_enabled) { false }
- it 'there are no runners available' do
+ it 'has no runners available' do
expect(project.any_runners?).to be_falsey
end
- it 'there is a specific runner' do
+ it 'has a specific runner' do
project.runners << specific_runner
expect(project.any_runners?).to be_truthy
end
- it 'there is a shared runner, but they are prohibited to use' do
+ it 'has a shared runner, but they are prohibited to use' do
shared_runner
expect(project.any_runners?).to be_falsey
end
@@ -721,7 +852,7 @@ describe Project, models: true do
context 'for shared runners enabled' do
let(:shared_runners_enabled) { true }
- it 'there is a shared runner' do
+ it 'has a shared runner' do
shared_runner
expect(project.any_runners?).to be_truthy
end
@@ -955,68 +1086,97 @@ describe Project, models: true do
end
describe '#protected_branch?' do
- let(:project) { create(:empty_project) }
+ context 'existing project' do
+ let(:project) { create(:project) }
- it 'returns true when the branch matches a protected branch via direct match' do
- project.protected_branches.create!(name: 'foo')
+ it 'returns true when the branch matches a protected branch via direct match' do
+ create(:protected_branch, project: project, name: "foo")
- expect(project.protected_branch?('foo')).to eq(true)
- end
+ expect(project.protected_branch?('foo')).to eq(true)
+ end
- it 'returns true when the branch matches a protected branch via wildcard match' do
- project.protected_branches.create!(name: 'production/*')
+ it 'returns true when the branch matches a protected branch via wildcard match' do
+ create(:protected_branch, project: project, name: "production/*")
- expect(project.protected_branch?('production/some-branch')).to eq(true)
- end
+ expect(project.protected_branch?('production/some-branch')).to eq(true)
+ end
- it 'returns false when the branch does not match a protected branch via direct match' do
- expect(project.protected_branch?('foo')).to eq(false)
- end
+ it 'returns false when the branch does not match a protected branch via direct match' do
+ expect(project.protected_branch?('foo')).to eq(false)
+ end
- it 'returns false when the branch does not match a protected branch via wildcard match' do
- project.protected_branches.create!(name: 'production/*')
+ it 'returns false when the branch does not match a protected branch via wildcard match' do
+ create(:protected_branch, project: project, name: "production/*")
- expect(project.protected_branch?('staging/some-branch')).to eq(false)
+ expect(project.protected_branch?('staging/some-branch')).to eq(false)
+ end
end
- end
- describe "#developers_can_push_to_protected_branch?" do
- let(:project) { create(:empty_project) }
+ context "new project" do
+ let(:project) { create(:empty_project) }
- context "when the branch matches a protected branch via direct match" do
- it "returns true if 'Developers can Push' is turned on" do
- create(:protected_branch, name: "production", project: project, developers_can_push: true)
+ it 'returns false when default_protected_branch is unprotected' do
+ stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_NONE)
- expect(project.developers_can_push_to_protected_branch?('production')).to be true
+ expect(project.protected_branch?('master')).to be false
end
- it "returns false if 'Developers can Push' is turned off" do
- create(:protected_branch, name: "production", project: project, developers_can_push: false)
+ it 'returns false when default_protected_branch lets developers push' do
+ stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_DEV_CAN_PUSH)
- expect(project.developers_can_push_to_protected_branch?('production')).to be false
+ expect(project.protected_branch?('master')).to be false
end
- end
- context "when the branch matches a protected branch via wilcard match" do
- it "returns true if 'Developers can Push' is turned on" do
- create(:protected_branch, name: "production/*", project: project, developers_can_push: true)
+ it 'returns true when default_branch_protection does not let developers push but let developer merge branches' do
+ stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_DEV_CAN_MERGE)
- expect(project.developers_can_push_to_protected_branch?('production/some-branch')).to be true
+ expect(project.protected_branch?('master')).to be true
end
- it "returns false if 'Developers can Push' is turned off" do
- create(:protected_branch, name: "production/*", project: project, developers_can_push: false)
+ it 'returns true when default_branch_protection is in full protection' do
+ stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_FULL)
- expect(project.developers_can_push_to_protected_branch?('production/some-branch')).to be false
+ expect(project.protected_branch?('master')).to be true
end
end
+ end
- context "when the branch does not match a protected branch" do
- it "returns false" do
- create(:protected_branch, name: "production/*", project: project, developers_can_push: true)
+ describe '#user_can_push_to_empty_repo?' do
+ let(:project) { create(:empty_project) }
+ let(:user) { create(:user) }
- expect(project.developers_can_push_to_protected_branch?('staging/some-branch')).to be false
- end
+ it 'returns false when default_branch_protection is in full protection and user is developer' do
+ project.team << [user, :developer]
+ stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_FULL)
+
+ expect(project.user_can_push_to_empty_repo?(user)).to be_falsey
+ end
+
+ it 'returns false when default_branch_protection only lets devs merge and user is dev' do
+ project.team << [user, :developer]
+ stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_DEV_CAN_MERGE)
+
+ expect(project.user_can_push_to_empty_repo?(user)).to be_falsey
+ end
+
+ it 'returns true when default_branch_protection lets devs push and user is developer' do
+ project.team << [user, :developer]
+ stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_DEV_CAN_PUSH)
+
+ expect(project.user_can_push_to_empty_repo?(user)).to be_truthy
+ end
+
+ it 'returns true when default_branch_protection is unprotected and user is developer' do
+ project.team << [user, :developer]
+ stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_NONE)
+
+ expect(project.user_can_push_to_empty_repo?(user)).to be_truthy
+ end
+
+ it 'returns true when user is master' do
+ project.team << [user, :master]
+
+ expect(project.user_can_push_to_empty_repo?(user)).to be_truthy
end
end
@@ -1096,6 +1256,111 @@ describe Project, models: true do
end
end
+ describe '#latest_successful_builds_for' do
+ def create_pipeline(status = 'success')
+ create(:ci_pipeline, project: project,
+ sha: project.commit.sha,
+ ref: project.default_branch,
+ status: status)
+ end
+
+ def create_build(new_pipeline = pipeline, name = 'test')
+ create(:ci_build, :success, :artifacts,
+ pipeline: new_pipeline,
+ status: new_pipeline.status,
+ name: name)
+ end
+
+ let(:project) { create(:project) }
+ let(:pipeline) { create_pipeline }
+
+ context 'with many builds' do
+ it 'gives the latest builds from latest pipeline' do
+ pipeline1 = create_pipeline
+ pipeline2 = create_pipeline
+ build1_p2 = create_build(pipeline2, 'test')
+ create_build(pipeline1, 'test')
+ create_build(pipeline1, 'test2')
+ build2_p2 = create_build(pipeline2, 'test2')
+
+ latest_builds = project.latest_successful_builds_for
+
+ expect(latest_builds).to contain_exactly(build2_p2, build1_p2)
+ end
+ end
+
+ context 'with succeeded pipeline' do
+ let!(:build) { create_build }
+
+ context 'standalone pipeline' do
+ it 'returns builds for ref for default_branch' do
+ builds = project.latest_successful_builds_for
+
+ expect(builds).to contain_exactly(build)
+ end
+
+ it 'returns empty relation if the build cannot be found' do
+ builds = project.latest_successful_builds_for('TAIL')
+
+ expect(builds).to be_kind_of(ActiveRecord::Relation)
+ expect(builds).to be_empty
+ end
+ end
+
+ context 'with some pending pipeline' do
+ before do
+ create_build(create_pipeline('pending'))
+ end
+
+ it 'gives the latest build from latest pipeline' do
+ latest_build = project.latest_successful_builds_for
+
+ expect(latest_build).to contain_exactly(build)
+ end
+ end
+ end
+
+ context 'with pending pipeline' do
+ before do
+ pipeline.update(status: 'pending')
+ create_build(pipeline)
+ end
+
+ it 'returns empty relation' do
+ builds = project.latest_successful_builds_for
+
+ expect(builds).to be_kind_of(ActiveRecord::Relation)
+ expect(builds).to be_empty
+ end
+ end
+ end
+
+ describe '#add_import_job' do
+ context 'forked' do
+ let(:forked_project_link) { create(:forked_project_link) }
+ let(:forked_from_project) { forked_project_link.forked_from_project }
+ let(:project) { forked_project_link.forked_to_project }
+
+ it 'schedules a RepositoryForkWorker job' do
+ expect(RepositoryForkWorker).to receive(:perform_async).
+ with(project.id, forked_from_project.repository_storage_path,
+ forked_from_project.path_with_namespace, project.namespace.path)
+
+ project.add_import_job
+ end
+ end
+
+ context 'not forked' do
+ let(:project) { create(:project) }
+
+ it 'schedules a RepositoryImportWorker job' do
+ expect(RepositoryImportWorker).to receive(:perform_async).with(project.id)
+
+ project.add_import_job
+ end
+ end
+ end
+
describe '.where_paths_in' do
context 'without any paths' do
it 'returns an empty relation' do
@@ -1128,4 +1393,84 @@ describe Project, models: true do
end
end
end
+
+ describe 'authorized_for_user' do
+ let(:group) { create(:group) }
+ let(:developer) { create(:user) }
+ let(:master) { create(:user) }
+ let(:personal_project) { create(:project, namespace: developer.namespace) }
+ let(:group_project) { create(:project, namespace: group) }
+ let(:members_project) { create(:project) }
+ let(:shared_project) { create(:project) }
+
+ before do
+ group.add_master(master)
+ group.add_developer(developer)
+
+ members_project.team << [developer, :developer]
+ members_project.team << [master, :master]
+
+ create(:project_group_link, project: shared_project, group: group)
+ end
+
+ it 'returns false for no user' do
+ expect(personal_project.authorized_for_user?(nil)).to be(false)
+ end
+
+ it 'returns true for personal projects of the user' do
+ expect(personal_project.authorized_for_user?(developer)).to be(true)
+ end
+
+ it 'returns true for projects of groups the user is a member of' do
+ expect(group_project.authorized_for_user?(developer)).to be(true)
+ end
+
+ it 'returns true for projects for which the user is a member of' do
+ expect(members_project.authorized_for_user?(developer)).to be(true)
+ end
+
+ it 'returns true for projects shared on a group the user is a member of' do
+ expect(shared_project.authorized_for_user?(developer)).to be(true)
+ end
+
+ it 'checks for the correct minimum level access' do
+ expect(group_project.authorized_for_user?(developer, Gitlab::Access::MASTER)).to be(false)
+ expect(group_project.authorized_for_user?(master, Gitlab::Access::MASTER)).to be(true)
+ expect(members_project.authorized_for_user?(developer, Gitlab::Access::MASTER)).to be(false)
+ expect(members_project.authorized_for_user?(master, Gitlab::Access::MASTER)).to be(true)
+ expect(shared_project.authorized_for_user?(developer, Gitlab::Access::MASTER)).to be(false)
+ expect(shared_project.authorized_for_user?(master, Gitlab::Access::MASTER)).to be(true)
+ end
+ end
+
+ describe 'change_head' do
+ let(:project) { create(:project) }
+
+ it 'calls the before_change_head method' do
+ expect(project.repository).to receive(:before_change_head)
+ project.change_head(project.default_branch)
+ end
+
+ it 'creates the new reference with rugged' do
+ expect(project.repository.rugged.references).to receive(:create).with('HEAD',
+ "refs/heads/#{project.default_branch}",
+ force: true)
+ project.change_head(project.default_branch)
+ end
+
+ it 'copies the gitattributes' do
+ expect(project.repository).to receive(:copy_gitattributes).with(project.default_branch)
+ project.change_head(project.default_branch)
+ end
+
+ it 'expires the avatar cache' do
+ expect(project.repository).to receive(:expire_avatar_cache).with(project.default_branch)
+ project.change_head(project.default_branch)
+ end
+
+ it 'reloads the default branch' do
+ expect(project).to receive(:reload_default_branch)
+ project.change_head(project.default_branch)
+ end
+ end
end
diff --git a/spec/models/project_team_spec.rb b/spec/models/project_team_spec.rb
index 9262aeb6ed8..5eaf0d3b7a6 100644
--- a/spec/models/project_team_spec.rb
+++ b/spec/models/project_team_spec.rb
@@ -151,8 +151,8 @@ describe ProjectTeam, models: true do
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 }
+ it { expect(project.team.max_member_access(nonmember.id)).to eq(Gitlab::Access::NO_ACCESS) }
+ it { expect(project.team.max_member_access(requester.id)).to eq(Gitlab::Access::NO_ACCESS) }
end
context 'when project is shared with group' do
@@ -168,14 +168,14 @@ describe ProjectTeam, models: true do
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 }
+ it { expect(project.team.max_member_access(nonmember.id)).to eq(Gitlab::Access::NO_ACCESS) }
+ it { expect(project.team.max_member_access(requester.id)).to eq(Gitlab::Access::NO_ACCESS) }
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 }
+ it { expect(project.team.max_member_access(master.id)).to eq(Gitlab::Access::NO_ACCESS) }
+ it { expect(project.team.max_member_access(reporter.id)).to eq(Gitlab::Access::NO_ACCESS) }
end
end
end
@@ -194,8 +194,74 @@ describe ProjectTeam, models: true do
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 }
+ it { expect(project.team.max_member_access(nonmember.id)).to eq(Gitlab::Access::NO_ACCESS) }
+ it { expect(project.team.max_member_access(requester.id)).to eq(Gitlab::Access::NO_ACCESS) }
end
end
+
+ shared_examples_for "#max_member_access_for_users" do |enable_request_store|
+ describe "#max_member_access_for_users" do
+ before do
+ RequestStore.begin! if enable_request_store
+ end
+
+ after do
+ if enable_request_store
+ RequestStore.end!
+ RequestStore.clear!
+ end
+ end
+
+ it 'returns correct roles for different users' do
+ master = create(:user)
+ reporter = create(:user)
+ promoted_guest = create(:user)
+ guest = create(:user)
+ project = create(:project)
+
+ project.team << [master, :master]
+ project.team << [reporter, :reporter]
+ project.team << [promoted_guest, :guest]
+ project.team << [guest, :guest]
+
+ group = create(:group)
+ group_developer = create(:user)
+ second_developer = create(:user)
+ project.project_group_links.create(
+ group: group,
+ group_access: Gitlab::Access::DEVELOPER)
+
+ group.add_master(promoted_guest)
+ group.add_developer(group_developer)
+ group.add_developer(second_developer)
+
+ second_group = create(:group)
+ project.project_group_links.create(
+ group: second_group,
+ group_access: Gitlab::Access::MASTER)
+ second_group.add_master(second_developer)
+
+ users = [master, reporter, promoted_guest, guest, group_developer, second_developer].map(&:id)
+
+ expected = {
+ master.id => Gitlab::Access::MASTER,
+ reporter.id => Gitlab::Access::REPORTER,
+ promoted_guest.id => Gitlab::Access::DEVELOPER,
+ guest.id => Gitlab::Access::GUEST,
+ group_developer.id => Gitlab::Access::DEVELOPER,
+ second_developer.id => Gitlab::Access::MASTER
+ }
+
+ expect(project.team.max_member_access_for_user_ids(users)).to eq(expected)
+ end
+ end
+ end
+
+ describe '#max_member_access_for_users with RequestStore' do
+ it_behaves_like "#max_member_access_for_users", true
+ end
+
+ describe '#max_member_access_for_users without RequestStore' do
+ it_behaves_like "#max_member_access_for_users", false
+ end
end
diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb
index 24e49c8def3..1fea50ad42c 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -4,19 +4,20 @@ describe Repository, models: true do
include RepoHelpers
TestBlob = Struct.new(:name)
- let(:repository) { create(:project).repository }
+ let(:project) { create(:project) }
+ let(:repository) { project.repository }
let(:user) { create(:user) }
let(:commit_options) do
author = repository.user_to_committer(user)
{ message: 'Test message', committer: author, author: author }
end
let(:merge_commit) do
- source_sha = repository.find_branch('feature').target
- merge_commit_sha = repository.merge(user, source_sha, 'master', commit_options)
- repository.commit(merge_commit_sha)
+ merge_request = create(:merge_request, source_branch: 'feature', target_branch: 'master', source_project: project)
+ merge_commit_id = repository.merge(user, merge_request, commit_options)
+ repository.commit(merge_commit_id)
end
- describe :branch_names_contains do
+ describe '#branch_names_contains' do
subject { repository.branch_names_contains(sample_commit.id) }
it { is_expected.to include('master') }
@@ -24,7 +25,7 @@ describe Repository, models: true do
it { is_expected.not_to include('fix') }
end
- describe :tag_names_contains do
+ describe '#tag_names_contains' do
subject { repository.tag_names_contains(sample_commit.id) }
it { is_expected.to include('v1.1.0') }
@@ -49,8 +50,9 @@ describe Repository, models: true do
double_first = double(committed_date: Time.now)
double_last = double(committed_date: Time.now - 1.second)
- allow(repository).to receive(:commit).with(tag_a.target).and_return(double_first)
- allow(repository).to receive(:commit).with(tag_b.target).and_return(double_last)
+ allow(tag_a).to receive(:target).and_return(double_first)
+ allow(tag_b).to receive(:target).and_return(double_last)
+ allow(repository).to receive(:tags).and_return([tag_a, tag_b])
end
it { is_expected.to eq(['v1.0.0', 'v1.1.0']) }
@@ -63,8 +65,9 @@ describe Repository, models: true do
double_first = double(committed_date: Time.now - 1.second)
double_last = double(committed_date: Time.now)
- allow(repository).to receive(:commit).with(tag_a.target).and_return(double_last)
- allow(repository).to receive(:commit).with(tag_b.target).and_return(double_first)
+ allow(tag_a).to receive(:target).and_return(double_last)
+ allow(tag_b).to receive(:target).and_return(double_first)
+ allow(repository).to receive(:tags).and_return([tag_a, tag_b])
end
it { is_expected.to eq(['v1.1.0', 'v1.0.0']) }
@@ -72,13 +75,13 @@ describe Repository, models: true do
end
end
- describe :last_commit_for_path do
+ describe '#last_commit_for_path' do
subject { repository.last_commit_for_path(sample_commit.id, '.gitignore').id }
it { is_expected.to eq('c1acaa58bbcbc3eafe538cb8274ba387047b69f8') }
end
- describe :find_commits_by_message do
+ describe '#find_commits_by_message' do
subject { repository.find_commits_by_message('submodule').map{ |k| k.id } }
it { is_expected.to include('5937ac0a7beb003549fc5fd26fc247adbce4a52e') }
@@ -87,7 +90,7 @@ describe Repository, models: true do
it { is_expected.not_to include('913c66a37b4a45b9769037c55c2d238bd0942d2e') }
end
- describe :blob_at do
+ describe '#blob_at' do
context 'blank sha' do
subject { repository.blob_at(Gitlab::Git::BLANK_SHA, '.gitignore') }
@@ -95,7 +98,7 @@ describe Repository, models: true do
end
end
- describe :merged_to_root_ref? do
+ describe '#merged_to_root_ref?' do
context 'merged branch' do
subject { repository.merged_to_root_ref?('improve/awesome') }
@@ -103,7 +106,7 @@ describe Repository, models: true do
end
end
- describe :can_be_merged? do
+ describe '#can_be_merged?' do
context 'mergeable branches' do
subject { repository.can_be_merged?('0b4bc9a49b562e85de7cc9e834518ea6828729b9', 'master') }
@@ -129,6 +132,36 @@ describe Repository, models: true do
end
end
+ describe :commit_file do
+ it 'commits change to a file successfully' do
+ expect do
+ repository.commit_file(user, 'CHANGELOG', 'Changelog!',
+ 'Updates file content',
+ 'master', true)
+ end.to change { repository.commits('master').count }.by(1)
+
+ blob = repository.blob_at('master', 'CHANGELOG')
+
+ expect(blob.data).to eq('Changelog!')
+ end
+ end
+
+ describe :update_file do
+ it 'updates filename successfully' do
+ expect do
+ repository.update_file(user, 'NEWLICENSE', 'Copyright!',
+ branch: 'master',
+ previous_path: 'LICENSE',
+ message: 'Changes filename')
+ end.to change { repository.commits('master').count }.by(1)
+
+ files = repository.ls_files('master')
+
+ expect(files).not_to include('LICENSE')
+ expect(files).to include('NEWLICENSE')
+ end
+ end
+
describe "search_files" do
let(:results) { repository.search_files('feature', 'master') }
subject { results }
@@ -305,16 +338,16 @@ describe Repository, models: true do
end
end
- describe :add_branch do
+ describe '#add_branch' do
context 'when pre hooks were successful' do
- it 'should run without errors' do
+ it 'runs without errors' do
hook = double(trigger: [true, nil])
expect(Gitlab::Git::Hook).to receive(:new).exactly(3).times.and_return(hook)
expect { repository.add_branch(user, 'new_feature', 'master') }.not_to raise_error
end
- it 'should create the branch' do
+ it 'creates the branch' do
allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([true, nil])
branch = repository.add_branch(user, 'new_feature', 'master')
@@ -330,7 +363,7 @@ describe Repository, models: true do
end
context 'when pre hooks failed' do
- it 'should get an error' do
+ it 'gets an error' do
allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([false, ''])
expect do
@@ -338,7 +371,7 @@ describe Repository, models: true do
end.to raise_error(GitHooksService::PreReceiveError)
end
- it 'should not create the branch' do
+ it 'does not create the branch' do
allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([false, ''])
expect do
@@ -349,15 +382,19 @@ describe Repository, models: true do
end
end
- describe :rm_branch do
+ describe '#rm_branch' do
+ let(:old_rev) { '0b4bc9a49b562e85de7cc9e834518ea6828729b9' } # git rev-parse feature
+ let(:blank_sha) { '0000000000000000000000000000000000000000' }
+
context 'when pre hooks were successful' do
- it 'should run without errors' do
- allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([true, nil])
+ it 'runs without errors' do
+ expect_any_instance_of(GitHooksService).to receive(:execute).
+ with(user, project.repository.path_to_repo, old_rev, blank_sha, 'refs/heads/feature')
expect { repository.rm_branch(user, 'feature') }.not_to raise_error
end
- it 'should delete the branch' do
+ it 'deletes the branch' do
allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([true, nil])
expect { repository.rm_branch(user, 'feature') }.not_to raise_error
@@ -367,7 +404,7 @@ describe Repository, models: true do
end
context 'when pre hooks failed' do
- it 'should get an error' do
+ it 'gets an error' do
allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([false, ''])
expect do
@@ -375,7 +412,7 @@ describe Repository, models: true do
end.to raise_error(GitHooksService::PreReceiveError)
end
- it 'should not delete the branch' do
+ it 'does not delete the branch' do
allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([false, ''])
expect do
@@ -386,28 +423,39 @@ describe Repository, models: true do
end
end
- describe :commit_with_hooks do
+ describe '#commit_with_hooks' do
+ let(:old_rev) { '0b4bc9a49b562e85de7cc9e834518ea6828729b9' } # git rev-parse feature
+
context 'when pre hooks were successful' do
before do
expect_any_instance_of(GitHooksService).to receive(:execute).
- and_return(true)
+ with(user, repository.path_to_repo, old_rev, sample_commit.id, 'refs/heads/feature').
+ and_yield.and_return(true)
end
- it 'should run without errors' do
+ it 'runs without errors' do
expect do
repository.commit_with_hooks(user, 'feature') { sample_commit.id }
end.not_to raise_error
end
- it 'should ensure the autocrlf Git option is set to :input' do
+ it 'ensures the autocrlf Git option is set to :input' do
expect(repository).to receive(:update_autocrlf_option)
repository.commit_with_hooks(user, 'feature') { sample_commit.id }
end
+
+ context "when the branch wasn't empty" do
+ it 'updates the head' do
+ expect(repository.find_branch('feature').target.id).to eq(old_rev)
+ repository.commit_with_hooks(user, 'feature') { sample_commit.id }
+ expect(repository.find_branch('feature').target.id).to eq(sample_commit.id)
+ end
+ end
end
context 'when pre hooks failed' do
- it 'should get an error' do
+ it 'gets an error' do
allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([false, ''])
expect do
@@ -415,6 +463,43 @@ describe Repository, models: true do
end.to raise_error(GitHooksService::PreReceiveError)
end
end
+
+ context 'when target branch is different from source branch' do
+ before do
+ allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([true, ''])
+ end
+
+ it 'expires branch cache' do
+ expect(repository).not_to receive(:expire_exists_cache)
+ expect(repository).not_to receive(:expire_root_ref_cache)
+ expect(repository).not_to receive(:expire_emptiness_caches)
+ expect(repository).to receive(:expire_branches_cache)
+ expect(repository).to receive(:expire_has_visible_content_cache)
+ expect(repository).to receive(:expire_branch_count_cache)
+
+ repository.commit_with_hooks(user, 'new-feature') { sample_commit.id }
+ end
+ end
+
+ context 'when repository is empty' do
+ before do
+ allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([true, ''])
+ end
+
+ it 'expires creation and branch cache' do
+ empty_repository = create(:empty_project, :empty_repo).repository
+
+ expect(empty_repository).to receive(:expire_exists_cache)
+ expect(empty_repository).to receive(:expire_root_ref_cache)
+ expect(empty_repository).to receive(:expire_emptiness_caches)
+ expect(empty_repository).to receive(:expire_branches_cache)
+ expect(empty_repository).to receive(:expire_has_visible_content_cache)
+ expect(empty_repository).to receive(:expire_branch_count_cache)
+
+ empty_repository.commit_file(user, 'CHANGELOG', 'Changelog!',
+ 'Updates file content', 'master', false)
+ end
+ end
end
describe '#exists?' do
@@ -630,10 +715,18 @@ describe Repository, models: true do
end
describe '#merge' do
- it 'should merge the code and return the commit id' do
+ it 'merges the code and return the commit id' do
expect(merge_commit).to be_present
expect(repository.blob_at(merge_commit.id, 'files/ruby/feature.rb')).to be_present
end
+
+ it 'sets the `in_progress_merge_commit_sha` flag for the given merge request' do
+ merge_request = create(:merge_request, source_branch: 'feature', target_branch: 'master', source_project: project)
+ merge_commit_id = repository.merge(user, merge_request, commit_options)
+ repository.commit(merge_commit_id)
+
+ expect(merge_request.in_progress_merge_commit_sha).to eq(merge_commit_id)
+ end
end
describe '#revert' do
@@ -641,13 +734,13 @@ describe Repository, models: true do
let(:update_image_commit) { repository.commit('2f63565e7aac07bcdadb654e253078b727143ec4') }
context 'when there is a conflict' do
- it 'should abort the operation' do
+ it 'aborts the operation' do
expect(repository.revert(user, new_image_commit, 'master')).to eq(false)
end
end
context 'when commit was already reverted' do
- it 'should abort the operation' do
+ it 'aborts the operation' do
repository.revert(user, update_image_commit, 'master')
expect(repository.revert(user, update_image_commit, 'master')).to eq(false)
@@ -655,13 +748,13 @@ describe Repository, models: true do
end
context 'when commit can be reverted' do
- it 'should revert the changes' do
+ it 'reverts the changes' do
expect(repository.revert(user, update_image_commit, 'master')).to be_truthy
end
end
context 'reverting a merge commit' do
- it 'should revert the changes' do
+ it 'reverts the changes' do
merge_commit
expect(repository.blob_at_branch('master', 'files/ruby/feature.rb')).to be_present
@@ -677,13 +770,13 @@ describe Repository, models: true do
let(:pickable_merge) { repository.commit('e56497bb5f03a90a51293fc6d516788730953899') }
context 'when there is a conflict' do
- it 'should abort the operation' do
+ it 'aborts the operation' do
expect(repository.cherry_pick(user, conflict_commit, 'master')).to eq(false)
end
end
context 'when commit was already cherry-picked' do
- it 'should abort the operation' do
+ it 'aborts the operation' do
repository.cherry_pick(user, pickable_commit, 'master')
expect(repository.cherry_pick(user, pickable_commit, 'master')).to eq(false)
@@ -691,13 +784,13 @@ describe Repository, models: true do
end
context 'when commit can be cherry-picked' do
- it 'should cherry-pick the changes' do
+ it 'cherry-picks the changes' do
expect(repository.cherry_pick(user, pickable_commit, 'master')).to be_truthy
end
end
context 'cherry-picking a merge commit' do
- it 'should cherry-pick the changes' do
+ it 'cherry-picks the changes' do
expect(repository.blob_at_branch('master', 'foo/bar/.gitkeep')).to be_nil
repository.cherry_pick(user, pickable_merge, 'master')
@@ -718,6 +811,30 @@ describe Repository, models: true do
repository.before_delete
end
+ it 'flushes the tags cache' do
+ expect(repository).to receive(:expire_tags_cache)
+
+ repository.before_delete
+ end
+
+ it 'flushes the tag count cache' do
+ expect(repository).to receive(:expire_tag_count_cache)
+
+ repository.before_delete
+ end
+
+ it 'flushes the branches cache' do
+ expect(repository).to receive(:expire_branches_cache)
+
+ repository.before_delete
+ end
+
+ it 'flushes the branch count cache' do
+ expect(repository).to receive(:expire_branch_count_cache)
+
+ repository.before_delete
+ end
+
it 'flushes the root ref cache' do
expect(repository).to receive(:expire_root_ref_cache)
@@ -748,6 +865,30 @@ describe Repository, models: true do
repository.before_delete
end
+ it 'flushes the tags cache' do
+ expect(repository).to receive(:expire_tags_cache)
+
+ repository.before_delete
+ end
+
+ it 'flushes the tag count cache' do
+ expect(repository).to receive(:expire_tag_count_cache)
+
+ repository.before_delete
+ end
+
+ it 'flushes the branches cache' do
+ expect(repository).to receive(:expire_branches_cache)
+
+ repository.before_delete
+ end
+
+ it 'flushes the branch count cache' do
+ expect(repository).to receive(:expire_branch_count_cache)
+
+ repository.before_delete
+ end
+
it 'flushes the root ref cache' do
expect(repository).to receive(:expire_root_ref_cache)
@@ -1021,7 +1162,7 @@ describe Repository, models: true do
it 'does not flush the cache if the commit does not change any logos' do
diff = double(:diff, new_path: 'test.txt')
- expect(commit).to receive(:diffs).and_return([diff])
+ expect(commit).to receive(:raw_diffs).and_return([diff])
expect(cache).not_to receive(:expire)
repository.expire_avatar_cache(repository.root_ref, '123')
@@ -1030,7 +1171,7 @@ describe Repository, models: true do
it 'flushes the cache if the commit changes any of the logos' do
diff = double(:diff, new_path: Repository::AVATAR_FILES[0])
- expect(commit).to receive(:diffs).and_return([diff])
+ expect(commit).to receive(:raw_diffs).and_return([diff])
expect(cache).to receive(:expire).with(:avatar)
repository.expire_avatar_cache(repository.root_ref, '123')
@@ -1082,51 +1223,31 @@ describe Repository, models: true do
end
end
- describe '#local_branches' do
- it 'returns the local branches' do
- masterrev = repository.find_branch('master').target
- create_remote_branch('joe', 'remote_branch', masterrev)
- repository.add_branch(user, 'local_branch', masterrev)
-
- expect(repository.local_branches.any? { |branch| branch.name == 'remote_branch' }).to eq(false)
- expect(repository.local_branches.any? { |branch| branch.name == 'local_branch' }).to eq(true)
+ describe "#keep_around" do
+ it "does not fail if we attempt to reference bad commit" do
+ expect(repository.kept_around?('abc1234')).to be_falsey
end
- end
-
- describe '.clean_old_archives' do
- let(:path) { Gitlab.config.gitlab.repository_downloads_path }
- context 'when the downloads directory does not exist' do
- it 'does not remove any archives' do
- expect(File).to receive(:directory?).with(path).and_return(false)
-
- expect(Gitlab::Popen).not_to receive(:popen)
+ it "stores a reference to the specified commit sha so it isn't garbage collected" do
+ repository.keep_around(sample_commit.id)
- described_class.clean_old_archives
- end
+ expect(repository.kept_around?(sample_commit.id)).to be_truthy
end
- context 'when the downloads directory exists' do
- it 'removes old archives' do
- expect(File).to receive(:directory?).with(path).and_return(true)
-
- expect(Gitlab::Popen).to receive(:popen)
+ it "attempting to call keep_around on truncated ref does not fail" do
+ repository.keep_around(sample_commit.id)
+ ref = repository.send(:keep_around_ref_name, sample_commit.id)
+ path = File.join(repository.path, ref)
+ # Corrupt the reference
+ File.truncate(path, 0)
- described_class.clean_old_archives
- end
- end
- end
+ expect(repository.kept_around?(sample_commit.id)).to be_falsey
- describe "#keep_around" do
- it "stores a reference to the specified commit sha so it isn't garbage collected" do
repository.keep_around(sample_commit.id)
- expect(repository.kept_around?(sample_commit.id)).to be_truthy
- end
- end
+ expect(repository.kept_around?(sample_commit.id)).to be_falsey
- def create_remote_branch(remote_name, branch_name, target)
- rugged = repository.rugged
- rugged.references.create("refs/remotes/#{remote_name}/#{branch_name}", target)
+ File.delete(path)
+ end
end
end
diff --git a/spec/models/service_spec.rb b/spec/models/service_spec.rb
index 96bbbec9ea1..05056a4bb47 100644
--- a/spec/models/service_spec.rb
+++ b/spec/models/service_spec.rb
@@ -22,11 +22,11 @@ describe Service, models: true do
@testable = @service.can_test?
end
- describe :can_test do
+ describe '#can_test?' do
it { expect(@testable).to eq(true) }
end
- describe :test do
+ describe '#test' do
let(:data) { 'test' }
it 'test runs execute' do
@@ -45,7 +45,7 @@ describe Service, models: true do
@testable = @service.can_test?
end
- describe :can_test do
+ describe '#can_test?' do
it { expect(@testable).to eq(true) }
end
end
@@ -65,13 +65,13 @@ describe Service, models: true do
end
let(:project) { create(:project) }
- describe 'should be prefilled for projects pushover service' do
+ describe 'is prefilled for projects pushover service' do
before do
service_template
project.build_missing_services
end
- it "should have all fields prefilled" do
+ it "has all fields prefilled" do
service = project.pushover_service
expect(service.template).to eq(false)
expect(service.device).to eq('MyDevice')
diff --git a/spec/models/user_agent_detail_spec.rb b/spec/models/user_agent_detail_spec.rb
new file mode 100644
index 00000000000..a8c25766e73
--- /dev/null
+++ b/spec/models/user_agent_detail_spec.rb
@@ -0,0 +1,31 @@
+require 'rails_helper'
+
+describe UserAgentDetail, type: :model do
+ describe '.submittable?' do
+ it 'is submittable when not already submitted' do
+ detail = build(:user_agent_detail)
+
+ expect(detail.submittable?).to be_truthy
+ end
+
+ it 'is not submittable when already submitted' do
+ detail = build(:user_agent_detail, submitted: true)
+
+ expect(detail.submittable?).to be_falsey
+ end
+ end
+
+ describe '.valid?' do
+ it 'is valid with a subject' do
+ detail = build(:user_agent_detail)
+
+ expect(detail).to be_valid
+ end
+
+ it 'is invalid without a subject' do
+ detail = build(:user_agent_detail, subject: nil)
+
+ expect(detail).not_to be_valid
+ end
+ end
+end
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 3984b30ddf8..8eb0c5033c9 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -31,6 +31,8 @@ describe User, models: true do
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) }
+ it { is_expected.to have_many(:builds).dependent(:nullify) }
+ it { is_expected.to have_many(:pipelines).dependent(:nullify) }
describe '#group_members' do
it 'does not include group memberships for which user is a requester' do
@@ -87,9 +89,9 @@ describe User, models: true do
end
describe 'email' do
- context 'when no signup domains listed' do
+ context 'when no signup domains whitelisted' do
before do
- allow_any_instance_of(ApplicationSetting).to receive(:restricted_signup_domains).and_return([])
+ allow_any_instance_of(ApplicationSetting).to receive(:domain_whitelist).and_return([])
end
it 'accepts any email' do
@@ -98,9 +100,9 @@ describe User, models: true do
end
end
- context 'when a signup domain is listed and subdomains are allowed' do
+ context 'when a signup domain is whitelisted and subdomains are allowed' do
before do
- allow_any_instance_of(ApplicationSetting).to receive(:restricted_signup_domains).and_return(['example.com', '*.example.com'])
+ allow_any_instance_of(ApplicationSetting).to receive(:domain_whitelist).and_return(['example.com', '*.example.com'])
end
it 'accepts info@example.com' do
@@ -119,9 +121,9 @@ describe User, models: true do
end
end
- context 'when a signup domain is listed and subdomains are not allowed' do
+ context 'when a signup domain is whitelisted and subdomains are not allowed' do
before do
- allow_any_instance_of(ApplicationSetting).to receive(:restricted_signup_domains).and_return(['example.com'])
+ allow_any_instance_of(ApplicationSetting).to receive(:domain_whitelist).and_return(['example.com'])
end
it 'accepts info@example.com' do
@@ -140,6 +142,53 @@ describe User, models: true do
end
end
+ context 'domain blacklist' do
+ before do
+ allow_any_instance_of(ApplicationSetting).to receive(:domain_blacklist_enabled?).and_return(true)
+ allow_any_instance_of(ApplicationSetting).to receive(:domain_blacklist).and_return(['example.com'])
+ end
+
+ context 'when a signup domain is blacklisted' do
+ it 'accepts info@test.com' do
+ user = build(:user, email: 'info@test.com')
+ expect(user).to be_valid
+ end
+
+ it 'rejects info@example.com' do
+ user = build(:user, email: 'info@example.com')
+ expect(user).not_to be_valid
+ end
+ end
+
+ context 'when a signup domain is blacklisted but a wildcard subdomain is allowed' do
+ before do
+ allow_any_instance_of(ApplicationSetting).to receive(:domain_blacklist).and_return(['test.example.com'])
+ allow_any_instance_of(ApplicationSetting).to receive(:domain_whitelist).and_return(['*.example.com'])
+ end
+
+ it 'gives priority to whitelist and allow info@test.example.com' do
+ user = build(:user, email: 'info@test.example.com')
+ expect(user).to be_valid
+ end
+ end
+
+ context 'with both lists containing a domain' do
+ before do
+ allow_any_instance_of(ApplicationSetting).to receive(:domain_whitelist).and_return(['test.com'])
+ end
+
+ it 'accepts info@test.com' do
+ user = build(:user, email: 'info@test.com')
+ expect(user).to be_valid
+ end
+
+ it 'rejects info@example.com' do
+ user = build(:user, email: 'info@example.com')
+ expect(user).not_to be_valid
+ end
+ end
+ end
+
context 'owns_notification_email' do
it 'accepts temp_oauth_email emails' do
user = build(:user, email: "temp-email-for-oauth@example.com")
@@ -255,18 +304,18 @@ describe User, models: true do
end
describe '#generate_password' do
- it "should execute callback when force_random_password specified" do
+ it "executes callback when force_random_password specified" do
user = build(:user, force_random_password: true)
expect(user).to receive(:generate_password)
user.save
end
- it "should not generate password by default" do
+ it "does not generate password by default" do
user = create(:user, password: 'abcdefghe')
expect(user.password).to eq('abcdefghe')
end
- it "should generate password when forcing random password" do
+ it "generates password when forcing random password" do
allow(Devise).to receive(:friendly_token).and_return('123456789')
user = create(:user, password: 'abcdefg', force_random_password: true)
expect(user.password).to eq('12345678')
@@ -274,7 +323,7 @@ describe User, models: true do
end
describe 'authentication token' do
- it "should have authentication token" do
+ it "has authentication token" do
user = create(:user)
expect(user.authentication_token).not_to be_blank
end
@@ -381,7 +430,7 @@ describe User, models: true do
describe 'blocking user' do
let(:user) { create(:user, name: 'John Smith') }
- it "should block user" do
+ it "blocks user" do
user.block
expect(user.blocked?).to be_truthy
end
@@ -427,7 +476,7 @@ describe User, models: true do
end
end
- describe :not_in_project do
+ describe '.not_in_project' do
before do
User.delete_all
@user = create :user
@@ -452,7 +501,7 @@ describe User, models: true do
describe 'with defaults' do
let(:user) { User.new }
- it "should apply defaults to user" do
+ it "applies defaults to user" do
expect(user.projects_limit).to eq(Gitlab.config.gitlab.default_projects_limit)
expect(user.can_create_group).to eq(Gitlab.config.gitlab.default_can_create_group)
expect(user.theme_id).to eq(Gitlab.config.gitlab.default_theme)
@@ -463,7 +512,7 @@ describe User, models: true do
describe 'with default overrides' do
let(:user) { User.new(projects_limit: 123, can_create_group: false, can_create_team: true, theme_id: 1) }
- it "should apply defaults to user" do
+ it "applies defaults to user" do
expect(user.projects_limit).to eq(123)
expect(user.can_create_group).to be_falsey
expect(user.theme_id).to eq(1)
@@ -553,7 +602,7 @@ describe User, models: true do
describe 'by_username_or_id' do
let(:user1) { create(:user, username: 'foo') }
- it "should get the correct user" do
+ it "gets the correct user" do
expect(User.by_username_or_id(user1.id)).to eq(user1)
expect(User.by_username_or_id('foo')).to eq(user1)
expect(User.by_username_or_id(-1)).to be_nil
@@ -565,7 +614,7 @@ describe User, models: true do
let(:username) { 'John' }
let!(:user) { create(:user, username: username) }
- it 'should get the correct user' do
+ it 'gets the correct user' do
expect(User.by_login(user.email.upcase)).to eq user
expect(User.by_login(user.email)).to eq user
expect(User.by_login(username.downcase)).to eq user
@@ -590,29 +639,29 @@ describe User, models: true do
describe 'all_ssh_keys' do
it { is_expected.to have_many(:keys).dependent(:destroy) }
- it "should have all ssh keys" do
+ it "has all ssh keys" do
user = create :user
key = create :key, key: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQD33bWLBxu48Sev9Fert1yzEO4WGcWglWF7K/AwblIUFselOt/QdOL9DSjpQGxLagO1s9wl53STIO8qGS4Ms0EJZyIXOEFMjFJ5xmjSy+S37By4sG7SsltQEHMxtbtFOaW5LV2wCrX+rUsRNqLMamZjgjcPO0/EgGCXIGMAYW4O7cwGZdXWYIhQ1Vwy+CsVMDdPkPgBXqK7nR/ey8KMs8ho5fMNgB5hBw/AL9fNGhRw3QTD6Q12Nkhl4VZES2EsZqlpNnJttnPdp847DUsT6yuLRlfiQfz5Cn9ysHFdXObMN5VYIiPFwHeYCZp1X2S4fDZooRE8uOLTfxWHPXwrhqSH", user_id: user.id
- expect(user.all_ssh_keys).to include(key.key)
+ expect(user.all_ssh_keys).to include(a_string_starting_with(key.key))
end
end
- describe :avatar_type do
+ describe '#avatar_type' do
let(:user) { create(:user) }
- it "should be true if avatar is image" do
+ it "is true if avatar is image" do
user.update_attribute(:avatar, 'uploads/avatar.png')
expect(user.avatar_type).to be_truthy
end
- it "should be false if avatar is html page" do
+ it "is false if avatar is html page" do
user.update_attribute(:avatar, 'uploads/avatar.html')
expect(user.avatar_type).to eq(["only images allowed"])
end
end
- describe :requires_ldap_check? do
+ describe '#requires_ldap_check?' do
let(:user) { User.new }
it 'is false when LDAP is disabled' do
@@ -651,7 +700,7 @@ describe User, models: true do
end
context 'ldap synchronized user' do
- describe :ldap_user? do
+ describe '#ldap_user?' do
it 'is true if provider name starts with ldap' do
user = create(:omniauth_user, provider: 'ldapmain')
expect(user.ldap_user?).to be_truthy
@@ -668,7 +717,7 @@ describe User, models: true do
end
end
- describe :ldap_identity do
+ describe '#ldap_identity' do
it 'returns ldap identity' do
user = create :omniauth_user
expect(user.ldap_identity.provider).not_to be_empty
@@ -825,7 +874,7 @@ describe User, models: true do
end
end
- describe :can_be_removed? do
+ describe '#can_be_removed?' do
subject { create(:user) }
context 'no owned groups' do
@@ -846,7 +895,9 @@ describe User, models: true do
subject { create(:user) }
let!(:project1) { create(:project) }
let!(:project2) { create(:project, forked_from_project: project1) }
- let!(:push_data) { Gitlab::PushDataBuilder.build_sample(project2, subject) }
+ let!(:push_data) do
+ Gitlab::DataBuilder::Push.build_sample(project2, subject)
+ end
let!(:push_event) { create(:event, action: Event::PUSHED, project: project2, target: project1, author: subject, data: push_data) }
before do
@@ -869,6 +920,16 @@ describe User, models: true do
expect(subject.recent_push).to eq(nil)
end
+
+ it "includes push events on any of the provided projects" do
+ expect(subject.recent_push(project1)).to eq(nil)
+ expect(subject.recent_push(project2)).to eq(push_event)
+
+ push_data1 = Gitlab::DataBuilder::Push.build_sample(project1, subject)
+ push_event1 = create(:event, action: Event::PUSHED, project: project1, target: project1, author: subject, data: push_data1)
+
+ expect(subject.recent_push([project1, project2])).to eq(push_event1) # Newest
+ end
end
describe '#authorized_groups' do
@@ -885,16 +946,72 @@ describe User, models: true do
end
describe '#authorized_projects' do
- let!(:user) { create(:user) }
- let!(:private_project) { create(:project, :private) }
+ context 'with a minimum access level' do
+ it 'includes projects for which the user is an owner' do
+ user = create(:user)
+ project = create(:empty_project, :private, namespace: user.namespace)
- before do
- private_project.team << [user, Gitlab::Access::MASTER]
+ expect(user.authorized_projects(Gitlab::Access::REPORTER))
+ .to contain_exactly(project)
+ end
+
+ it 'includes projects for which the user is a master' do
+ user = create(:user)
+ project = create(:empty_project, :private)
+
+ project.team << [user, Gitlab::Access::MASTER]
+
+ expect(user.authorized_projects(Gitlab::Access::REPORTER))
+ .to contain_exactly(project)
+ end
end
+ end
+
+ describe '#projects_where_can_admin_issues' do
+ let(:user) { create(:user) }
- subject { user.authorized_projects }
+ it 'includes projects for which the user access level is above or equal to reporter' do
+ create(:project)
+ reporter_project = create(:project)
+ developer_project = create(:project)
+ master_project = create(:project)
- it { is_expected.to eq([private_project]) }
+ reporter_project.team << [user, :reporter]
+ developer_project.team << [user, :developer]
+ master_project.team << [user, :master]
+
+ expect(user.projects_where_can_admin_issues.to_a).to eq([master_project, developer_project, reporter_project])
+ expect(user.can?(:admin_issue, master_project)).to eq(true)
+ expect(user.can?(:admin_issue, developer_project)).to eq(true)
+ expect(user.can?(:admin_issue, reporter_project)).to eq(true)
+ end
+
+ it 'does not include for which the user access level is below reporter' do
+ project = create(:project)
+ guest_project = create(:project)
+
+ guest_project.team << [user, :guest]
+
+ expect(user.projects_where_can_admin_issues.to_a).to be_empty
+ expect(user.can?(:admin_issue, guest_project)).to eq(false)
+ expect(user.can?(:admin_issue, project)).to eq(false)
+ end
+
+ it 'does not include archived projects' do
+ project = create(:project)
+ project.update_attributes(archived: true)
+
+ expect(user.projects_where_can_admin_issues.to_a).to be_empty
+ expect(user.can?(:admin_issue, project)).to eq(false)
+ end
+
+ it 'does not include projects for which issues are disabled' do
+ project = create(:project)
+ project.update_attributes(issues_enabled: false)
+
+ expect(user.projects_where_can_admin_issues.to_a).to be_empty
+ expect(user.can?(:admin_issue, project)).to eq(false)
+ end
end
describe '#ci_authorized_runners' do
diff --git a/spec/models/wiki_page_spec.rb b/spec/models/wiki_page_spec.rb
index ddc49495eda..5c34b1b0a30 100644
--- a/spec/models/wiki_page_spec.rb
+++ b/spec/models/wiki_page_spec.rb
@@ -147,12 +147,12 @@ describe WikiPage, models: true do
@page = wiki.find_page("Delete Page")
end
- it "should delete the page" do
+ it "deletes the page" do
@page.delete
expect(wiki.pages).to be_empty
end
- it "should return true" do
+ it "returns true" do
expect(@page.delete).to eq(true)
end
end
@@ -183,7 +183,7 @@ describe WikiPage, models: true do
destroy_page("Title")
end
- it "should be replace a hyphen to a space" do
+ it "replaces a hyphen to a space" do
@page.title = "Import-existing-repositories-into-GitLab"
expect(@page.title).to eq("Import existing repositories into GitLab")
end
diff --git a/spec/requests/api/access_requests_spec.rb b/spec/requests/api/access_requests_spec.rb
new file mode 100644
index 00000000000..d78494b76fa
--- /dev/null
+++ b/spec/requests/api/access_requests_spec.rb
@@ -0,0 +1,246 @@
+require 'spec_helper'
+
+describe API::AccessRequests, api: true do
+ include ApiHelpers
+
+ let(:master) { create(:user) }
+ let(:developer) { create(:user) }
+ let(:access_requester) { create(:user) }
+ let(:stranger) { create(:user) }
+
+ let(:project) do
+ project = create(:project, :public, creator_id: master.id, namespace: master.namespace)
+ project.team << [developer, :developer]
+ project.team << [master, :master]
+ project.request_access(access_requester)
+ project
+ end
+
+ let(:group) do
+ group = create(:group, :public)
+ group.add_developer(developer)
+ group.add_owner(master)
+ group.request_access(access_requester)
+ group
+ end
+
+ shared_examples 'GET /:sources/:id/access_requests' do |source_type|
+ context "with :sources == #{source_type.pluralize}" do
+ it_behaves_like 'a 404 response when source is private' do
+ let(:route) { get api("/#{source_type.pluralize}/#{source.id}/access_requests", stranger) }
+ end
+
+ context 'when authenticated as a non-master/owner' do
+ %i[developer access_requester stranger].each do |type|
+ context "as a #{type}" do
+ it 'returns 403' do
+ user = public_send(type)
+ get api("/#{source_type.pluralize}/#{source.id}/access_requests", user)
+
+ expect(response).to have_http_status(403)
+ end
+ end
+ end
+ end
+
+ context 'when authenticated as a master/owner' do
+ it 'returns access requesters' do
+ get api("/#{source_type.pluralize}/#{source.id}/access_requests", master)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.size).to eq(1)
+ end
+ end
+ end
+ end
+
+ shared_examples 'POST /:sources/:id/access_requests' do |source_type|
+ context "with :sources == #{source_type.pluralize}" do
+ it_behaves_like 'a 404 response when source is private' do
+ let(:route) { post api("/#{source_type.pluralize}/#{source.id}/access_requests", stranger) }
+ end
+
+ context 'when authenticated as a member' do
+ %i[developer master].each do |type|
+ context "as a #{type}" do
+ it 'returns 400' do
+ expect do
+ user = public_send(type)
+ post api("/#{source_type.pluralize}/#{source.id}/access_requests", user)
+
+ expect(response).to have_http_status(400)
+ end.not_to change { source.requesters.count }
+ end
+ end
+ end
+ end
+
+ context 'when authenticated as an access requester' do
+ it 'returns 400' do
+ expect do
+ post api("/#{source_type.pluralize}/#{source.id}/access_requests", access_requester)
+
+ expect(response).to have_http_status(400)
+ end.not_to change { source.requesters.count }
+ end
+ end
+
+ context 'when authenticated as a stranger' do
+ it 'returns 201' do
+ expect do
+ post api("/#{source_type.pluralize}/#{source.id}/access_requests", stranger)
+
+ expect(response).to have_http_status(201)
+ end.to change { source.requesters.count }.by(1)
+
+ # User attributes
+ expect(json_response['id']).to eq(stranger.id)
+ expect(json_response['name']).to eq(stranger.name)
+ expect(json_response['username']).to eq(stranger.username)
+ expect(json_response['state']).to eq(stranger.state)
+ expect(json_response['avatar_url']).to eq(stranger.avatar_url)
+ expect(json_response['web_url']).to eq(Gitlab::Routing.url_helpers.user_url(stranger))
+
+ # Member attributes
+ expect(json_response['requested_at']).to be_present
+ end
+ end
+ end
+ end
+
+ shared_examples 'PUT /:sources/:id/access_requests/:user_id/approve' do |source_type|
+ context "with :sources == #{source_type.pluralize}" do
+ it_behaves_like 'a 404 response when source is private' do
+ let(:route) { put api("/#{source_type.pluralize}/#{source.id}/access_requests/#{access_requester.id}/approve", stranger) }
+ end
+
+ context 'when authenticated as a non-master/owner' do
+ %i[developer access_requester stranger].each do |type|
+ context "as a #{type}" do
+ it 'returns 403' do
+ user = public_send(type)
+ put api("/#{source_type.pluralize}/#{source.id}/access_requests/#{access_requester.id}/approve", user)
+
+ expect(response).to have_http_status(403)
+ end
+ end
+ end
+ end
+
+ context 'when authenticated as a master/owner' do
+ it 'returns 201' do
+ expect do
+ put api("/#{source_type.pluralize}/#{source.id}/access_requests/#{access_requester.id}/approve", master),
+ access_level: Member::MASTER
+
+ expect(response).to have_http_status(201)
+ end.to change { source.members.count }.by(1)
+ # User attributes
+ expect(json_response['id']).to eq(access_requester.id)
+ expect(json_response['name']).to eq(access_requester.name)
+ expect(json_response['username']).to eq(access_requester.username)
+ expect(json_response['state']).to eq(access_requester.state)
+ expect(json_response['avatar_url']).to eq(access_requester.avatar_url)
+ expect(json_response['web_url']).to eq(Gitlab::Routing.url_helpers.user_url(access_requester))
+
+ # Member attributes
+ expect(json_response['access_level']).to eq(Member::MASTER)
+ end
+
+ context 'user_id does not match an existing access requester' do
+ it 'returns 404' do
+ expect do
+ put api("/#{source_type.pluralize}/#{source.id}/access_requests/#{stranger.id}/approve", master)
+
+ expect(response).to have_http_status(404)
+ end.not_to change { source.members.count }
+ end
+ end
+ end
+ end
+ end
+
+ shared_examples 'DELETE /:sources/:id/access_requests/:user_id' do |source_type|
+ context "with :sources == #{source_type.pluralize}" do
+ it_behaves_like 'a 404 response when source is private' do
+ let(:route) { delete api("/#{source_type.pluralize}/#{source.id}/access_requests/#{access_requester.id}", stranger) }
+ end
+
+ context 'when authenticated as a non-master/owner' do
+ %i[developer stranger].each do |type|
+ context "as a #{type}" do
+ it 'returns 403' do
+ user = public_send(type)
+ delete api("/#{source_type.pluralize}/#{source.id}/access_requests/#{access_requester.id}", user)
+
+ expect(response).to have_http_status(403)
+ end
+ end
+ end
+ end
+
+ context 'when authenticated as the access requester' do
+ it 'returns 200' do
+ expect do
+ delete api("/#{source_type.pluralize}/#{source.id}/access_requests/#{access_requester.id}", access_requester)
+
+ expect(response).to have_http_status(200)
+ end.to change { source.requesters.count }.by(-1)
+ end
+ end
+
+ context 'when authenticated as a master/owner' do
+ it 'returns 200' do
+ expect do
+ delete api("/#{source_type.pluralize}/#{source.id}/access_requests/#{access_requester.id}", master)
+
+ expect(response).to have_http_status(200)
+ end.to change { source.requesters.count }.by(-1)
+ end
+
+ context 'user_id does not match an existing access requester' do
+ it 'returns 404' do
+ expect do
+ delete api("/#{source_type.pluralize}/#{source.id}/access_requests/#{stranger.id}", master)
+
+ expect(response).to have_http_status(404)
+ end.not_to change { source.requesters.count }
+ end
+ end
+ end
+ end
+ end
+
+ it_behaves_like 'GET /:sources/:id/access_requests', 'project' do
+ let(:source) { project }
+ end
+
+ it_behaves_like 'GET /:sources/:id/access_requests', 'group' do
+ let(:source) { group }
+ end
+
+ it_behaves_like 'POST /:sources/:id/access_requests', 'project' do
+ let(:source) { project }
+ end
+
+ it_behaves_like 'POST /:sources/:id/access_requests', 'group' do
+ let(:source) { group }
+ end
+
+ it_behaves_like 'PUT /:sources/:id/access_requests/:user_id/approve', 'project' do
+ let(:source) { project }
+ end
+
+ it_behaves_like 'PUT /:sources/:id/access_requests/:user_id/approve', 'group' do
+ let(:source) { group }
+ end
+
+ it_behaves_like 'DELETE /:sources/:id/access_requests/:user_id', 'project' do
+ let(:source) { project }
+ end
+
+ it_behaves_like 'DELETE /:sources/:id/access_requests/:user_id', 'group' do
+ let(:source) { group }
+ end
+end
diff --git a/spec/requests/api/api_helpers_spec.rb b/spec/requests/api/api_helpers_spec.rb
index 83025953889..bbdf8f03c2b 100644
--- a/spec/requests/api/api_helpers_spec.rb
+++ b/spec/requests/api/api_helpers_spec.rb
@@ -3,6 +3,7 @@ require 'spec_helper'
describe API::Helpers, api: true do
include API::Helpers
include ApiHelpers
+ include SentryHelper
let(:user) { create(:user) }
let(:admin) { create(:admin) }
@@ -41,19 +42,19 @@ describe API::Helpers, api: true do
describe ".current_user" do
describe "when authenticating using a user's private token" do
- it "should return nil for an invalid token" do
+ it "returns nil for an invalid token" do
env[API::Helpers::PRIVATE_TOKEN_HEADER] = 'invalid token'
allow_any_instance_of(self.class).to receive(:doorkeeper_guard){ false }
expect(current_user).to be_nil
end
- it "should return nil for a user without access" do
+ it "returns nil for a user without access" do
env[API::Helpers::PRIVATE_TOKEN_HEADER] = user.private_token
- allow(Gitlab::UserAccess).to receive(:allowed?).and_return(false)
+ allow_any_instance_of(Gitlab::UserAccess).to receive(:allowed?).and_return(false)
expect(current_user).to be_nil
end
- it "should leave user as is when sudo not specified" do
+ it "leaves user as is when sudo not specified" do
env[API::Helpers::PRIVATE_TOKEN_HEADER] = user.private_token
expect(current_user).to eq(user)
clear_env
@@ -65,19 +66,19 @@ describe API::Helpers, api: true do
describe "when authenticating using a user's personal access tokens" do
let(:personal_access_token) { create(:personal_access_token, user: user) }
- it "should return nil for an invalid token" do
+ it "returns nil for an invalid token" do
env[API::Helpers::PRIVATE_TOKEN_HEADER] = 'invalid token'
allow_any_instance_of(self.class).to receive(:doorkeeper_guard){ false }
expect(current_user).to be_nil
end
- it "should return nil for a user without access" do
+ it "returns nil for a user without access" do
env[API::Helpers::PRIVATE_TOKEN_HEADER] = personal_access_token.token
- allow(Gitlab::UserAccess).to receive(:allowed?).and_return(false)
+ allow_any_instance_of(Gitlab::UserAccess).to receive(:allowed?).and_return(false)
expect(current_user).to be_nil
end
- it "should leave user as is when sudo not specified" do
+ it "leaves user as is when sudo not specified" do
env[API::Helpers::PRIVATE_TOKEN_HEADER] = personal_access_token.token
expect(current_user).to eq(user)
clear_env
@@ -100,7 +101,7 @@ describe API::Helpers, api: true do
end
end
- it "should change current user to sudo when admin" do
+ it "changes current user to sudo when admin" do
set_env(admin, user.id)
expect(current_user).to eq(user)
set_param(admin, user.id)
@@ -111,7 +112,7 @@ describe API::Helpers, api: true do
expect(current_user).to eq(user)
end
- it "should throw an error when the current user is not an admin and attempting to sudo" do
+ it "throws an error when the current user is not an admin and attempting to sudo" do
set_env(user, admin.id)
expect { current_user }.to raise_error(Exception)
set_param(user, admin.id)
@@ -122,7 +123,7 @@ describe API::Helpers, api: true do
expect { current_user }.to raise_error(Exception)
end
- it "should throw an error when the user cannot be found for a given id" do
+ it "throws an error when the user cannot be found for a given id" do
id = user.id + admin.id
expect(user.id).not_to eq(id)
expect(admin.id).not_to eq(id)
@@ -133,7 +134,7 @@ describe API::Helpers, api: true do
expect { current_user }.to raise_error(Exception)
end
- it "should throw an error when the user cannot be found for a given username" do
+ it "throws an error when the user cannot be found for a given username" do
username = "#{user.username}#{admin.username}"
expect(user.username).not_to eq(username)
expect(admin.username).not_to eq(username)
@@ -144,7 +145,7 @@ describe API::Helpers, api: true do
expect { current_user }.to raise_error(Exception)
end
- it "should handle sudo's to oneself" do
+ it "handles sudo's to oneself" do
set_env(admin, admin.id)
expect(current_user).to eq(admin)
set_param(admin, admin.id)
@@ -155,7 +156,7 @@ describe API::Helpers, api: true do
expect(current_user).to eq(admin)
end
- it "should handle multiple sudo's to oneself" do
+ it "handles multiple sudo's to oneself" do
set_env(admin, user.id)
expect(current_user).to eq(user)
expect(current_user).to eq(user)
@@ -171,7 +172,7 @@ describe API::Helpers, api: true do
expect(current_user).to eq(user)
end
- it "should handle multiple sudo's to oneself using string ids" do
+ it "handles multiple sudo's to oneself using string ids" do
set_env(admin, user.id.to_s)
expect(current_user).to eq(user)
expect(current_user).to eq(user)
@@ -183,7 +184,7 @@ describe API::Helpers, api: true do
end
describe '.sudo_identifier' do
- it "should return integers when input is an int" do
+ it "returns integers when input is an int" do
set_env(admin, '123')
expect(sudo_identifier).to eq(123)
set_env(admin, '0001234567890')
@@ -195,7 +196,7 @@ describe API::Helpers, api: true do
expect(sudo_identifier).to eq(1234567890)
end
- it "should return string when input is an is not an int" do
+ it "returns string when input is an is not an int" do
set_env(admin, '12.30')
expect(sudo_identifier).to eq("12.30")
set_env(admin, 'hello')
@@ -211,4 +212,53 @@ describe API::Helpers, api: true do
expect(sudo_identifier).to eq(' 123')
end
end
+
+ describe '.to_boolean' do
+ it 'converts a valid string to a boolean' do
+ expect(to_boolean('true')).to be_truthy
+ expect(to_boolean('YeS')).to be_truthy
+ expect(to_boolean('t')).to be_truthy
+ expect(to_boolean('1')).to be_truthy
+ expect(to_boolean('ON')).to be_truthy
+ expect(to_boolean('FaLse')).to be_falsy
+ expect(to_boolean('F')).to be_falsy
+ expect(to_boolean('NO')).to be_falsy
+ expect(to_boolean('n')).to be_falsy
+ expect(to_boolean('0')).to be_falsy
+ expect(to_boolean('oFF')).to be_falsy
+ end
+
+ it 'converts an invalid string to nil' do
+ expect(to_boolean('fals')).to be_nil
+ expect(to_boolean('yeah')).to be_nil
+ expect(to_boolean('')).to be_nil
+ expect(to_boolean(nil)).to be_nil
+ end
+ end
+
+ describe '.handle_api_exception' do
+ before do
+ allow_any_instance_of(self.class).to receive(:sentry_enabled?).and_return(true)
+ allow_any_instance_of(self.class).to receive(:rack_response)
+ end
+
+ it 'does not report a MethodNotAllowed exception to Sentry' do
+ exception = Grape::Exceptions::MethodNotAllowed.new({ 'X-GitLab-Test' => '1' })
+ allow(exception).to receive(:backtrace).and_return(caller)
+
+ expect(Raven).not_to receive(:capture_exception).with(exception)
+
+ handle_api_exception(exception)
+ end
+
+ it 'does report RuntimeError to Sentry' do
+ exception = RuntimeError.new('test error')
+ allow(exception).to receive(:backtrace).and_return(caller)
+
+ expect_any_instance_of(self.class).to receive(:sentry_context)
+ expect(Raven).to receive(:capture_exception).with(exception)
+
+ handle_api_exception(exception)
+ end
+ end
end
diff --git a/spec/requests/api/award_emoji_spec.rb b/spec/requests/api/award_emoji_spec.rb
index 72a6d45f47d..73c268c0d1e 100644
--- a/spec/requests/api/award_emoji_spec.rb
+++ b/spec/requests/api/award_emoji_spec.rb
@@ -22,7 +22,7 @@ describe API::API, api: true do
expect(json_response.first['name']).to eq(award_emoji.name)
end
- it "should return a 404 error when issue id not found" do
+ it "returns a 404 error when issue id not found" do
get api("/projects/#{project.id}/issues/12345/award_emoji", user)
expect(response).to have_http_status(404)
@@ -124,17 +124,33 @@ describe API::API, api: true do
expect(json_response['user']['username']).to eq(user.username)
end
- it "should return a 400 bad request error if the name is not given" do
+ it "returns a 400 bad request error if the name is not given" do
post api("/projects/#{project.id}/issues/#{issue.id}/award_emoji", user)
expect(response).to have_http_status(400)
end
- it "should return a 401 unauthorized error if the user is not authenticated" do
+ it "returns a 401 unauthorized error if the user is not authenticated" do
post api("/projects/#{project.id}/issues/#{issue.id}/award_emoji"), name: 'thumbsup'
expect(response).to have_http_status(401)
end
+
+ it "normalizes +1 as thumbsup award" do
+ post api("/projects/#{project.id}/issues/#{issue.id}/award_emoji", user), name: '+1'
+
+ expect(issue.award_emoji.last.name).to eq("thumbsup")
+ end
+
+ context 'when the emoji already has been awarded' do
+ it 'returns a 404 status code' do
+ post api("/projects/#{project.id}/issues/#{issue.id}/award_emoji", user), name: 'thumbsup'
+ post api("/projects/#{project.id}/issues/#{issue.id}/award_emoji", user), name: 'thumbsup'
+
+ expect(response).to have_http_status(404)
+ expect(json_response["message"]).to match("has already been taken")
+ end
+ end
end
end
@@ -147,6 +163,22 @@ describe API::API, api: true do
expect(response).to have_http_status(201)
expect(json_response['user']['username']).to eq(user.username)
end
+
+ it "normalizes +1 as thumbsup award" do
+ post api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note.id}/award_emoji", user), name: '+1'
+
+ expect(note.award_emoji.last.name).to eq("thumbsup")
+ end
+
+ context 'when the emoji already has been awarded' do
+ it 'returns a 404 status code' do
+ post api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note.id}/award_emoji", user), name: 'rocket'
+ post api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note.id}/award_emoji", user), name: 'rocket'
+
+ expect(response).to have_http_status(404)
+ expect(json_response["message"]).to match("has already been taken")
+ end
+ end
end
describe 'DELETE /projects/:id/awardable/:awardable_id/award_emoji/:award_id' do
diff --git a/spec/requests/api/branches_spec.rb b/spec/requests/api/branches_spec.rb
index b11ca26ee68..3fd989dd7a6 100644
--- a/spec/requests/api/branches_spec.rb
+++ b/spec/requests/api/branches_spec.rb
@@ -13,7 +13,7 @@ describe API::API, api: true do
let!(:branch_sha) { '0b4bc9a49b562e85de7cc9e834518ea6828729b9' }
describe "GET /projects/:id/repository/branches" do
- it "should return an array of project branches" do
+ it "returns an array of project branches" do
project.repository.expire_cache
get api("/projects/#{project.id}/repository/branches", user)
@@ -25,47 +25,130 @@ describe API::API, api: true do
end
describe "GET /projects/:id/repository/branches/:branch" do
- it "should return the branch information for a single branch" do
+ it "returns the branch information for a single branch" do
get api("/projects/#{project.id}/repository/branches/#{branch_name}", user)
expect(response).to have_http_status(200)
expect(json_response['name']).to eq(branch_name)
expect(json_response['commit']['id']).to eq(branch_sha)
expect(json_response['protected']).to eq(false)
+ expect(json_response['developers_can_push']).to eq(false)
+ expect(json_response['developers_can_merge']).to eq(false)
end
- it "should return a 403 error if guest" do
+ it "returns a 403 error if guest" do
get api("/projects/#{project.id}/repository/branches", user2)
expect(response).to have_http_status(403)
end
- it "should return a 404 error if branch is not available" do
+ it "returns a 404 error if branch is not available" do
get api("/projects/#{project.id}/repository/branches/unknown", user)
expect(response).to have_http_status(404)
end
end
- describe "PUT /projects/:id/repository/branches/:branch/protect" do
- it "should protect a single branch" do
+ describe 'PUT /projects/:id/repository/branches/:branch/protect' do
+ it 'protects a single branch' do
put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user)
+
expect(response).to have_http_status(200)
+ expect(json_response['name']).to eq(branch_name)
+ expect(json_response['commit']['id']).to eq(branch_sha)
+ expect(json_response['protected']).to eq(true)
+ expect(json_response['developers_can_push']).to eq(false)
+ expect(json_response['developers_can_merge']).to eq(false)
+ end
+
+ it 'protects a single branch and developers can push' do
+ put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user),
+ developers_can_push: true
+
+ expect(response).to have_http_status(200)
+ expect(json_response['name']).to eq(branch_name)
+ expect(json_response['commit']['id']).to eq(branch_sha)
+ expect(json_response['protected']).to eq(true)
+ expect(json_response['developers_can_push']).to eq(true)
+ expect(json_response['developers_can_merge']).to eq(false)
+ end
+ it 'protects a single branch and developers can merge' do
+ put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user),
+ developers_can_merge: true
+
+ expect(response).to have_http_status(200)
expect(json_response['name']).to eq(branch_name)
expect(json_response['commit']['id']).to eq(branch_sha)
expect(json_response['protected']).to eq(true)
+ expect(json_response['developers_can_push']).to eq(false)
+ expect(json_response['developers_can_merge']).to eq(true)
+ end
+
+ it 'protects a single branch and developers can push and merge' do
+ put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user),
+ developers_can_push: true, developers_can_merge: true
+
+ expect(response).to have_http_status(200)
+ expect(json_response['name']).to eq(branch_name)
+ expect(json_response['commit']['id']).to eq(branch_sha)
+ expect(json_response['protected']).to eq(true)
+ expect(json_response['developers_can_push']).to eq(true)
+ expect(json_response['developers_can_merge']).to eq(true)
+ end
+
+ it 'protects a single branch and developers cannot push and merge' do
+ put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user),
+ developers_can_push: 'tru', developers_can_merge: 'tr'
+
+ expect(response).to have_http_status(200)
+ expect(json_response['name']).to eq(branch_name)
+ expect(json_response['commit']['id']).to eq(branch_sha)
+ expect(json_response['protected']).to eq(true)
+ expect(json_response['developers_can_push']).to eq(false)
+ expect(json_response['developers_can_merge']).to eq(false)
+ end
+
+ context 'on a protected branch' do
+ let(:protected_branch) { 'foo' }
+
+ before do
+ project.repository.add_branch(user, protected_branch, 'master')
+ create(:protected_branch, :developers_can_push, :developers_can_merge, project: project, name: protected_branch)
+ end
+
+ it 'updates that a developer can push' do
+ put api("/projects/#{project.id}/repository/branches/#{protected_branch}/protect", user),
+ developers_can_push: false, developers_can_merge: false
+
+ expect(response).to have_http_status(200)
+ expect(json_response['name']).to eq(protected_branch)
+ expect(json_response['protected']).to eq(true)
+ expect(json_response['developers_can_push']).to eq(false)
+ expect(json_response['developers_can_merge']).to eq(false)
+ end
+
+ it 'does not update that a developer can push' do
+ put api("/projects/#{project.id}/repository/branches/#{protected_branch}/protect", user),
+ developers_can_push: 'foobar', developers_can_merge: 'foo'
+
+ expect(response).to have_http_status(200)
+ expect(json_response['name']).to eq(protected_branch)
+ expect(json_response['protected']).to eq(true)
+ expect(json_response['developers_can_push']).to eq(true)
+ expect(json_response['developers_can_merge']).to eq(true)
+ end
end
- it "should return a 404 error if branch not found" do
+ it "returns a 404 error if branch not found" do
put api("/projects/#{project.id}/repository/branches/unknown/protect", user)
expect(response).to have_http_status(404)
end
- it "should return a 403 error if guest" do
+ it "returns a 403 error if guest" do
put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user2)
expect(response).to have_http_status(403)
end
- it "should return success when protect branch again" do
+ it "returns success when protect branch again" do
put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user)
put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user)
expect(response).to have_http_status(200)
@@ -73,7 +156,7 @@ describe API::API, api: true do
end
describe "PUT /projects/:id/repository/branches/:branch/unprotect" do
- it "should unprotect a single branch" do
+ it "unprotects a single branch" do
put api("/projects/#{project.id}/repository/branches/#{branch_name}/unprotect", user)
expect(response).to have_http_status(200)
@@ -82,12 +165,12 @@ describe API::API, api: true do
expect(json_response['protected']).to eq(false)
end
- it "should return success when unprotect branch" do
+ it "returns success when unprotect branch" do
put api("/projects/#{project.id}/repository/branches/unknown/unprotect", user)
expect(response).to have_http_status(404)
end
- it "should return success when unprotect branch again" do
+ it "returns success when unprotect branch again" do
put api("/projects/#{project.id}/repository/branches/#{branch_name}/unprotect", user)
put api("/projects/#{project.id}/repository/branches/#{branch_name}/unprotect", user)
expect(response).to have_http_status(200)
@@ -95,7 +178,7 @@ describe API::API, api: true do
end
describe "POST /projects/:id/repository/branches" do
- it "should create a new branch" do
+ it "creates a new branch" do
post api("/projects/#{project.id}/repository/branches", user),
branch_name: 'feature1',
ref: branch_sha
@@ -106,14 +189,14 @@ describe API::API, api: true do
expect(json_response['commit']['id']).to eq(branch_sha)
end
- it "should deny for user without push access" do
+ it "denies for user without push access" do
post api("/projects/#{project.id}/repository/branches", user2),
branch_name: branch_name,
ref: branch_sha
expect(response).to have_http_status(403)
end
- it 'should return 400 if branch name is invalid' do
+ it 'returns 400 if branch name is invalid' do
post api("/projects/#{project.id}/repository/branches", user),
branch_name: 'new design',
ref: branch_sha
@@ -121,7 +204,7 @@ describe API::API, api: true do
expect(json_response['message']).to eq('Branch name is invalid')
end
- it 'should return 400 if branch already exists' do
+ it 'returns 400 if branch already exists' do
post api("/projects/#{project.id}/repository/branches", user),
branch_name: 'new_design1',
ref: branch_sha
@@ -134,7 +217,7 @@ describe API::API, api: true do
expect(json_response['message']).to eq('Branch already exists')
end
- it 'should return 400 if ref name is invalid' do
+ it 'returns 400 if ref name is invalid' do
post api("/projects/#{project.id}/repository/branches", user),
branch_name: 'new_design3',
ref: 'foo'
@@ -148,25 +231,25 @@ describe API::API, api: true do
allow_any_instance_of(Repository).to receive(:rm_branch).and_return(true)
end
- it "should remove branch" do
+ it "removes branch" do
delete api("/projects/#{project.id}/repository/branches/#{branch_name}", user)
expect(response).to have_http_status(200)
expect(json_response['branch_name']).to eq(branch_name)
end
- it 'should return 404 if branch not exists' do
+ it 'returns 404 if branch not exists' do
delete api("/projects/#{project.id}/repository/branches/foobar", user)
expect(response).to have_http_status(404)
end
- it "should remove protected branch" do
- project.protected_branches.create(name: branch_name)
+ it "removes protected branch" do
+ create(:protected_branch, project: project, name: branch_name)
delete api("/projects/#{project.id}/repository/branches/#{branch_name}", user)
expect(response).to have_http_status(405)
expect(json_response['message']).to eq('Protected branch cant be removed')
end
- it "should not remove HEAD branch" do
+ it "does not remove HEAD branch" do
delete api("/projects/#{project.id}/repository/branches/master", user)
expect(response).to have_http_status(405)
expect(json_response['message']).to eq('Cannot remove HEAD branch')
diff --git a/spec/requests/api/builds_spec.rb b/spec/requests/api/builds_spec.rb
index f5b39c3d698..9a17a705b1e 100644
--- a/spec/requests/api/builds_spec.rb
+++ b/spec/requests/api/builds_spec.rb
@@ -1,15 +1,15 @@
require 'spec_helper'
-describe API::API, api: true do
+describe API::API, api: true do
include ApiHelpers
let(:user) { create(:user) }
let(:api_user) { user }
- let(:user2) { create(:user) }
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!(:pipeline) { create(:ci_pipeline, project: project, sha: project.commit.id) }
+ let(:reporter) { create(:project_member, :reporter, project: project) }
+ let(:guest) { create(:project_member, :guest, project: project) }
+ let!(:pipeline) { create(:ci_empty_pipeline, project: project, sha: project.commit.id, ref: project.default_branch) }
let!(:build) { create(:ci_build, pipeline: pipeline) }
describe 'GET /projects/:id/builds ' do
@@ -18,7 +18,7 @@ describe API::API, api: true do
before { get api("/projects/#{project.id}/builds?#{query}", api_user) }
context 'authorized user' do
- it 'should return project builds' do
+ it 'returns project builds' do
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
end
@@ -84,7 +84,7 @@ describe API::API, api: true do
get api("/projects/#{project.id}/repository/commits/#{project.commit.id}/builds", api_user)
end
- it 'should return project builds for specific commit' do
+ it 'returns project builds for specific commit' do
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.size).to eq 2
@@ -113,7 +113,7 @@ describe API::API, api: true do
get api("/projects/#{project.id}/repository/commits/#{project.commit.id}/builds", nil)
end
- it 'should not return project builds' do
+ it 'does not return project builds' do
expect(response).to have_http_status(401)
expect(json_response.except('message')).to be_empty
end
@@ -125,7 +125,7 @@ describe API::API, api: true do
before { get api("/projects/#{project.id}/builds/#{build.id}", api_user) }
context 'authorized user' do
- it 'should return specific build data' do
+ it 'returns specific build data' do
expect(response).to have_http_status(200)
expect(json_response['name']).to eq('test')
end
@@ -134,7 +134,7 @@ describe API::API, api: true do
context 'unauthorized user' do
let(:api_user) { nil }
- it 'should not return specific build data' do
+ it 'does not return specific build data' do
expect(response).to have_http_status(401)
end
end
@@ -152,7 +152,7 @@ describe API::API, api: true do
'Content-Disposition' => 'attachment; filename=ci_build_artifacts.zip' }
end
- it 'should return specific build artifacts' do
+ it 'returns specific build artifacts' do
expect(response).to have_http_status(200)
expect(response.headers).to include(download_headers)
end
@@ -161,24 +161,122 @@ describe API::API, api: true do
context 'unauthorized user' do
let(:api_user) { nil }
- it 'should not return specific build artifacts' do
+ it 'does not return specific build artifacts' do
expect(response).to have_http_status(401)
end
end
end
- it 'should not return build artifacts if not uploaded' do
+ it 'does not return build artifacts if not uploaded' do
expect(response).to have_http_status(404)
end
end
+ describe 'GET /projects/:id/artifacts/:ref_name/download?job=name' do
+ let(:api_user) { reporter.user }
+ let(:build) { create(:ci_build, :artifacts, pipeline: pipeline) }
+
+ before do
+ build.success
+ end
+
+ def path_for_ref(ref = pipeline.ref, job = build.name)
+ api("/projects/#{project.id}/builds/artifacts/#{ref}/download?job=#{job}", api_user)
+ end
+
+ context 'when not logged in' do
+ let(:api_user) { nil }
+
+ before do
+ get path_for_ref
+ end
+
+ it 'gives 401' do
+ expect(response).to have_http_status(401)
+ end
+ end
+
+ context 'when logging as guest' do
+ let(:api_user) { guest.user }
+
+ before do
+ get path_for_ref
+ end
+
+ it 'gives 403' do
+ expect(response).to have_http_status(403)
+ end
+ end
+
+ context 'non-existing build' do
+ shared_examples 'not found' do
+ it { expect(response).to have_http_status(:not_found) }
+ end
+
+ context 'has no such ref' do
+ before do
+ get path_for_ref('TAIL', build.name)
+ end
+
+ it_behaves_like 'not found'
+ end
+
+ context 'has no such build' do
+ before do
+ get path_for_ref(pipeline.ref, 'NOBUILD')
+ end
+
+ it_behaves_like 'not found'
+ end
+ end
+
+ context 'find proper build' do
+ shared_examples 'a valid file' do
+ let(:download_headers) do
+ { 'Content-Transfer-Encoding' => 'binary',
+ 'Content-Disposition' =>
+ "attachment; filename=#{build.artifacts_file.filename}" }
+ end
+
+ it { expect(response).to have_http_status(200) }
+ it { expect(response.headers).to include(download_headers) }
+ end
+
+ context 'with regular branch' do
+ before do
+ pipeline.update(ref: 'master',
+ sha: project.commit('master').sha)
+
+ get path_for_ref('master')
+ end
+
+ it_behaves_like 'a valid file'
+ end
+
+ context 'with branch name containing slash' do
+ before do
+ pipeline.update(ref: 'improve/awesome',
+ sha: project.commit('improve/awesome').sha)
+ end
+
+ before do
+ get path_for_ref('improve/awesome')
+ end
+
+ it_behaves_like 'a valid file'
+ end
+ end
+ end
+
describe 'GET /projects/:id/builds/:build_id/trace' do
let(:build) { create(:ci_build, :trace, pipeline: pipeline) }
- before { get api("/projects/#{project.id}/builds/#{build.id}/trace", api_user) }
+ before do
+ get api("/projects/#{project.id}/builds/#{build.id}/trace", api_user)
+ end
context 'authorized user' do
- it 'should return specific build trace' do
+ it 'returns specific build trace' do
expect(response).to have_http_status(200)
expect(response.body).to eq(build.trace)
end
@@ -187,7 +285,7 @@ describe API::API, api: true do
context 'unauthorized user' do
let(:api_user) { nil }
- it 'should not return specific build trace' do
+ it 'does not return specific build trace' do
expect(response).to have_http_status(401)
end
end
@@ -198,16 +296,16 @@ describe API::API, api: true do
context 'authorized user' do
context 'user with :update_build persmission' do
- it 'should cancel running or pending build' do
+ it 'cancels running or pending build' do
expect(response).to have_http_status(201)
expect(project.builds.first.status).to eq('canceled')
end
end
context 'user without :update_build permission' do
- let(:api_user) { user2 }
+ let(:api_user) { reporter.user }
- it 'should not cancel build' do
+ it 'does not cancel build' do
expect(response).to have_http_status(403)
end
end
@@ -216,7 +314,7 @@ describe API::API, api: true do
context 'unauthorized user' do
let(:api_user) { nil }
- it 'should not cancel build' do
+ it 'does not cancel build' do
expect(response).to have_http_status(401)
end
end
@@ -229,7 +327,7 @@ describe API::API, api: true do
context 'authorized user' do
context 'user with :update_build permission' do
- it 'should retry non-running build' do
+ it 'retries non-running build' do
expect(response).to have_http_status(201)
expect(project.builds.first.status).to eq('canceled')
expect(json_response['status']).to eq('pending')
@@ -237,9 +335,9 @@ describe API::API, api: true do
end
context 'user without :update_build permission' do
- let(:api_user) { user2 }
+ let(:api_user) { reporter.user }
- it 'should not retry build' do
+ it 'does not retry build' do
expect(response).to have_http_status(403)
end
end
@@ -248,7 +346,7 @@ describe API::API, api: true do
context 'unauthorized user' do
let(:api_user) { nil }
- it 'should not retry build' do
+ it 'does not retry build' do
expect(response).to have_http_status(401)
end
end
@@ -262,14 +360,14 @@ describe API::API, api: true do
context 'build is erasable' do
let(:build) { create(:ci_build, :trace, :artifacts, :success, project: project, pipeline: pipeline) }
- it 'should erase build content' do
+ it 'erases build content' do
expect(response.status).to eq 201
expect(build.trace).to be_empty
expect(build.artifacts_file.exists?).to be_falsy
expect(build.artifacts_metadata.exists?).to be_falsy
end
- it 'should update build' do
+ it 'updates build' do
expect(build.reload.erased_at).to be_truthy
expect(build.reload.erased_by).to eq user
end
@@ -278,7 +376,7 @@ describe API::API, api: true do
context 'build is not erasable' do
let(:build) { create(:ci_build, :trace, project: project, pipeline: pipeline) }
- it 'should respond with forbidden' do
+ it 'responds with forbidden' do
expect(response.status).to eq 403
end
end
@@ -309,4 +407,27 @@ describe API::API, api: true do
end
end
end
+
+ describe 'POST /projects/:id/builds/:build_id/play' do
+ before do
+ post api("/projects/#{project.id}/builds/#{build.id}/play", user)
+ end
+
+ context 'on an playable build' do
+ let(:build) { create(:ci_build, :manual, project: project, pipeline: pipeline) }
+
+ it 'plays the build' do
+ expect(response).to have_http_status 200
+ expect(json_response['user']['id']).to eq(user.id)
+ expect(json_response['id']).to eq(build.id)
+ end
+ end
+
+ context 'on a non-playable build' do
+ it 'returns a status code 400, Bad Request' do
+ expect(response).to have_http_status 400
+ expect(response.body).to match("Unplayable Build")
+ end
+ end
+ end
end
diff --git a/spec/requests/api/commit_statuses_spec.rb b/spec/requests/api/commit_statuses_spec.rb
index 2da01da7fa1..2d6093fec7a 100644
--- a/spec/requests/api/commit_statuses_spec.rb
+++ b/spec/requests/api/commit_statuses_spec.rb
@@ -99,7 +99,7 @@ describe API::CommitStatuses, api: true do
context "guest user" do
before { get api(get_url, guest) }
- it "should not return project commits" do
+ it "does not return project commits" do
expect(response).to have_http_status(403)
end
end
@@ -107,7 +107,7 @@ describe API::CommitStatuses, api: true do
context "unauthorized user" do
before { get api(get_url) }
- it "should not return project commits" do
+ it "does not return project commits" do
expect(response).to have_http_status(401)
end
end
@@ -179,7 +179,7 @@ describe API::CommitStatuses, api: true do
context 'reporter user' do
before { post api(post_url, reporter) }
- it 'should not create commit status' do
+ it 'does not create commit status' do
expect(response).to have_http_status(403)
end
end
@@ -187,7 +187,7 @@ describe API::CommitStatuses, api: true do
context 'guest user' do
before { post api(post_url, guest) }
- it 'should not create commit status' do
+ it 'does not create commit status' do
expect(response).to have_http_status(403)
end
end
@@ -195,7 +195,7 @@ describe API::CommitStatuses, api: true do
context 'unauthorized user' do
before { post api(post_url) }
- it 'should not create commit status' do
+ it 'does not create commit status' do
expect(response).to have_http_status(401)
end
end
diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb
index 5219c808791..7ca75d77673 100644
--- a/spec/requests/api/commits_spec.rb
+++ b/spec/requests/api/commits_spec.rb
@@ -17,7 +17,7 @@ describe API::API, api: true do
context "authorized user" do
before { project.team << [user2, :reporter] }
- it "should return project commits" do
+ it "returns project commits" do
get api("/projects/#{project.id}/repository/commits", user)
expect(response).to have_http_status(200)
@@ -27,14 +27,14 @@ describe API::API, api: true do
end
context "unauthorized user" do
- it "should not return project commits" do
+ it "does not return project commits" do
get api("/projects/#{project.id}/repository/commits")
expect(response).to have_http_status(401)
end
end
context "since optional parameter" do
- it "should return project commits since provided parameter" do
+ it "returns project commits since provided parameter" do
commits = project.repository.commits("master")
since = commits.second.created_at
@@ -47,7 +47,7 @@ describe API::API, api: true do
end
context "until optional parameter" do
- it "should return project commits until provided parameter" do
+ it "returns project commits until provided parameter" do
commits = project.repository.commits("master")
before = commits.second.created_at
@@ -60,7 +60,7 @@ describe API::API, api: true do
end
context "invalid xmlschema date parameters" do
- it "should return an invalid parameter error message" do
+ it "returns an invalid parameter error message" do
get api("/projects/#{project.id}/repository/commits?since=invalid-date", user)
expect(response).to have_http_status(400)
@@ -71,34 +71,51 @@ describe API::API, api: true do
describe "GET /projects:id/repository/commits/:sha" do
context "authorized user" do
- it "should return a commit by sha" do
+ it "returns a commit by sha" do
get api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}", user)
+
expect(response).to have_http_status(200)
expect(json_response['id']).to eq(project.repository.commit.id)
expect(json_response['title']).to eq(project.repository.commit.title)
+ expect(json_response['stats']['additions']).to eq(project.repository.commit.stats.additions)
+ expect(json_response['stats']['deletions']).to eq(project.repository.commit.stats.deletions)
+ expect(json_response['stats']['total']).to eq(project.repository.commit.stats.total)
end
- it "should return a 404 error if not found" do
+ it "returns a 404 error if not found" do
get api("/projects/#{project.id}/repository/commits/invalid_sha", user)
expect(response).to have_http_status(404)
end
- it "should return nil for commit without CI" do
+ it "returns nil for commit without CI" do
get api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}", user)
+
expect(response).to have_http_status(200)
expect(json_response['status']).to be_nil
end
- it "should return status for CI" do
+ it "returns status for CI" do
pipeline = project.ensure_pipeline(project.repository.commit.sha, 'master')
+ pipeline.update(status: 'success')
+
get api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}", user)
+
expect(response).to have_http_status(200)
expect(json_response['status']).to eq(pipeline.status)
end
+
+ it "returns status for CI when pipeline is created" do
+ project.ensure_pipeline(project.repository.commit.sha, 'master')
+
+ get api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['status']).to be_nil
+ end
end
context "unauthorized user" do
- it "should not return the selected commit" do
+ it "does not return the selected commit" do
get api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}")
expect(response).to have_http_status(401)
end
@@ -109,7 +126,7 @@ describe API::API, api: true do
context "authorized user" do
before { project.team << [user2, :reporter] }
- it "should return the diff of the selected commit" do
+ it "returns the diff of the selected commit" do
get api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}/diff", user)
expect(response).to have_http_status(200)
@@ -118,14 +135,14 @@ describe API::API, api: true do
expect(json_response.first.keys).to include "diff"
end
- it "should return a 404 error if invalid commit" do
+ it "returns a 404 error if invalid commit" do
get api("/projects/#{project.id}/repository/commits/invalid_sha/diff", user)
expect(response).to have_http_status(404)
end
end
context "unauthorized user" do
- it "should not return the diff of the selected commit" do
+ it "does not return the diff of the selected commit" do
get api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}/diff")
expect(response).to have_http_status(401)
end
@@ -134,7 +151,7 @@ describe API::API, api: true do
describe 'GET /projects:id/repository/commits/:sha/comments' do
context 'authorized user' do
- it 'should return merge_request comments' do
+ it 'returns merge_request comments' do
get api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}/comments", user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
@@ -143,14 +160,14 @@ describe API::API, api: true do
expect(json_response.first['author']['id']).to eq(user.id)
end
- it 'should return a 404 error if merge_request_id not found' do
+ it 'returns a 404 error if merge_request_id not found' do
get api("/projects/#{project.id}/repository/commits/1234ab/comments", user)
expect(response).to have_http_status(404)
end
end
context 'unauthorized user' do
- it 'should not return the diff of the selected commit' do
+ it 'does not return the diff of the selected commit' do
get api("/projects/#{project.id}/repository/commits/1234ab/comments")
expect(response).to have_http_status(401)
end
@@ -159,7 +176,7 @@ describe API::API, api: true do
describe 'POST /projects:id/repository/commits/:sha/comments' do
context 'authorized user' do
- it 'should return comment' do
+ it 'returns comment' do
post api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}/comments", user), note: 'My comment'
expect(response).to have_http_status(201)
expect(json_response['note']).to eq('My comment')
@@ -168,28 +185,28 @@ describe API::API, api: true do
expect(json_response['line_type']).to be_nil
end
- it 'should return the inline comment' do
- post api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}/comments", user), note: 'My comment', path: project.repository.commit.diffs.first.new_path, line: 7, line_type: 'new'
+ it 'returns the inline comment' do
+ post api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}/comments", user), note: 'My comment', path: project.repository.commit.raw_diffs.first.new_path, line: 7, line_type: 'new'
expect(response).to have_http_status(201)
expect(json_response['note']).to eq('My comment')
- expect(json_response['path']).to eq(project.repository.commit.diffs.first.new_path)
+ expect(json_response['path']).to eq(project.repository.commit.raw_diffs.first.new_path)
expect(json_response['line']).to eq(7)
expect(json_response['line_type']).to eq('new')
end
- it 'should return 400 if note is missing' do
+ it 'returns 400 if note is missing' do
post api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}/comments", user)
expect(response).to have_http_status(400)
end
- it 'should return 404 if note is attached to non existent commit' do
+ it 'returns 404 if note is attached to non existent commit' do
post api("/projects/#{project.id}/repository/commits/1234ab/comments", user), note: 'My comment'
expect(response).to have_http_status(404)
end
end
context 'unauthorized user' do
- it 'should not return the diff of the selected commit' do
+ it 'does not return the diff of the selected commit' do
post api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}/comments")
expect(response).to have_http_status(401)
end
diff --git a/spec/requests/api/deploy_keys_spec.rb b/spec/requests/api/deploy_keys_spec.rb
new file mode 100644
index 00000000000..7d8cc45327c
--- /dev/null
+++ b/spec/requests/api/deploy_keys_spec.rb
@@ -0,0 +1,160 @@
+require 'spec_helper'
+
+describe API::API, api: true do
+ include ApiHelpers
+
+ let(:user) { create(:user) }
+ let(:admin) { create(:admin) }
+ let(:project) { create(:project, creator_id: user.id) }
+ let(:deploy_key) { create(:deploy_key, public: true) }
+
+ let!(:deploy_keys_project) do
+ create(:deploy_keys_project, project: project, deploy_key: deploy_key)
+ end
+
+ describe 'GET /deploy_keys' do
+ context 'when unauthenticated' do
+ it 'should return authentication error' do
+ get api('/deploy_keys')
+
+ expect(response.status).to eq(401)
+ end
+ end
+
+ context 'when authenticated as non-admin user' do
+ it 'should return a 403 error' do
+ get api('/deploy_keys', user)
+
+ expect(response.status).to eq(403)
+ end
+ end
+
+ context 'when authenticated as admin' do
+ it 'should return all deploy keys' do
+ get api('/deploy_keys', admin)
+
+ expect(response.status).to eq(200)
+ expect(json_response).to be_an Array
+ expect(json_response.first['id']).to eq(deploy_keys_project.deploy_key.id)
+ end
+ end
+ end
+
+ describe 'GET /projects/:id/deploy_keys' do
+ before { deploy_key }
+
+ it 'should return array of ssh keys' do
+ get api("/projects/#{project.id}/deploy_keys", admin)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.first['title']).to eq(deploy_key.title)
+ end
+ end
+
+ describe 'GET /projects/:id/deploy_keys/:key_id' do
+ it 'should return a single key' do
+ get api("/projects/#{project.id}/deploy_keys/#{deploy_key.id}", admin)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['title']).to eq(deploy_key.title)
+ end
+
+ it 'should return 404 Not Found with invalid ID' do
+ get api("/projects/#{project.id}/deploy_keys/404", admin)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ describe 'POST /projects/:id/deploy_keys' do
+ it 'should not create an invalid ssh key' do
+ post api("/projects/#{project.id}/deploy_keys", admin), { title: 'invalid key' }
+
+ expect(response).to have_http_status(400)
+ expect(json_response['message']['key']).to eq([
+ 'can\'t be blank',
+ 'is too short (minimum is 0 characters)',
+ 'is invalid'
+ ])
+ end
+
+ it 'should not create a key without title' do
+ post api("/projects/#{project.id}/deploy_keys", admin), key: 'some key'
+
+ expect(response).to have_http_status(400)
+ expect(json_response['message']['title']).to eq([
+ 'can\'t be blank',
+ 'is too short (minimum is 0 characters)'
+ ])
+ end
+
+ it 'should create new ssh key' do
+ key_attrs = attributes_for :another_key
+
+ expect do
+ post api("/projects/#{project.id}/deploy_keys", admin), key_attrs
+ end.to change{ project.deploy_keys.count }.by(1)
+ end
+ end
+
+ describe 'DELETE /projects/:id/deploy_keys/:key_id' do
+ before { deploy_key }
+
+ it 'should delete existing key' do
+ expect do
+ delete api("/projects/#{project.id}/deploy_keys/#{deploy_key.id}", admin)
+ end.to change{ project.deploy_keys.count }.by(-1)
+ end
+
+ it 'should return 404 Not Found with invalid ID' do
+ delete api("/projects/#{project.id}/deploy_keys/404", admin)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ describe 'POST /projects/:id/deploy_keys/:key_id/enable' do
+ let(:project2) { create(:empty_project) }
+
+ context 'when the user can admin the project' do
+ it 'enables the key' do
+ expect do
+ post api("/projects/#{project2.id}/deploy_keys/#{deploy_key.id}/enable", admin)
+ end.to change { project2.deploy_keys.count }.from(0).to(1)
+
+ expect(response).to have_http_status(201)
+ expect(json_response['id']).to eq(deploy_key.id)
+ end
+ end
+
+ context 'when authenticated as non-admin user' do
+ it 'should return a 404 error' do
+ post api("/projects/#{project2.id}/deploy_keys/#{deploy_key.id}/enable", user)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
+
+ describe 'DELETE /projects/:id/deploy_keys/:key_id/disable' do
+ context 'when the user can admin the project' do
+ it 'disables the key' do
+ expect do
+ delete api("/projects/#{project.id}/deploy_keys/#{deploy_key.id}/disable", admin)
+ end.to change { project.deploy_keys.count }.from(1).to(0)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['id']).to eq(deploy_key.id)
+ end
+ end
+
+ context 'when authenticated as non-admin user' do
+ it 'should return a 404 error' do
+ delete api("/projects/#{project.id}/deploy_keys/#{deploy_key.id}/disable", user)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/deployments_spec.rb b/spec/requests/api/deployments_spec.rb
new file mode 100644
index 00000000000..8fa8c66db6c
--- /dev/null
+++ b/spec/requests/api/deployments_spec.rb
@@ -0,0 +1,60 @@
+require 'spec_helper'
+
+describe API::API, api: true do
+ include ApiHelpers
+
+ let(:user) { create(:user) }
+ let(:non_member) { create(:user) }
+ let(:project) { deployment.environment.project }
+ let!(:deployment) { create(:deployment) }
+
+ before do
+ project.team << [user, :master]
+ end
+
+ describe 'GET /projects/:id/deployments' do
+ context 'as member of the project' do
+ it_behaves_like 'a paginated resources' do
+ let(:request) { get api("/projects/#{project.id}/deployments", user) }
+ end
+
+ it 'returns projects deployments' do
+ get api("/projects/#{project.id}/deployments", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.size).to eq(1)
+ expect(json_response.first['iid']).to eq(deployment.iid)
+ expect(json_response.first['sha']).to match /\A\h{40}\z/
+ end
+ end
+
+ context 'as non member' do
+ it 'returns a 404 status code' do
+ get api("/projects/#{project.id}/deployments", non_member)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
+
+ describe 'GET /projects/:id/deployments/:deployment_id' do
+ context 'as a member of the project' do
+ it 'returns the projects deployment' do
+ get api("/projects/#{project.id}/deployments/#{deployment.id}", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['sha']).to match /\A\h{40}\z/
+ expect(json_response['id']).to eq(deployment.id)
+ end
+ end
+
+ context 'as non member' do
+ it 'returns a 404 status code' do
+ get api("/projects/#{project.id}/deployments/#{deployment.id}", non_member)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/environments_spec.rb b/spec/requests/api/environments_spec.rb
new file mode 100644
index 00000000000..1898b07835d
--- /dev/null
+++ b/spec/requests/api/environments_spec.rb
@@ -0,0 +1,130 @@
+require 'spec_helper'
+
+describe API::API, api: true do
+ include ApiHelpers
+
+ let(:user) { create(:user) }
+ let(:non_member) { create(:user) }
+ let(:project) { create(:project, :private, namespace: user.namespace) }
+ let!(:environment) { create(:environment, project: project) }
+
+ before do
+ project.team << [user, :master]
+ end
+
+ describe 'GET /projects/:id/environments' do
+ context 'as member of the project' do
+ it_behaves_like 'a paginated resources' do
+ let(:request) { get api("/projects/#{project.id}/environments", user) }
+ end
+
+ it 'returns project environments' do
+ get api("/projects/#{project.id}/environments", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.size).to eq(1)
+ expect(json_response.first['name']).to eq(environment.name)
+ expect(json_response.first['external_url']).to eq(environment.external_url)
+ expect(json_response.first['project']['id']).to eq(project.id)
+ end
+ end
+
+ context 'as non member' do
+ it 'returns a 404 status code' do
+ get api("/projects/#{project.id}/environments", non_member)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
+
+ describe 'POST /projects/:id/environments' do
+ context 'as a member' do
+ it 'creates a environment with valid params' do
+ post api("/projects/#{project.id}/environments", user), name: "mepmep"
+
+ expect(response).to have_http_status(201)
+ expect(json_response['name']).to eq('mepmep')
+ expect(json_response['external']).to be nil
+ end
+
+ it 'requires name to be passed' do
+ post api("/projects/#{project.id}/environments", user), external_url: 'test.gitlab.com'
+
+ expect(response).to have_http_status(400)
+ end
+
+ it 'returns a 400 if environment already exists' do
+ post api("/projects/#{project.id}/environments", user), name: environment.name
+
+ expect(response).to have_http_status(400)
+ end
+ end
+
+ context 'a non member' do
+ it 'rejects the request' do
+ post api("/projects/#{project.id}/environments", non_member), name: 'gitlab.com'
+
+ expect(response).to have_http_status(404)
+ end
+
+ it 'returns a 400 when the required params are missing' do
+ post api("/projects/12345/environments", non_member), external_url: 'http://env.git.com'
+ end
+ end
+ end
+
+ describe 'PUT /projects/:id/environments/:environment_id' do
+ it 'returns a 200 if name and external_url are changed' do
+ url = 'https://mepmep.whatever.ninja'
+ put api("/projects/#{project.id}/environments/#{environment.id}", user),
+ name: 'Mepmep', external_url: url
+
+ expect(response).to have_http_status(200)
+ expect(json_response['name']).to eq('Mepmep')
+ expect(json_response['external_url']).to eq(url)
+ end
+
+ it "won't update the external_url if only the name is passed" do
+ url = environment.external_url
+ put api("/projects/#{project.id}/environments/#{environment.id}", user),
+ name: 'Mepmep'
+
+ expect(response).to have_http_status(200)
+ expect(json_response['name']).to eq('Mepmep')
+ expect(json_response['external_url']).to eq(url)
+ end
+
+ it 'returns a 404 if the environment does not exist' do
+ put api("/projects/#{project.id}/environments/12345", user)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ describe 'DELETE /projects/:id/environments/:environment_id' do
+ context 'as a master' do
+ it 'returns a 200 for an existing environment' do
+ delete api("/projects/#{project.id}/environments/#{environment.id}", user)
+
+ expect(response).to have_http_status(200)
+ end
+
+ it 'returns a 404 for non existing id' do
+ delete api("/projects/#{project.id}/environments/12345", user)
+
+ expect(response).to have_http_status(404)
+ expect(json_response['message']).to eq('404 Not found')
+ end
+ end
+
+ context 'a non member' do
+ it 'rejects the request' do
+ delete api("/projects/#{project.id}/environments/#{environment.id}", non_member)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/files_spec.rb b/spec/requests/api/files_spec.rb
index 2e5448143d5..2d1213df8a7 100644
--- a/spec/requests/api/files_spec.rb
+++ b/spec/requests/api/files_spec.rb
@@ -9,7 +9,7 @@ describe API::API, api: true do
before { project.team << [user, :developer] }
describe "GET /projects/:id/repository/files" do
- it "should return file info" do
+ it "returns file info" do
params = {
file_path: file_path,
ref: 'master',
@@ -23,12 +23,12 @@ describe API::API, api: true do
expect(Base64.decode64(json_response['content']).lines.first).to eq("require 'fileutils'\n")
end
- it "should return a 400 bad request if no params given" do
+ it "returns a 400 bad request if no params given" do
get api("/projects/#{project.id}/repository/files", user)
expect(response).to have_http_status(400)
end
- it "should return a 404 if such file does not exist" do
+ it "returns a 404 if such file does not exist" do
params = {
file_path: 'app/models/application.rb',
ref: 'master',
@@ -49,18 +49,18 @@ describe API::API, api: true do
}
end
- it "should create a new file in project repo" do
+ it "creates a new file in project repo" do
post api("/projects/#{project.id}/repository/files", user), valid_params
expect(response).to have_http_status(201)
expect(json_response['file_path']).to eq('newfile.rb')
end
- it "should return a 400 bad request if no params given" do
+ it "returns a 400 bad request if no params given" do
post api("/projects/#{project.id}/repository/files", user)
expect(response).to have_http_status(400)
end
- it "should return a 400 if editor fails to create file" do
+ it "returns a 400 if editor fails to create file" do
allow_any_instance_of(Repository).to receive(:commit_file).
and_return(false)
@@ -79,13 +79,13 @@ describe API::API, api: true do
}
end
- it "should update existing file in project repo" do
+ it "updates existing file in project repo" do
put api("/projects/#{project.id}/repository/files", user), valid_params
expect(response).to have_http_status(200)
expect(json_response['file_path']).to eq(file_path)
end
- it "should return a 400 bad request if no params given" do
+ it "returns a 400 bad request if no params given" do
put api("/projects/#{project.id}/repository/files", user)
expect(response).to have_http_status(400)
end
@@ -100,18 +100,18 @@ describe API::API, api: true do
}
end
- it "should delete existing file in project repo" do
+ it "deletes existing file in project repo" do
delete api("/projects/#{project.id}/repository/files", user), valid_params
expect(response).to have_http_status(200)
expect(json_response['file_path']).to eq(file_path)
end
- it "should return a 400 bad request if no params given" do
+ it "returns a 400 bad request if no params given" do
delete api("/projects/#{project.id}/repository/files", user)
expect(response).to have_http_status(400)
end
- it "should return a 400 if fails to create file" do
+ it "returns a 400 if fails to create file" do
allow_any_instance_of(Repository).to receive(:remove_file).and_return(false)
delete api("/projects/#{project.id}/repository/files", user), valid_params
diff --git a/spec/requests/api/fork_spec.rb b/spec/requests/api/fork_spec.rb
index a9f5aa924b7..f802fcd2d2e 100644
--- a/spec/requests/api/fork_spec.rb
+++ b/spec/requests/api/fork_spec.rb
@@ -20,7 +20,7 @@ describe API::API, api: true do
before { user3 }
context 'when authenticated' do
- it 'should fork if user has sufficient access to project' do
+ it 'forks if user has sufficient access to project' do
post api("/projects/fork/#{project.id}", user2)
expect(response).to have_http_status(201)
expect(json_response['name']).to eq(project.name)
@@ -30,7 +30,7 @@ describe API::API, api: true do
expect(json_response['forked_from_project']['id']).to eq(project.id)
end
- it 'should fork if user is admin' do
+ it 'forks if user is admin' do
post api("/projects/fork/#{project.id}", admin)
expect(response).to have_http_status(201)
expect(json_response['name']).to eq(project.name)
@@ -40,20 +40,20 @@ describe API::API, api: true do
expect(json_response['forked_from_project']['id']).to eq(project.id)
end
- it 'should fail on missing project access for the project to fork' do
+ it 'fails on missing project access for the project to fork' do
post api("/projects/fork/#{project.id}", user3)
expect(response).to have_http_status(404)
expect(json_response['message']).to eq('404 Project Not Found')
end
- it 'should fail if forked project exists in the user namespace' do
+ it 'fails if forked project exists in the user namespace' do
post api("/projects/fork/#{project.id}", user)
expect(response).to have_http_status(409)
expect(json_response['message']['name']).to eq(['has already been taken'])
expect(json_response['message']['path']).to eq(['has already been taken'])
end
- it 'should fail if project to fork from does not exist' do
+ it 'fails if project to fork from does not exist' do
post api('/projects/fork/424242', user)
expect(response).to have_http_status(404)
expect(json_response['message']).to eq('404 Project Not Found')
@@ -61,7 +61,7 @@ describe API::API, api: true do
end
context 'when unauthenticated' do
- it 'should return authentication error' do
+ it 'returns authentication error' do
post api("/projects/fork/#{project.id}")
expect(response).to have_http_status(401)
expect(json_response['message']).to eq('401 Unauthorized')
diff --git a/spec/requests/api/group_members_spec.rb b/spec/requests/api/group_members_spec.rb
deleted file mode 100644
index 52f9e7d4681..00000000000
--- a/spec/requests/api/group_members_spec.rb
+++ /dev/null
@@ -1,199 +0,0 @@
-require 'spec_helper'
-
-describe API::API, api: true do
- include ApiHelpers
-
- let(:owner) { create(:user) }
- let(:reporter) { create(:user) }
- let(:developer) { create(:user) }
- let(:master) { create(:user) }
- let(:guest) { create(:user) }
- let(:stranger) { create(:user) }
-
- let!(:group_with_members) do
- group = create(:group, :private)
- group.add_users([reporter.id], GroupMember::REPORTER)
- group.add_users([developer.id], GroupMember::DEVELOPER)
- group.add_users([master.id], GroupMember::MASTER)
- group.add_users([guest.id], GroupMember::GUEST)
- group
- end
-
- let!(:group_no_members) { create(:group) }
-
- before do
- group_with_members.add_owner owner
- group_no_members.add_owner owner
- end
-
- describe "GET /groups/:id/members" do
- context "when authenticated as user that is part or the group" do
- it "each user: should return an array of members groups of group3" do
- [owner, master, developer, reporter, guest].each do |user|
- get api("/groups/#{group_with_members.id}/members", user)
- expect(response).to have_http_status(200)
- expect(json_response).to be_an Array
- expect(json_response.size).to eq(5)
- expect(json_response.find { |e| e['id'] == owner.id }['access_level']).to eq(GroupMember::OWNER)
- expect(json_response.find { |e| e['id'] == reporter.id }['access_level']).to eq(GroupMember::REPORTER)
- expect(json_response.find { |e| e['id'] == developer.id }['access_level']).to eq(GroupMember::DEVELOPER)
- expect(json_response.find { |e| e['id'] == master.id }['access_level']).to eq(GroupMember::MASTER)
- expect(json_response.find { |e| e['id'] == guest.id }['access_level']).to eq(GroupMember::GUEST)
- end
- end
-
- it 'users not part of the group should get access error' do
- get api("/groups/#{group_with_members.id}/members", stranger)
-
- expect(response).to have_http_status(404)
- end
- end
- end
-
- describe "POST /groups/:id/members" do
- context "when not a member of the group" do
- it "should not add guest as member of group_no_members when adding being done by person outside the group" do
- post api("/groups/#{group_no_members.id}/members", reporter), user_id: guest.id, access_level: GroupMember::MASTER
- expect(response).to have_http_status(403)
- end
- end
-
- context "when a member of the group" do
- it "should return ok and add new member" do
- new_user = create(:user)
-
- expect do
- post api("/groups/#{group_no_members.id}/members", owner), user_id: new_user.id, access_level: GroupMember::MASTER
- end.to change { group_no_members.members.count }.by(1)
-
- expect(response).to have_http_status(201)
- expect(json_response['name']).to eq(new_user.name)
- expect(json_response['access_level']).to eq(GroupMember::MASTER)
- end
-
- it "should not allow guest to modify group members" do
- new_user = create(:user)
-
- expect do
- post api("/groups/#{group_with_members.id}/members", guest), user_id: new_user.id, access_level: GroupMember::MASTER
- end.not_to change { group_with_members.members.count }
-
- expect(response).to have_http_status(403)
- end
-
- it "should return error if member already exists" do
- post api("/groups/#{group_with_members.id}/members", owner), user_id: master.id, access_level: GroupMember::MASTER
- expect(response).to have_http_status(409)
- end
-
- it "should return a 400 error when user id is not given" do
- post api("/groups/#{group_no_members.id}/members", owner), access_level: GroupMember::MASTER
- expect(response).to have_http_status(400)
- end
-
- it "should return a 400 error when access level is not given" do
- post api("/groups/#{group_no_members.id}/members", owner), user_id: master.id
- expect(response).to have_http_status(400)
- end
-
- it "should return a 422 error when access level is not known" do
- post api("/groups/#{group_no_members.id}/members", owner), user_id: master.id, access_level: 1234
- expect(response).to have_http_status(422)
- end
- end
- end
-
- describe 'PUT /groups/:id/members/:user_id' do
- context 'when not a member of the group' do
- it 'should return a 409 error if the user is not a group member' do
- put(
- api("/groups/#{group_no_members.id}/members/#{developer.id}",
- owner), access_level: GroupMember::MASTER
- )
- expect(response).to have_http_status(404)
- end
- end
-
- context 'when a member of the group' do
- it 'should return ok and update member access level' do
- put(
- api("/groups/#{group_with_members.id}/members/#{reporter.id}",
- owner),
- access_level: GroupMember::MASTER
- )
-
- expect(response).to have_http_status(200)
-
- get api("/groups/#{group_with_members.id}/members", owner)
- json_reporter = json_response.find do |e|
- e['id'] == reporter.id
- end
-
- expect(json_reporter['access_level']).to eq(GroupMember::MASTER)
- end
-
- it 'should not allow guest to modify group members' do
- put(
- api("/groups/#{group_with_members.id}/members/#{developer.id}",
- guest),
- access_level: GroupMember::MASTER
- )
-
- expect(response).to have_http_status(403)
-
- get api("/groups/#{group_with_members.id}/members", owner)
- json_developer = json_response.find do |e|
- e['id'] == developer.id
- end
-
- expect(json_developer['access_level']).to eq(GroupMember::DEVELOPER)
- end
-
- it 'should return a 400 error when access level is not given' do
- put(
- api("/groups/#{group_with_members.id}/members/#{master.id}", owner)
- )
- expect(response).to have_http_status(400)
- end
-
- it 'should return a 422 error when access level is not known' do
- put(
- api("/groups/#{group_with_members.id}/members/#{master.id}", owner),
- access_level: 1234
- )
- expect(response).to have_http_status(422)
- end
- end
- end
-
- describe 'DELETE /groups/:id/members/:user_id' do
- context 'when not a member of the group' do
- it "should not delete guest's membership of group_with_members" do
- random_user = create(:user)
- delete api("/groups/#{group_with_members.id}/members/#{owner.id}", random_user)
-
- expect(response).to have_http_status(404)
- end
- end
-
- context "when a member of the group" do
- it "should delete guest's membership of group" do
- expect do
- delete api("/groups/#{group_with_members.id}/members/#{guest.id}", owner)
- end.to change { group_with_members.members.count }.by(-1)
-
- expect(response).to have_http_status(200)
- end
-
- it "should return a 404 error when user id is not known" do
- delete api("/groups/#{group_with_members.id}/members/1328", owner)
- expect(response).to have_http_status(404)
- end
-
- it "should not allow guest to modify group members" do
- delete api("/groups/#{group_with_members.id}/members/#{master.id}", guest)
- expect(response).to have_http_status(403)
- end
- end
- end
-end
diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb
index c2c94040ece..4860b23c2ed 100644
--- a/spec/requests/api/groups_spec.rb
+++ b/spec/requests/api/groups_spec.rb
@@ -21,14 +21,14 @@ describe API::API, api: true do
describe "GET /groups" do
context "when unauthenticated" do
- it "should return authentication error" do
+ it "returns authentication error" do
get api("/groups")
expect(response).to have_http_status(401)
end
end
context "when authenticated as user" do
- it "normal user: should return an array of groups of user1" do
+ it "normal user: returns an array of groups of user1" do
get api("/groups", user1)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
@@ -38,7 +38,7 @@ describe API::API, api: true do
end
context "when authenticated as admin" do
- it "admin: should return an array of all groups" do
+ it "admin: returns an array of all groups" do
get api("/groups", admin)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
@@ -70,12 +70,12 @@ describe API::API, api: true do
expect(json_response['shared_projects'][0]['id']).to eq(project.id)
end
- it "should not return a non existing group" do
+ it "does not return a non existing group" do
get api("/groups/1328", user1)
expect(response).to have_http_status(404)
end
- it "should not return a group not attached to user1" do
+ it "does not return a group not attached to user1" do
get api("/groups/#{group2.id}", user1)
expect(response).to have_http_status(404)
@@ -83,31 +83,31 @@ describe API::API, api: true do
end
context "when authenticated as admin" do
- it "should return any existing group" do
+ it "returns any existing group" do
get api("/groups/#{group2.id}", admin)
expect(response).to have_http_status(200)
expect(json_response['name']).to eq(group2.name)
end
- it "should not return a non existing group" do
+ it "does not return a non existing group" do
get api("/groups/1328", admin)
expect(response).to have_http_status(404)
end
end
context 'when using group path in URL' do
- it 'should return any existing group' do
+ it 'returns any existing group' do
get api("/groups/#{group1.path}", admin)
expect(response).to have_http_status(200)
expect(json_response['name']).to eq(group1.name)
end
- it 'should not return a non existing group' do
+ it 'does not return a non existing group' do
get api('/groups/unknown', admin)
expect(response).to have_http_status(404)
end
- it 'should not return a group not attached to user1' do
+ it 'does not return a group not attached to user1' do
get api("/groups/#{group2.path}", user1)
expect(response).to have_http_status(404)
@@ -161,7 +161,7 @@ describe API::API, api: true do
describe "GET /groups/:id/projects" do
context "when authenticated as user" do
- it "should return the group's projects" do
+ it "returns the group's projects" do
get api("/groups/#{group1.id}/projects", user1)
expect(response).to have_http_status(200)
@@ -170,12 +170,12 @@ describe API::API, api: true do
expect(project_names).to match_array([project1.name, project3.name])
end
- it "should not return a non existing group" do
+ it "does not return a non existing group" do
get api("/groups/1328/projects", user1)
expect(response).to have_http_status(404)
end
- it "should not return a group not attached to user1" do
+ it "does not return a group not attached to user1" do
get api("/groups/#{group2.id}/projects", user1)
expect(response).to have_http_status(404)
@@ -215,12 +215,12 @@ describe API::API, api: true do
expect(project_names).to match_array([project1.name, project3.name])
end
- it 'should not return a non existing group' do
+ it 'does not return a non existing group' do
get api('/groups/unknown/projects', admin)
expect(response).to have_http_status(404)
end
- it 'should not return a group not attached to user1' do
+ it 'does not return a group not attached to user1' do
get api("/groups/#{group2.path}/projects", user1)
expect(response).to have_http_status(404)
@@ -230,30 +230,30 @@ describe API::API, api: true do
describe "POST /groups" do
context "when authenticated as user without group permissions" do
- it "should not create group" do
+ it "does not create group" do
post api("/groups", user1), attributes_for(:group)
expect(response).to have_http_status(403)
end
end
context "when authenticated as user with group permissions" do
- it "should create group" do
+ it "creates group" do
post api("/groups", user3), attributes_for(:group)
expect(response).to have_http_status(201)
end
- it "should not create group, duplicate" do
+ it "does not create group, duplicate" do
post api("/groups", user3), { name: 'Duplicate Test', path: group2.path }
expect(response).to have_http_status(400)
expect(response.message).to eq("Bad Request")
end
- it "should return 400 bad request error if name not given" do
+ it "returns 400 bad request error if name not given" do
post api("/groups", user3), { path: group2.path }
expect(response).to have_http_status(400)
end
- it "should return 400 bad request error if path not given" do
+ it "returns 400 bad request error if path not given" do
post api("/groups", user3), { name: 'test' }
expect(response).to have_http_status(400)
end
@@ -262,24 +262,24 @@ describe API::API, api: true do
describe "DELETE /groups/:id" do
context "when authenticated as user" do
- it "should remove group" do
+ it "removes group" do
delete api("/groups/#{group1.id}", user1)
expect(response).to have_http_status(200)
end
- it "should not remove a group if not an owner" do
+ it "does not remove a group if not an owner" do
user4 = create(:user)
group1.add_master(user4)
delete api("/groups/#{group1.id}", user3)
expect(response).to have_http_status(403)
end
- it "should not remove a non existing group" do
+ it "does not remove a non existing group" do
delete api("/groups/1328", user1)
expect(response).to have_http_status(404)
end
- it "should not remove a group not attached to user1" do
+ it "does not remove a group not attached to user1" do
delete api("/groups/#{group2.id}", user1)
expect(response).to have_http_status(404)
@@ -287,12 +287,12 @@ describe API::API, api: true do
end
context "when authenticated as admin" do
- it "should remove any existing group" do
+ it "removes any existing group" do
delete api("/groups/#{group2.id}", admin)
expect(response).to have_http_status(200)
end
- it "should not remove a non existing group" do
+ it "does not remove a non existing group" do
delete api("/groups/1328", admin)
expect(response).to have_http_status(404)
end
@@ -308,14 +308,14 @@ describe API::API, api: true do
end
context "when authenticated as user" do
- it "should not transfer project to group" do
+ it "does not transfer project to group" do
post api("/groups/#{group1.id}/projects/#{project.id}", user2)
expect(response).to have_http_status(403)
end
end
context "when authenticated as admin" do
- it "should transfer project to group" do
+ it "transfers project to group" do
post api("/groups/#{group1.id}/projects/#{project.id}", admin)
expect(response).to have_http_status(201)
end
diff --git a/spec/requests/api/internal_spec.rb b/spec/requests/api/internal_spec.rb
index e567d36afa8..5d06abcfeb3 100644
--- a/spec/requests/api/internal_spec.rb
+++ b/spec/requests/api/internal_spec.rb
@@ -38,6 +38,68 @@ describe API::API, api: true do
end
end
+ describe 'GET /internal/two_factor_recovery_codes' do
+ it 'returns an error message when the key does not exist' do
+ post api('/internal/two_factor_recovery_codes'),
+ secret_token: secret_token,
+ key_id: 12345
+
+ expect(response).to have_http_status(404)
+ expect(json_response['message']).to eq('404 Not found')
+ end
+
+ it 'returns an error message when the key is a deploy key' do
+ deploy_key = create(:deploy_key)
+
+ post api('/internal/two_factor_recovery_codes'),
+ secret_token: secret_token,
+ key_id: deploy_key.id
+
+ expect(json_response['success']).to be_falsey
+ expect(json_response['message']).to eq('Deploy keys cannot be used to retrieve recovery codes')
+ end
+
+ it 'returns an error message when the user does not exist' do
+ key_without_user = create(:key, user: nil)
+
+ post api('/internal/two_factor_recovery_codes'),
+ secret_token: secret_token,
+ key_id: key_without_user.id
+
+ expect(json_response['success']).to be_falsey
+ expect(json_response['message']).to eq('Could not find a user for the given key')
+ expect(json_response['recovery_codes']).to be_nil
+ end
+
+ context 'when two-factor is enabled' do
+ it 'returns new recovery codes when the user exists' do
+ allow_any_instance_of(User).to receive(:two_factor_enabled?).and_return(true)
+ allow_any_instance_of(User)
+ .to receive(:generate_otp_backup_codes!).and_return(%w(119135e5a3ebce8e 34bd7b74adbc8861))
+
+ post api('/internal/two_factor_recovery_codes'),
+ secret_token: secret_token,
+ key_id: key.id
+
+ expect(json_response['success']).to be_truthy
+ expect(json_response['recovery_codes']).to match_array(%w(119135e5a3ebce8e 34bd7b74adbc8861))
+ end
+ end
+
+ context 'when two-factor is not enabled' do
+ it 'returns an error message' do
+ allow_any_instance_of(User).to receive(:two_factor_enabled?).and_return(false)
+
+ post api('/internal/two_factor_recovery_codes'),
+ secret_token: secret_token,
+ key_id: key.id
+
+ expect(json_response['success']).to be_falsey
+ expect(json_response['recovery_codes']).to be_nil
+ end
+ end
+ end
+
describe "GET /internal/discover" do
it do
get(api("/internal/discover"), key_id: key.id, secret_token: secret_token)
@@ -56,13 +118,21 @@ describe API::API, api: true do
context "git push with project.wiki" do
it 'responds with success' do
- project_wiki = create(:project, name: 'my.wiki', path: 'my.wiki')
- project_wiki.team << [user, :developer]
+ push(key, project.wiki)
+
+ expect(response).to have_http_status(200)
+ expect(json_response["status"]).to be_truthy
+ expect(json_response["repository_path"]).to eq(project.wiki.repository.path_to_repo)
+ end
+ end
- push(key, project_wiki)
+ context "git pull with project.wiki" do
+ it 'responds with success' do
+ pull(key, project.wiki)
expect(response).to have_http_status(200)
expect(json_response["status"]).to be_truthy
+ expect(json_response["repository_path"]).to eq(project.wiki.repository.path_to_repo)
end
end
@@ -267,6 +337,24 @@ describe API::API, api: true do
end
end
+ describe 'GET /internal/merge_request_urls' do
+ let(:repo_name) { "#{project.namespace.name}/#{project.path}" }
+ let(:changes) { URI.escape("#{Gitlab::Git::BLANK_SHA} 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/new_branch") }
+
+ before do
+ project.team << [user, :developer]
+ get api("/internal/merge_request_urls?project=#{repo_name}&changes=#{changes}"), secret_token: secret_token
+ end
+
+ it 'returns link to create new merge request' do
+ expect(json_response).to match [{
+ "branch_name" => "new_branch",
+ "url" => "http://localhost/#{project.namespace.name}/#{project.path}/merge_requests/new?merge_request%5Bsource_branch%5D=new_branch",
+ "new_merge_request" => true
+ }]
+ end
+ end
+
def pull(key, project, protocol = 'ssh')
post(
api("/internal/allowed"),
diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb
index 6adccb4ebae..b8038fc85a1 100644
--- a/spec/requests/api/issues_spec.rb
+++ b/spec/requests/api/issues_spec.rb
@@ -49,28 +49,29 @@ describe API::API, api: true do
describe "GET /issues" do
context "when unauthenticated" do
- it "should return authentication error" do
+ it "returns authentication error" do
get api("/issues")
expect(response).to have_http_status(401)
end
end
context "when authenticated" do
- it "should return an array of issues" do
+ it "returns an array of issues" do
get api("/issues", user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.first['title']).to eq(issue.title)
+ expect(json_response.last).to have_key('web_url')
end
- it "should add pagination headers and keep query params" do
+ it "adds pagination headers and keep query params" do
get api("/issues?state=closed&per_page=3", user)
expect(response.headers['Link']).to eq(
'<http://www.example.com/api/v3/issues?page=1&per_page=3&private_token=%s&state=closed>; rel="first", <http://www.example.com/api/v3/issues?page=1&per_page=3&private_token=%s&state=closed>; rel="last"' % [user.private_token, user.private_token]
)
end
- it 'should return an array of closed issues' do
+ it 'returns an array of closed issues' do
get api('/issues?state=closed', user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
@@ -78,7 +79,7 @@ describe API::API, api: true do
expect(json_response.first['id']).to eq(closed_issue.id)
end
- it 'should return an array of opened issues' do
+ it 'returns an array of opened issues' do
get api('/issues?state=opened', user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
@@ -86,7 +87,7 @@ describe API::API, api: true do
expect(json_response.first['id']).to eq(issue.id)
end
- it 'should return an array of all issues' do
+ it 'returns an array of all issues' do
get api('/issues?state=all', user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
@@ -95,7 +96,7 @@ describe API::API, api: true do
expect(json_response.second['id']).to eq(closed_issue.id)
end
- it 'should return an array of labeled issues' do
+ it 'returns an array of labeled issues' do
get api("/issues?labels=#{label.title}", user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
@@ -103,7 +104,7 @@ describe API::API, api: true do
expect(json_response.first['labels']).to eq([label.title])
end
- it 'should return an array of labeled issues when at least one label matches' do
+ it 'returns an array of labeled issues when at least one label matches' do
get api("/issues?labels=#{label.title},foo,bar", user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
@@ -111,14 +112,14 @@ describe API::API, api: true do
expect(json_response.first['labels']).to eq([label.title])
end
- it 'should return an empty array if no issue matches labels' do
+ it 'returns an empty array if no issue matches labels' do
get api('/issues?labels=foo,bar', user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(0)
end
- it 'should return an array of labeled issues matching given state' do
+ it 'returns an array of labeled issues matching given state' do
get api("/issues?labels=#{label.title}&state=opened", user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
@@ -127,7 +128,7 @@ describe API::API, api: true do
expect(json_response.first['state']).to eq('opened')
end
- it 'should return an empty array if no issue matches labels and state filters' do
+ it 'returns an empty array if no issue matches labels and state filters' do
get api("/issues?labels=#{label.title}&state=closed", user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
@@ -282,7 +283,7 @@ describe API::API, api: true do
let(:base_url) { "/projects/#{project.id}" }
let(:title) { milestone.title }
- it 'should return project issues without confidential issues for non project members' do
+ it 'returns project issues without confidential issues for non project members' do
get api("#{base_url}/issues", non_member)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
@@ -290,7 +291,7 @@ 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
+ it 'returns project issues without confidential issues for project members with guest role' do
get api("#{base_url}/issues", guest)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
@@ -298,7 +299,7 @@ describe API::API, api: true do
expect(json_response.first['title']).to eq(issue.title)
end
- it 'should return project confidential issues for author' do
+ it 'returns project confidential issues for author' do
get api("#{base_url}/issues", author)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
@@ -306,7 +307,7 @@ describe API::API, api: true do
expect(json_response.first['title']).to eq(issue.title)
end
- it 'should return project confidential issues for assignee' do
+ it 'returns project confidential issues for assignee' do
get api("#{base_url}/issues", assignee)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
@@ -314,7 +315,7 @@ describe API::API, api: true do
expect(json_response.first['title']).to eq(issue.title)
end
- it 'should return project issues with confidential issues for project members' do
+ it 'returns project issues with confidential issues for project members' do
get api("#{base_url}/issues", user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
@@ -322,7 +323,7 @@ describe API::API, api: true do
expect(json_response.first['title']).to eq(issue.title)
end
- it 'should return project confidential issues for admin' do
+ it 'returns project confidential issues for admin' do
get api("#{base_url}/issues", admin)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
@@ -330,7 +331,7 @@ describe API::API, api: true do
expect(json_response.first['title']).to eq(issue.title)
end
- it 'should return an array of labeled project issues' do
+ it 'returns an array of labeled project issues' do
get api("#{base_url}/issues?labels=#{label.title}", user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
@@ -338,7 +339,7 @@ describe API::API, api: true do
expect(json_response.first['labels']).to eq([label.title])
end
- it 'should return an array of labeled project issues when at least one label matches' do
+ it 'returns an array of labeled project issues when at least one label matches' do
get api("#{base_url}/issues?labels=#{label.title},foo,bar", user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
@@ -346,28 +347,28 @@ describe API::API, api: true do
expect(json_response.first['labels']).to eq([label.title])
end
- it 'should return an empty array if no project issue matches labels' do
+ it 'returns an empty array if no project issue matches labels' do
get api("#{base_url}/issues?labels=foo,bar", user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(0)
end
- it 'should return an empty array if no issue matches milestone' do
+ it 'returns an empty array if no issue matches milestone' do
get api("#{base_url}/issues?milestone=#{empty_milestone.title}", user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(0)
end
- it 'should return an empty array if milestone does not exist' do
+ it 'returns an empty array if milestone does not exist' do
get api("#{base_url}/issues?milestone=foo", user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(0)
end
- it 'should return an array of issues in given milestone' do
+ it 'returns an array of issues in given milestone' do
get api("#{base_url}/issues?milestone=#{title}", user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
@@ -376,7 +377,7 @@ describe API::API, api: true do
expect(json_response.second['id']).to eq(closed_issue.id)
end
- it 'should return an array of issues matching state in milestone' do
+ it 'returns an array of issues matching state in milestone' do
get api("#{base_url}/issues?milestone=#{milestone.title}"\
'&state=closed', user)
expect(response).to have_http_status(200)
@@ -405,7 +406,7 @@ describe API::API, api: true do
expect(json_response['author']).to be_a Hash
end
- it "should return a project issue by id" do
+ it "returns a project issue by id" do
get api("/projects/#{project.id}/issues/#{issue.id}", user)
expect(response).to have_http_status(200)
@@ -413,7 +414,7 @@ describe API::API, api: true do
expect(json_response['iid']).to eq(issue.iid)
end
- it 'should return a project issue by iid' do
+ it 'returns a project issue by iid' do
get api("/projects/#{project.id}/issues?iid=#{issue.iid}", user)
expect(response.status).to eq 200
expect(json_response.first['title']).to eq issue.title
@@ -421,44 +422,44 @@ describe API::API, api: true do
expect(json_response.first['iid']).to eq issue.iid
end
- it "should return 404 if issue id not found" do
+ it "returns 404 if issue id not found" do
get api("/projects/#{project.id}/issues/54321", user)
expect(response).to have_http_status(404)
end
context 'confidential issues' do
- it "should return 404 for non project members" do
+ it "returns 404 for non project members" do
get api("/projects/#{project.id}/issues/#{confidential_issue.id}", non_member)
expect(response).to have_http_status(404)
end
- it "should return 404 for project members with guest role" do
+ it "returns 404 for project members with guest role" do
get api("/projects/#{project.id}/issues/#{confidential_issue.id}", guest)
expect(response).to have_http_status(404)
end
- it "should return confidential issue for project members" do
+ it "returns confidential issue for project members" do
get api("/projects/#{project.id}/issues/#{confidential_issue.id}", user)
expect(response).to have_http_status(200)
expect(json_response['title']).to eq(confidential_issue.title)
expect(json_response['iid']).to eq(confidential_issue.iid)
end
- it "should return confidential issue for author" do
+ it "returns confidential issue for author" do
get api("/projects/#{project.id}/issues/#{confidential_issue.id}", author)
expect(response).to have_http_status(200)
expect(json_response['title']).to eq(confidential_issue.title)
expect(json_response['iid']).to eq(confidential_issue.iid)
end
- it "should return confidential issue for assignee" do
+ it "returns confidential issue for assignee" do
get api("/projects/#{project.id}/issues/#{confidential_issue.id}", assignee)
expect(response).to have_http_status(200)
expect(json_response['title']).to eq(confidential_issue.title)
expect(json_response['iid']).to eq(confidential_issue.iid)
end
- it "should return confidential issue for admin" do
+ it "returns confidential issue for admin" do
get api("/projects/#{project.id}/issues/#{confidential_issue.id}", admin)
expect(response).to have_http_status(200)
expect(json_response['title']).to eq(confidential_issue.title)
@@ -468,7 +469,7 @@ describe API::API, api: true do
end
describe "POST /projects/:id/issues" do
- it "should create a new project issue" do
+ it "creates a new project issue" do
post api("/projects/#{project.id}/issues", user),
title: 'new issue', labels: 'label, label2'
expect(response).to have_http_status(201)
@@ -477,12 +478,12 @@ describe API::API, api: true do
expect(json_response['labels']).to eq(['label', 'label2'])
end
- it "should return a 400 bad request if title not given" do
+ it "returns a 400 bad request if title not given" do
post api("/projects/#{project.id}/issues", user), labels: 'label, label2'
expect(response).to have_http_status(400)
end
- it 'should allow special label names' do
+ it 'allows special label names' do
post api("/projects/#{project.id}/issues", user),
title: 'new issue',
labels: 'label, label?, label&foo, ?, &'
@@ -494,7 +495,7 @@ describe API::API, api: true do
expect(json_response['labels']).to include '&'
end
- it 'should return 400 if title is too long' do
+ it 'returns 400 if title is too long' do
post api("/projects/#{project.id}/issues", user),
title: 'g' * 256
expect(response).to have_http_status(400)
@@ -503,6 +504,20 @@ describe API::API, api: true do
])
end
+ context 'with due date' do
+ it 'creates a new project issue' do
+ due_date = 2.weeks.from_now.strftime('%Y-%m-%d')
+
+ post api("/projects/#{project.id}/issues", user),
+ title: 'new issue', due_date: due_date
+
+ expect(response).to have_http_status(201)
+ expect(json_response['title']).to eq('new issue')
+ expect(json_response['description']).to be_nil
+ expect(json_response['due_date']).to eq(due_date)
+ end
+ end
+
context 'when an admin or owner makes the request' do
it 'accepts the creation date to be set' do
creation_time = 2.weeks.ago
@@ -517,10 +532,8 @@ describe API::API, api: true do
describe 'POST /projects/:id/issues with spam filtering' do
before do
- Grape::Endpoint.before_each do |endpoint|
- allow(endpoint).to receive(:check_for_spam?).and_return(true)
- allow(endpoint).to receive(:is_spam?).and_return(true)
- end
+ allow_any_instance_of(SpamService).to receive(:check_for_spam?).and_return(true)
+ allow_any_instance_of(AkismetService).to receive_messages(is_spam?: true)
end
let(:params) do
@@ -531,7 +544,7 @@ describe API::API, api: true do
}
end
- it "should not create a new project issue" do
+ it "does not create a new project issue" do
expect { post api("/projects/#{project.id}/issues", user), params }.not_to change(Issue, :count)
expect(response).to have_http_status(400)
expect(json_response['message']).to eq({ "error" => "Spam detected" })
@@ -542,12 +555,11 @@ describe API::API, api: true do
expect(spam_logs[0].description).to eq('content here')
expect(spam_logs[0].user).to eq(user)
expect(spam_logs[0].noteable_type).to eq('Issue')
- expect(spam_logs[0].project_id).to eq(project.id)
end
end
describe "PUT /projects/:id/issues/:issue_id to update only title" do
- it "should update a project issue" do
+ it "updates a project issue" do
put api("/projects/#{project.id}/issues/#{issue.id}", user),
title: 'updated title'
expect(response).to have_http_status(200)
@@ -555,13 +567,13 @@ describe API::API, api: true do
expect(json_response['title']).to eq('updated title')
end
- it "should return 404 error if issue id not found" do
+ it "returns 404 error if issue id not found" do
put api("/projects/#{project.id}/issues/44444", user),
title: 'updated title'
expect(response).to have_http_status(404)
end
- it 'should allow special label names' do
+ it 'allows special label names' do
put api("/projects/#{project.id}/issues/#{issue.id}", user),
title: 'updated title',
labels: 'label, label?, label&foo, ?, &'
@@ -575,33 +587,33 @@ describe API::API, api: true do
end
context 'confidential issues' do
- it "should return 403 for non project members" do
+ it "returns 403 for non project members" do
put api("/projects/#{project.id}/issues/#{confidential_issue.id}", non_member),
title: 'updated title'
expect(response).to have_http_status(403)
end
- it "should return 403 for project members with guest role" do
+ it "returns 403 for project members with guest role" do
put api("/projects/#{project.id}/issues/#{confidential_issue.id}", guest),
title: 'updated title'
expect(response).to have_http_status(403)
end
- it "should update a confidential issue for project members" do
+ it "updates a confidential issue for project members" do
put api("/projects/#{project.id}/issues/#{confidential_issue.id}", user),
title: 'updated title'
expect(response).to have_http_status(200)
expect(json_response['title']).to eq('updated title')
end
- it "should update a confidential issue for author" do
+ it "updates a confidential issue for author" do
put api("/projects/#{project.id}/issues/#{confidential_issue.id}", author),
title: 'updated title'
expect(response).to have_http_status(200)
expect(json_response['title']).to eq('updated title')
end
- it "should update a confidential issue for admin" do
+ it "updates a confidential issue for admin" do
put api("/projects/#{project.id}/issues/#{confidential_issue.id}", admin),
title: 'updated title'
expect(response).to have_http_status(200)
@@ -614,21 +626,21 @@ describe API::API, api: true do
let!(:label) { create(:label, title: 'dummy', project: project) }
let!(:label_link) { create(:label_link, label: label, target: issue) }
- it 'should not update labels if not present' do
+ it 'does not update labels if not present' do
put api("/projects/#{project.id}/issues/#{issue.id}", user),
title: 'updated title'
expect(response).to have_http_status(200)
expect(json_response['labels']).to eq([label.title])
end
- it 'should remove all labels' do
+ it 'removes all labels' do
put api("/projects/#{project.id}/issues/#{issue.id}", user),
labels: ''
expect(response).to have_http_status(200)
expect(json_response['labels']).to eq([])
end
- it 'should update labels' do
+ it 'updates labels' do
put api("/projects/#{project.id}/issues/#{issue.id}", user),
labels: 'foo,bar'
expect(response).to have_http_status(200)
@@ -636,7 +648,7 @@ describe API::API, api: true do
expect(json_response['labels']).to include 'bar'
end
- it 'should allow special label names' do
+ it 'allows special label names' do
put api("/projects/#{project.id}/issues/#{issue.id}", user),
labels: 'label:foo, label-bar,label_bar,label/bar,label?bar,label&bar,?,&'
expect(response.status).to eq(200)
@@ -650,7 +662,7 @@ describe API::API, api: true do
expect(json_response['labels']).to include '&'
end
- it 'should return 400 if title is too long' do
+ it 'returns 400 if title is too long' do
put api("/projects/#{project.id}/issues/#{issue.id}", user),
title: 'g' * 256
expect(response).to have_http_status(400)
@@ -661,7 +673,7 @@ describe API::API, api: true do
end
describe "PUT /projects/:id/issues/:issue_id to update state and label" do
- it "should update a project issue" do
+ it "updates a project issue" do
put api("/projects/#{project.id}/issues/#{issue.id}", user),
labels: 'label2', state_event: "close"
expect(response).to have_http_status(200)
@@ -683,6 +695,17 @@ describe API::API, api: true do
end
end
+ describe 'PUT /projects/:id/issues/:issue_id to update due date' do
+ it 'creates a new project issue' do
+ due_date = 2.weeks.from_now.strftime('%Y-%m-%d')
+
+ put api("/projects/#{project.id}/issues/#{issue.id}", user), due_date: due_date
+
+ expect(response).to have_http_status(200)
+ expect(json_response['due_date']).to eq(due_date)
+ end
+ end
+
describe "DELETE /projects/:id/issues/:issue_id" do
it "rejects a non member from deleting an issue" do
delete api("/projects/#{project.id}/issues/#{issue.id}", non_member)
diff --git a/spec/requests/api/keys_spec.rb b/spec/requests/api/keys_spec.rb
index 1861882d59e..893ed5c2b10 100644
--- a/spec/requests/api/keys_spec.rb
+++ b/spec/requests/api/keys_spec.rb
@@ -12,20 +12,20 @@ describe API::API, api: true do
before { admin }
context 'when unauthenticated' do
- it 'should return authentication error' do
+ it 'returns authentication error' do
get api("/keys/#{key.id}")
expect(response).to have_http_status(401)
end
end
context 'when authenticated' do
- it 'should return 404 for non-existing key' do
+ it 'returns 404 for non-existing key' do
get api('/keys/999999', admin)
expect(response).to have_http_status(404)
expect(json_response['message']).to eq('404 Not found')
end
- it 'should return single ssh key with user information' do
+ it 'returns single ssh key with user information' do
user.keys << key
user.save
get api("/keys/#{key.id}", admin)
diff --git a/spec/requests/api/labels_spec.rb b/spec/requests/api/labels_spec.rb
index 63636b4a1b6..83789223019 100644
--- a/spec/requests/api/labels_spec.rb
+++ b/spec/requests/api/labels_spec.rb
@@ -12,7 +12,7 @@ describe API::API, api: true do
end
describe 'GET /projects/:id/labels' do
- it 'should return project labels' do
+ it 'returns project labels' do
get api("/projects/#{project.id}/labels", user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
@@ -22,7 +22,7 @@ describe API::API, api: true do
end
describe 'POST /projects/:id/labels' do
- it 'should return created label when all params' do
+ it 'returns created label when all params' do
post api("/projects/#{project.id}/labels", user),
name: 'Foo',
color: '#FFAABB',
@@ -33,7 +33,7 @@ describe API::API, api: true do
expect(json_response['description']).to eq('test')
end
- it 'should return created label when only required params' do
+ it 'returns created label when only required params' do
post api("/projects/#{project.id}/labels", user),
name: 'Foo & Bar',
color: '#FFAABB'
@@ -43,17 +43,17 @@ describe API::API, api: true do
expect(json_response['description']).to be_nil
end
- it 'should return a 400 bad request if name not given' do
+ it 'returns a 400 bad request if name not given' do
post api("/projects/#{project.id}/labels", user), color: '#FFAABB'
expect(response).to have_http_status(400)
end
- it 'should return a 400 bad request if color not given' do
+ it 'returns a 400 bad request if color not given' do
post api("/projects/#{project.id}/labels", user), name: 'Foobar'
expect(response).to have_http_status(400)
end
- it 'should return 400 for invalid color' do
+ it 'returns 400 for invalid color' do
post api("/projects/#{project.id}/labels", user),
name: 'Foo',
color: '#FFAA'
@@ -61,7 +61,7 @@ describe API::API, api: true do
expect(json_response['message']['color']).to eq(['must be a valid color code'])
end
- it 'should return 400 for too long color code' do
+ it 'returns 400 for too long color code' do
post api("/projects/#{project.id}/labels", user),
name: 'Foo',
color: '#FFAAFFFF'
@@ -69,7 +69,7 @@ describe API::API, api: true do
expect(json_response['message']['color']).to eq(['must be a valid color code'])
end
- it 'should return 400 for invalid name' do
+ it 'returns 400 for invalid name' do
post api("/projects/#{project.id}/labels", user),
name: ',',
color: '#FFAABB'
@@ -77,7 +77,7 @@ describe API::API, api: true do
expect(json_response['message']['title']).to eq(['is invalid'])
end
- it 'should return 409 if label already exists' do
+ it 'returns 409 if label already exists' do
post api("/projects/#{project.id}/labels", user),
name: 'label1',
color: '#FFAABB'
@@ -87,25 +87,25 @@ describe API::API, api: true do
end
describe 'DELETE /projects/:id/labels' do
- it 'should return 200 for existing label' do
+ it 'returns 200 for existing label' do
delete api("/projects/#{project.id}/labels", user), name: 'label1'
expect(response).to have_http_status(200)
end
- it 'should return 404 for non existing label' do
+ it 'returns 404 for non existing label' do
delete api("/projects/#{project.id}/labels", user), name: 'label2'
expect(response).to have_http_status(404)
expect(json_response['message']).to eq('404 Label Not Found')
end
- it 'should return 400 for wrong parameters' do
+ it 'returns 400 for wrong parameters' do
delete api("/projects/#{project.id}/labels", user)
expect(response).to have_http_status(400)
end
end
describe 'PUT /projects/:id/labels' do
- it 'should return 200 if name and colors and description are changed' do
+ it 'returns 200 if name and colors and description are changed' do
put api("/projects/#{project.id}/labels", user),
name: 'label1',
new_name: 'New Label',
@@ -117,7 +117,7 @@ describe API::API, api: true do
expect(json_response['description']).to eq('test')
end
- it 'should return 200 if name is changed' do
+ it 'returns 200 if name is changed' do
put api("/projects/#{project.id}/labels", user),
name: 'label1',
new_name: 'New Label'
@@ -126,7 +126,7 @@ describe API::API, api: true do
expect(json_response['color']).to eq(label1.color)
end
- it 'should return 200 if colors is changed' do
+ it 'returns 200 if colors is changed' do
put api("/projects/#{project.id}/labels", user),
name: 'label1',
color: '#FFFFFF'
@@ -135,7 +135,7 @@ describe API::API, api: true do
expect(json_response['color']).to eq('#FFFFFF')
end
- it 'should return 200 if description is changed' do
+ it 'returns 200 if description is changed' do
put api("/projects/#{project.id}/labels", user),
name: 'label1',
description: 'test'
@@ -144,27 +144,27 @@ describe API::API, api: true do
expect(json_response['description']).to eq('test')
end
- it 'should return 404 if label does not exist' do
+ it 'returns 404 if label does not exist' do
put api("/projects/#{project.id}/labels", user),
name: 'label2',
new_name: 'label3'
expect(response).to have_http_status(404)
end
- it 'should return 400 if no label name given' do
+ it 'returns 400 if no label name given' do
put api("/projects/#{project.id}/labels", user), new_name: 'label2'
expect(response).to have_http_status(400)
expect(json_response['message']).to eq('400 (Bad request) "name" not given')
end
- it 'should return 400 if no new parameters given' do
+ it 'returns 400 if no new parameters given' do
put api("/projects/#{project.id}/labels", user), name: 'label1'
expect(response).to have_http_status(400)
expect(json_response['message']).to eq('Required parameters '\
'"new_name" or "color" missing')
end
- it 'should return 400 for invalid name' do
+ it 'returns 400 for invalid name' do
put api("/projects/#{project.id}/labels", user),
name: 'label1',
new_name: ',',
@@ -173,7 +173,7 @@ describe API::API, api: true do
expect(json_response['message']['title']).to eq(['is invalid'])
end
- it 'should return 400 when color code is too short' do
+ it 'returns 400 when color code is too short' do
put api("/projects/#{project.id}/labels", user),
name: 'label1',
color: '#FF'
@@ -181,7 +181,7 @@ describe API::API, api: true do
expect(json_response['message']['color']).to eq(['must be a valid color code'])
end
- it 'should return 400 for too long color code' do
+ it 'returns 400 for too long color code' do
post api("/projects/#{project.id}/labels", user),
name: 'Foo',
color: '#FFAAFFFF'
@@ -192,7 +192,7 @@ describe API::API, api: true do
describe "POST /projects/:id/labels/:label_id/subscription" do
context "when label_id is a label title" do
- it "should subscribe to the label" do
+ it "subscribes to the label" do
post api("/projects/#{project.id}/labels/#{label1.title}/subscription", user)
expect(response).to have_http_status(201)
@@ -202,7 +202,7 @@ describe API::API, api: true do
end
context "when label_id is a label ID" do
- it "should subscribe to the label" do
+ it "subscribes to the label" do
post api("/projects/#{project.id}/labels/#{label1.id}/subscription", user)
expect(response).to have_http_status(201)
@@ -214,7 +214,7 @@ describe API::API, api: true do
context "when user is already subscribed to label" do
before { label1.subscribe(user) }
- it "should return 304" do
+ it "returns 304" do
post api("/projects/#{project.id}/labels/#{label1.id}/subscription", user)
expect(response).to have_http_status(304)
@@ -222,7 +222,7 @@ describe API::API, api: true do
end
context "when label ID is not found" do
- it "should a return 404 error" do
+ it "returns 404 error" do
post api("/projects/#{project.id}/labels/1234/subscription", user)
expect(response).to have_http_status(404)
@@ -234,7 +234,7 @@ describe API::API, api: true do
before { label1.subscribe(user) }
context "when label_id is a label title" do
- it "should unsubscribe from the label" do
+ it "unsubscribes from the label" do
delete api("/projects/#{project.id}/labels/#{label1.title}/subscription", user)
expect(response).to have_http_status(200)
@@ -244,7 +244,7 @@ describe API::API, api: true do
end
context "when label_id is a label ID" do
- it "should unsubscribe from the label" do
+ it "unsubscribes from the label" do
delete api("/projects/#{project.id}/labels/#{label1.id}/subscription", user)
expect(response).to have_http_status(200)
@@ -256,7 +256,7 @@ describe API::API, api: true do
context "when user is already unsubscribed from label" do
before { label1.unsubscribe(user) }
- it "should return 304" do
+ it "returns 304" do
delete api("/projects/#{project.id}/labels/#{label1.id}/subscription", user)
expect(response).to have_http_status(304)
@@ -264,7 +264,7 @@ describe API::API, api: true do
end
context "when label ID is not found" do
- it "should a return 404 error" do
+ it "returns 404 error" do
delete api("/projects/#{project.id}/labels/1234/subscription", user)
expect(response).to have_http_status(404)
diff --git a/spec/requests/api/members_spec.rb b/spec/requests/api/members_spec.rb
new file mode 100644
index 00000000000..1e365bf353a
--- /dev/null
+++ b/spec/requests/api/members_spec.rb
@@ -0,0 +1,314 @@
+require 'spec_helper'
+
+describe API::Members, api: true do
+ include ApiHelpers
+
+ let(:master) { create(:user) }
+ let(:developer) { create(:user) }
+ let(:access_requester) { create(:user) }
+ let(:stranger) { create(:user) }
+
+ let(:project) do
+ project = create(:project, :public, creator_id: master.id, namespace: master.namespace)
+ project.team << [developer, :developer]
+ project.team << [master, :master]
+ project.request_access(access_requester)
+ project
+ end
+
+ let!(:group) do
+ group = create(:group, :public)
+ group.add_developer(developer)
+ group.add_owner(master)
+ group.request_access(access_requester)
+ group
+ end
+
+ shared_examples 'GET /:sources/:id/members' do |source_type|
+ context "with :sources == #{source_type.pluralize}" do
+ it_behaves_like 'a 404 response when source is private' do
+ let(:route) { get api("/#{source_type.pluralize}/#{source.id}/members", stranger) }
+ end
+
+ context 'when authenticated as a non-member' do
+ %i[access_requester stranger].each do |type|
+ context "as a #{type}" do
+ it 'returns 200' do
+ user = public_send(type)
+ get api("/#{source_type.pluralize}/#{source.id}/members", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response.size).to eq(2)
+ end
+ end
+ end
+ end
+
+ it 'finds members with query string' do
+ get api("/#{source_type.pluralize}/#{source.id}/members", developer), query: master.username
+
+ expect(response).to have_http_status(200)
+ expect(json_response.count).to eq(1)
+ expect(json_response.first['username']).to eq(master.username)
+ end
+ end
+ end
+
+ shared_examples 'GET /:sources/:id/members/:user_id' do |source_type|
+ context "with :sources == #{source_type.pluralize}" do
+ it_behaves_like 'a 404 response when source is private' do
+ let(:route) { get api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", stranger) }
+ end
+
+ context 'when authenticated as a non-member' do
+ %i[access_requester stranger].each do |type|
+ context "as a #{type}" do
+ it 'returns 200' do
+ user = public_send(type)
+ get api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", user)
+
+ expect(response).to have_http_status(200)
+ # User attributes
+ expect(json_response['id']).to eq(developer.id)
+ expect(json_response['name']).to eq(developer.name)
+ expect(json_response['username']).to eq(developer.username)
+ expect(json_response['state']).to eq(developer.state)
+ expect(json_response['avatar_url']).to eq(developer.avatar_url)
+ expect(json_response['web_url']).to eq(Gitlab::Routing.url_helpers.user_url(developer))
+
+ # Member attributes
+ expect(json_response['access_level']).to eq(Member::DEVELOPER)
+ end
+ end
+ end
+ end
+ end
+ end
+
+ shared_examples 'POST /:sources/:id/members' do |source_type|
+ context "with :sources == #{source_type.pluralize}" do
+ it_behaves_like 'a 404 response when source is private' do
+ let(:route) { post api("/#{source_type.pluralize}/#{source.id}/members", stranger) }
+ end
+
+ context 'when authenticated as a non-member or member with insufficient rights' do
+ %i[access_requester stranger developer].each do |type|
+ context "as a #{type}" do
+ it 'returns 403' do
+ user = public_send(type)
+ post api("/#{source_type.pluralize}/#{source.id}/members", user)
+
+ expect(response).to have_http_status(403)
+ end
+ end
+ end
+ end
+
+ context 'when authenticated as a master/owner' do
+ context 'and new member is already a requester' do
+ it 'transforms the requester into a proper member' do
+ expect do
+ post api("/#{source_type.pluralize}/#{source.id}/members", master),
+ user_id: access_requester.id, access_level: Member::MASTER
+
+ expect(response).to have_http_status(201)
+ end.to change { source.members.count }.by(1)
+ expect(source.requesters.count).to eq(0)
+ expect(json_response['id']).to eq(access_requester.id)
+ expect(json_response['access_level']).to eq(Member::MASTER)
+ end
+ end
+
+ it 'creates a new member' do
+ expect do
+ post api("/#{source_type.pluralize}/#{source.id}/members", master),
+ user_id: stranger.id, access_level: Member::DEVELOPER, expires_at: '2016-08-05'
+
+ expect(response).to have_http_status(201)
+ end.to change { source.members.count }.by(1)
+ expect(json_response['id']).to eq(stranger.id)
+ expect(json_response['access_level']).to eq(Member::DEVELOPER)
+ expect(json_response['expires_at']).to eq('2016-08-05')
+ end
+ end
+
+ it "returns #{source_type == 'project' ? 201 : 409} if member already exists" do
+ post api("/#{source_type.pluralize}/#{source.id}/members", master),
+ user_id: master.id, access_level: Member::MASTER
+
+ expect(response).to have_http_status(source_type == 'project' ? 201 : 409)
+ end
+
+ it 'returns 400 when user_id is not given' do
+ post api("/#{source_type.pluralize}/#{source.id}/members", master),
+ access_level: Member::MASTER
+
+ expect(response).to have_http_status(400)
+ end
+
+ it 'returns 400 when access_level is not given' do
+ post api("/#{source_type.pluralize}/#{source.id}/members", master),
+ user_id: stranger.id
+
+ expect(response).to have_http_status(400)
+ end
+
+ it 'returns 422 when access_level is not valid' do
+ post api("/#{source_type.pluralize}/#{source.id}/members", master),
+ user_id: stranger.id, access_level: 1234
+
+ expect(response).to have_http_status(422)
+ end
+ end
+ end
+
+ shared_examples 'PUT /:sources/:id/members/:user_id' do |source_type|
+ context "with :sources == #{source_type.pluralize}" do
+ it_behaves_like 'a 404 response when source is private' do
+ let(:route) { put api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", stranger) }
+ end
+
+ context 'when authenticated as a non-member or member with insufficient rights' do
+ %i[access_requester stranger developer].each do |type|
+ context "as a #{type}" do
+ it 'returns 403' do
+ user = public_send(type)
+ put api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", user)
+
+ expect(response).to have_http_status(403)
+ end
+ end
+ end
+ end
+
+ context 'when authenticated as a master/owner' do
+ it 'updates the member' do
+ put api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", master),
+ access_level: Member::MASTER, expires_at: '2016-08-05'
+
+ expect(response).to have_http_status(200)
+ expect(json_response['id']).to eq(developer.id)
+ expect(json_response['access_level']).to eq(Member::MASTER)
+ expect(json_response['expires_at']).to eq('2016-08-05')
+ end
+ end
+
+ it 'returns 409 if member does not exist' do
+ put api("/#{source_type.pluralize}/#{source.id}/members/123", master),
+ access_level: Member::MASTER
+
+ expect(response).to have_http_status(404)
+ end
+
+ it 'returns 400 when access_level is not given' do
+ put api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", master)
+
+ expect(response).to have_http_status(400)
+ end
+
+ it 'returns 422 when access level is not valid' do
+ put api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", master),
+ access_level: 1234
+
+ expect(response).to have_http_status(422)
+ end
+ end
+ end
+
+ shared_examples 'DELETE /:sources/:id/members/:user_id' do |source_type|
+ context "with :sources == #{source_type.pluralize}" do
+ it_behaves_like 'a 404 response when source is private' do
+ let(:route) { delete api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", stranger) }
+ end
+
+ context 'when authenticated as a non-member or member with insufficient rights' do
+ %i[access_requester stranger].each do |type|
+ context "as a #{type}" do
+ it 'returns 403' do
+ user = public_send(type)
+ delete api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", user)
+
+ expect(response).to have_http_status(403)
+ end
+ end
+ end
+ end
+
+ context 'when authenticated as a member and deleting themself' do
+ it 'deletes the member' do
+ expect do
+ delete api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", developer)
+
+ expect(response).to have_http_status(200)
+ end.to change { source.members.count }.by(-1)
+ end
+ end
+
+ context 'when authenticated as a master/owner' do
+ context 'and member is a requester' do
+ it "returns #{source_type == 'project' ? 200 : 404}" do
+ expect do
+ delete api("/#{source_type.pluralize}/#{source.id}/members/#{access_requester.id}", master)
+
+ expect(response).to have_http_status(source_type == 'project' ? 200 : 404)
+ end.not_to change { source.requesters.count }
+ end
+ end
+
+ it 'deletes the member' do
+ expect do
+ delete api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", master)
+
+ expect(response).to have_http_status(200)
+ end.to change { source.members.count }.by(-1)
+ end
+ end
+
+ it "returns #{source_type == 'project' ? 200 : 404} if member does not exist" do
+ delete api("/#{source_type.pluralize}/#{source.id}/members/123", master)
+
+ expect(response).to have_http_status(source_type == 'project' ? 200 : 404)
+ end
+ end
+ end
+
+ it_behaves_like 'GET /:sources/:id/members', 'project' do
+ let(:source) { project }
+ end
+
+ it_behaves_like 'GET /:sources/:id/members', 'group' do
+ let(:source) { group }
+ end
+
+ it_behaves_like 'GET /:sources/:id/members/:user_id', 'project' do
+ let(:source) { project }
+ end
+
+ it_behaves_like 'GET /:sources/:id/members/:user_id', 'group' do
+ let(:source) { group }
+ end
+
+ it_behaves_like 'POST /:sources/:id/members', 'project' do
+ let(:source) { project }
+ end
+
+ it_behaves_like 'POST /:sources/:id/members', 'group' do
+ let(:source) { group }
+ end
+
+ it_behaves_like 'PUT /:sources/:id/members/:user_id', 'project' do
+ let(:source) { project }
+ end
+
+ it_behaves_like 'PUT /:sources/:id/members/:user_id', 'group' do
+ let(:source) { group }
+ end
+
+ it_behaves_like 'DELETE /:sources/:id/members/:user_id', 'project' do
+ let(:source) { project }
+ end
+
+ it_behaves_like 'DELETE /:sources/:id/members/:user_id', 'group' do
+ let(:source) { group }
+ end
+end
diff --git a/spec/requests/api/merge_request_diffs_spec.rb b/spec/requests/api/merge_request_diffs_spec.rb
new file mode 100644
index 00000000000..8f1e5ac9891
--- /dev/null
+++ b/spec/requests/api/merge_request_diffs_spec.rb
@@ -0,0 +1,49 @@
+require "spec_helper"
+
+describe API::API, 'MergeRequestDiffs', api: true do
+ include ApiHelpers
+
+ let!(:user) { create(:user) }
+ let!(:merge_request) { create(:merge_request, importing: true) }
+ let!(:project) { merge_request.target_project }
+
+ before do
+ merge_request.merge_request_diffs.create(head_commit_sha: '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9')
+ merge_request.merge_request_diffs.create(head_commit_sha: '5937ac0a7beb003549fc5fd26fc247adbce4a52e')
+ project.team << [user, :master]
+ end
+
+ describe 'GET /projects/:id/merge_requests/:merge_request_id/versions' do
+ context 'valid merge request' do
+ before { get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/versions", user) }
+ let(:merge_request_diff) { merge_request.merge_request_diffs.first }
+
+ it { expect(response.status).to eq 200 }
+ it { expect(json_response.size).to eq(merge_request.merge_request_diffs.size) }
+ it { expect(json_response.first['id']).to eq(merge_request_diff.id) }
+ it { expect(json_response.first['head_commit_sha']).to eq(merge_request_diff.head_commit_sha) }
+ end
+
+ it 'returns a 404 when merge_request_id not found' do
+ get api("/projects/#{project.id}/merge_requests/999/versions", user)
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ describe 'GET /projects/:id/merge_requests/:merge_request_id/versions/:version_id' do
+ context 'valid merge request' do
+ before { get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/versions/#{merge_request_diff.id}", user) }
+ let(:merge_request_diff) { merge_request.merge_request_diffs.first }
+
+ it { expect(response.status).to eq 200 }
+ it { expect(json_response['id']).to eq(merge_request_diff.id) }
+ it { expect(json_response['head_commit_sha']).to eq(merge_request_diff.head_commit_sha) }
+ it { expect(json_response['diffs'].size).to eq(merge_request_diff.diffs.size) }
+ end
+
+ it 'returns a 404 when merge_request_id not found' do
+ get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/versions/999", user)
+ expect(response).to have_http_status(404)
+ end
+ end
+end
diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb
index 4a1b5600bdf..baff872e28e 100644
--- a/spec/requests/api/merge_requests_spec.rb
+++ b/spec/requests/api/merge_requests_spec.rb
@@ -20,22 +20,23 @@ describe API::API, api: true do
describe "GET /projects/:id/merge_requests" do
context "when unauthenticated" do
- it "should return authentication error" do
+ it "returns authentication error" do
get api("/projects/#{project.id}/merge_requests")
expect(response).to have_http_status(401)
end
end
context "when authenticated" do
- it "should return an array of all merge_requests" do
+ it "returns an array of all merge_requests" do
get api("/projects/#{project.id}/merge_requests", user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(3)
expect(json_response.last['title']).to eq(merge_request.title)
+ expect(json_response.last).to have_key('web_url')
end
- it "should return an array of all merge_requests" do
+ it "returns an array of all merge_requests" do
get api("/projects/#{project.id}/merge_requests?state", user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
@@ -43,7 +44,7 @@ describe API::API, api: true do
expect(json_response.last['title']).to eq(merge_request.title)
end
- it "should return an array of open merge_requests" do
+ it "returns an array of open merge_requests" do
get api("/projects/#{project.id}/merge_requests?state=opened", user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
@@ -51,7 +52,7 @@ describe API::API, api: true do
expect(json_response.last['title']).to eq(merge_request.title)
end
- it "should return an array of closed merge_requests" do
+ it "returns an array of closed merge_requests" do
get api("/projects/#{project.id}/merge_requests?state=closed", user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
@@ -59,7 +60,7 @@ describe API::API, api: true do
expect(json_response.first['title']).to eq(merge_request_closed.title)
end
- it "should return an array of merged merge_requests" do
+ it "returns an array of merged merge_requests" do
get api("/projects/#{project.id}/merge_requests?state=merged", user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
@@ -73,7 +74,7 @@ describe API::API, api: true do
@mr_earlier = mr_with_earlier_created_and_updated_at_time
end
- it "should return an array of merge_requests in ascending order" do
+ it "returns an array of merge_requests in ascending order" do
get api("/projects/#{project.id}/merge_requests?sort=asc", user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
@@ -82,7 +83,7 @@ describe API::API, api: true do
expect(response_dates).to eq(response_dates.sort)
end
- it "should return an array of merge_requests in descending order" do
+ it "returns an array of merge_requests in descending order" do
get api("/projects/#{project.id}/merge_requests?sort=desc", user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
@@ -91,7 +92,7 @@ describe API::API, api: true do
expect(response_dates).to eq(response_dates.sort.reverse)
end
- it "should return an array of merge_requests ordered by updated_at" do
+ it "returns an array of merge_requests ordered by updated_at" do
get api("/projects/#{project.id}/merge_requests?order_by=updated_at", user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
@@ -100,7 +101,7 @@ describe API::API, api: true do
expect(response_dates).to eq(response_dates.sort.reverse)
end
- it "should return an array of merge_requests ordered by created_at" do
+ it "returns an array of merge_requests ordered by created_at" do
get api("/projects/#{project.id}/merge_requests?order_by=created_at&sort=asc", user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
@@ -138,18 +139,22 @@ 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['should_close_merge_request']).to be_falsy
+ expect(json_response['force_close_merge_request']).to be_falsy
end
- it "should return merge_request" do
+ it "returns merge_request" do
get api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user)
expect(response).to have_http_status(200)
expect(json_response['title']).to eq(merge_request.title)
expect(json_response['iid']).to eq(merge_request.iid)
expect(json_response['work_in_progress']).to eq(false)
expect(json_response['merge_status']).to eq('can_be_merged')
+ expect(json_response['should_close_merge_request']).to be_falsy
+ expect(json_response['force_close_merge_request']).to be_falsy
end
- it 'should return merge_request by iid' do
+ it 'returns merge_request by iid' do
url = "/projects/#{project.id}/merge_requests?iid=#{merge_request.iid}"
get api(url, user)
expect(response.status).to eq 200
@@ -157,7 +162,7 @@ describe API::API, api: true do
expect(json_response.first['id']).to eq merge_request.id
end
- it "should return a 404 error if merge_request_id not found" do
+ it "returns a 404 error if merge_request_id not found" do
get api("/projects/#{project.id}/merge_requests/999", user)
expect(response).to have_http_status(404)
end
@@ -165,7 +170,7 @@ describe API::API, api: true do
context 'Work in Progress' do
let!(:merge_request_wip) { create(:merge_request, author: user, assignee: user, source_project: project, target_project: project, title: "WIP: Test", created_at: base_time + 1.second) }
- it "should return merge_request" do
+ it "returns merge_request" do
get api("/projects/#{project.id}/merge_requests/#{merge_request_wip.id}", user)
expect(response).to have_http_status(200)
expect(json_response['work_in_progress']).to eq(true)
@@ -191,7 +196,7 @@ describe API::API, api: true do
end
describe 'GET /projects/:id/merge_requests/:merge_request_id/changes' do
- it 'should return the change information of the merge_request' do
+ it 'returns the change information of the merge_request' do
get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/changes", user)
expect(response.status).to eq 200
expect(json_response['changes'].size).to eq(merge_request.diffs.size)
@@ -205,7 +210,7 @@ describe API::API, api: true do
describe "POST /projects/:id/merge_requests" do
context 'between branches projects' do
- it "should return merge_request" do
+ it "returns merge_request" do
post api("/projects/#{project.id}/merge_requests", user),
title: 'Test merge_request',
source_branch: 'feature_conflict',
@@ -219,31 +224,31 @@ describe API::API, api: true do
expect(json_response['milestone']['id']).to eq(milestone.id)
end
- it "should return 422 when source_branch equals target_branch" do
+ it "returns 422 when source_branch equals target_branch" do
post api("/projects/#{project.id}/merge_requests", user),
title: "Test merge_request", source_branch: "master", target_branch: "master", author: user
expect(response).to have_http_status(422)
end
- it "should return 400 when source_branch is missing" do
+ it "returns 400 when source_branch is missing" do
post api("/projects/#{project.id}/merge_requests", user),
title: "Test merge_request", target_branch: "master", author: user
expect(response).to have_http_status(400)
end
- it "should return 400 when target_branch is missing" do
+ it "returns 400 when target_branch is missing" do
post api("/projects/#{project.id}/merge_requests", user),
title: "Test merge_request", source_branch: "markdown", author: user
expect(response).to have_http_status(400)
end
- it "should return 400 when title is missing" do
+ it "returns 400 when title is missing" do
post api("/projects/#{project.id}/merge_requests", user),
target_branch: 'master', source_branch: 'markdown'
expect(response).to have_http_status(400)
end
- it 'should allow special label names' do
+ it 'allows special label names' do
post api("/projects/#{project.id}/merge_requests", user),
title: 'Test merge_request',
source_branch: 'markdown',
@@ -268,7 +273,7 @@ describe API::API, api: true do
@mr = MergeRequest.all.last
end
- it 'should return 409 when MR already exists for source/target' do
+ it 'returns 409 when MR already exists for source/target' do
expect do
post api("/projects/#{project.id}/merge_requests", user),
title: 'New test merge_request',
@@ -290,7 +295,7 @@ describe API::API, api: true do
fork_project.team << [user2, :reporters]
end
- it "should return merge_request" do
+ it "returns merge_request" do
post api("/projects/#{fork_project.id}/merge_requests", user2),
title: 'Test merge_request', source_branch: "feature_conflict", target_branch: "master",
author: user2, target_project_id: project.id, description: 'Test description for Test merge_request'
@@ -299,7 +304,7 @@ describe API::API, api: true do
expect(json_response['description']).to eq('Test description for Test merge_request')
end
- it "should not return 422 when source_branch equals target_branch" do
+ it "does not return 422 when source_branch equals target_branch" do
expect(project.id).not_to eq(fork_project.id)
expect(fork_project.forked?).to be_truthy
expect(fork_project.forked_from_project).to eq(project)
@@ -309,26 +314,26 @@ describe API::API, api: true do
expect(json_response['title']).to eq('Test merge_request')
end
- it "should return 400 when source_branch is missing" do
+ it "returns 400 when source_branch is missing" do
post api("/projects/#{fork_project.id}/merge_requests", user2),
title: 'Test merge_request', target_branch: "master", author: user2, target_project_id: project.id
expect(response).to have_http_status(400)
end
- it "should return 400 when target_branch is missing" do
+ it "returns 400 when target_branch is missing" do
post api("/projects/#{fork_project.id}/merge_requests", user2),
title: 'Test merge_request', target_branch: "master", author: user2, target_project_id: project.id
expect(response).to have_http_status(400)
end
- it "should return 400 when title is missing" do
+ it "returns 400 when title is missing" do
post api("/projects/#{fork_project.id}/merge_requests", user2),
target_branch: 'master', source_branch: 'markdown', author: user2, target_project_id: project.id
expect(response).to have_http_status(400)
end
context 'when target_branch is specified' do
- it 'should return 422 if not a forked project' do
+ it 'returns 422 if not a forked project' do
post api("/projects/#{project.id}/merge_requests", user),
title: 'Test merge_request',
target_branch: 'master',
@@ -338,7 +343,7 @@ describe API::API, api: true do
expect(response).to have_http_status(422)
end
- it 'should return 422 if targeting a different fork' do
+ it 'returns 422 if targeting a different fork' do
post api("/projects/#{fork_project.id}/merge_requests", user2),
title: 'Test merge_request',
target_branch: 'master',
@@ -349,7 +354,7 @@ describe API::API, api: true do
end
end
- it "should return 201 when target_branch is specified and for the same project" do
+ it "returns 201 when target_branch is specified and for the same project" do
post api("/projects/#{fork_project.id}/merge_requests", user2),
title: 'Test merge_request', target_branch: 'master', source_branch: 'markdown', author: user2, target_project_id: fork_project.id
expect(response).to have_http_status(201)
@@ -381,7 +386,7 @@ describe API::API, api: true do
end
describe "PUT /projects/:id/merge_requests/:merge_request_id to close MR" do
- it "should return merge_request" do
+ it "returns merge_request" do
put api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), state_event: "close"
expect(response).to have_http_status(200)
expect(json_response['state']).to eq('closed')
@@ -391,13 +396,13 @@ describe API::API, api: true do
describe "PUT /projects/:id/merge_requests/:merge_request_id/merge" do
let(:pipeline) { create(:ci_pipeline_without_jobs) }
- it "should return merge_request in case of success" do
+ it "returns merge_request in case of success" do
put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user)
expect(response).to have_http_status(200)
end
- it "should return 406 if branch can't be merged" do
+ it "returns 406 if branch can't be merged" do
allow_any_instance_of(MergeRequest).
to receive(:can_be_merged?).and_return(false)
@@ -407,14 +412,14 @@ describe API::API, api: true do
expect(json_response['message']).to eq('Branch cannot be merged')
end
- it "should return 405 if merge_request is not open" do
+ it "returns 405 if merge_request is not open" do
merge_request.close
put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user)
expect(response).to have_http_status(405)
expect(json_response['message']).to eq('405 Method Not Allowed')
end
- it "should return 405 if merge_request is a work in progress" do
+ it "returns 405 if merge_request is a work in progress" do
merge_request.update_attribute(:title, "WIP: #{merge_request.title}")
put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user)
expect(response).to have_http_status(405)
@@ -430,7 +435,7 @@ describe API::API, api: true do
expect(json_response['message']).to eq('405 Method Not Allowed')
end
- it "should return 401 if user has no permissions to merge" do
+ it "returns 401 if user has no permissions to merge" do
user2 = create(:user)
project.team << [user2, :reporter]
put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user2)
@@ -482,19 +487,19 @@ describe API::API, api: true do
expect(json_response['milestone']['id']).to eq(milestone.id)
end
- it "should return 400 when source_branch is specified" do
+ it "returns 400 when source_branch is specified" do
put api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user),
source_branch: "master", target_branch: "master"
expect(response).to have_http_status(400)
end
- it "should return merge_request with renamed target_branch" do
+ it "returns merge_request with renamed target_branch" do
put api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), target_branch: "wiki"
expect(response).to have_http_status(200)
expect(json_response['target_branch']).to eq('wiki')
end
- it 'should allow special label names' do
+ it 'allows special label names' do
put api("/projects/#{project.id}/merge_requests/#{merge_request.id}",
user),
title: 'new issue',
@@ -509,7 +514,7 @@ describe API::API, api: true do
end
describe "POST /projects/:id/merge_requests/:merge_request_id/comments" do
- it "should return comment" do
+ it "returns comment" do
original_count = merge_request.notes.size
post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/comments", user), note: "My comment"
@@ -520,12 +525,12 @@ describe API::API, api: true do
expect(merge_request.notes.size).to eq(original_count + 1)
end
- it "should return 400 if note is missing" do
+ it "returns 400 if note is missing" do
post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/comments", user)
expect(response).to have_http_status(400)
end
- it "should return 404 if note is attached to non existent merge request" do
+ it "returns 404 if note is attached to non existent merge request" do
post api("/projects/#{project.id}/merge_requests/404/comments", user),
note: 'My comment'
expect(response).to have_http_status(404)
@@ -533,7 +538,7 @@ describe API::API, api: true do
end
describe "GET :id/merge_requests/:merge_request_id/comments" do
- it "should return merge_request comments ordered by created_at" do
+ it "returns merge_request comments ordered by created_at" do
get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/comments", user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
@@ -543,7 +548,7 @@ describe API::API, api: true do
expect(json_response.last['note']).to eq("another comment on a MR")
end
- it "should return a 404 error if merge_request_id not found" do
+ it "returns a 404 error if merge_request_id not found" do
get api("/projects/#{project.id}/merge_requests/999/comments", user)
expect(response).to have_http_status(404)
end
diff --git a/spec/requests/api/milestones_spec.rb b/spec/requests/api/milestones_spec.rb
index 0f4e38b2475..d6a0c656e74 100644
--- a/spec/requests/api/milestones_spec.rb
+++ b/spec/requests/api/milestones_spec.rb
@@ -10,14 +10,14 @@ describe API::API, api: true do
before { project.team << [user, :developer] }
describe 'GET /projects/:id/milestones' do
- it 'should return project milestones' do
+ it 'returns project milestones' do
get api("/projects/#{project.id}/milestones", user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.first['title']).to eq(milestone.title)
end
- it 'should return a 401 error if user not authenticated' do
+ it 'returns a 401 error if user not authenticated' do
get api("/projects/#{project.id}/milestones")
expect(response).to have_http_status(401)
end
@@ -42,14 +42,14 @@ describe API::API, api: true do
end
describe 'GET /projects/:id/milestones/:milestone_id' do
- it 'should return a project milestone by id' do
+ it 'returns a project milestone by id' do
get api("/projects/#{project.id}/milestones/#{milestone.id}", user)
expect(response).to have_http_status(200)
expect(json_response['title']).to eq(milestone.title)
expect(json_response['iid']).to eq(milestone.iid)
end
- it 'should return a project milestone by iid' do
+ it 'returns a project milestone by iid' do
get api("/projects/#{project.id}/milestones?iid=#{closed_milestone.iid}", user)
expect(response.status).to eq 200
@@ -58,26 +58,26 @@ describe API::API, api: true do
expect(json_response.first['id']).to eq closed_milestone.id
end
- it 'should return 401 error if user not authenticated' do
+ it 'returns 401 error if user not authenticated' do
get api("/projects/#{project.id}/milestones/#{milestone.id}")
expect(response).to have_http_status(401)
end
- it 'should return a 404 error if milestone id not found' do
+ it 'returns a 404 error if milestone id not found' do
get api("/projects/#{project.id}/milestones/1234", user)
expect(response).to have_http_status(404)
end
end
describe 'POST /projects/:id/milestones' do
- it 'should create a new project milestone' do
+ it 'creates a new project milestone' do
post api("/projects/#{project.id}/milestones", user), title: 'new milestone'
expect(response).to have_http_status(201)
expect(json_response['title']).to eq('new milestone')
expect(json_response['description']).to be_nil
end
- it 'should create a new project milestone with description and due date' do
+ it 'creates a new project milestone with description and due date' do
post api("/projects/#{project.id}/milestones", user),
title: 'new milestone', description: 'release', due_date: '2013-03-02'
expect(response).to have_http_status(201)
@@ -85,21 +85,21 @@ describe API::API, api: true do
expect(json_response['due_date']).to eq('2013-03-02')
end
- it 'should return a 400 error if title is missing' do
+ it 'returns a 400 error if title is missing' do
post api("/projects/#{project.id}/milestones", user)
expect(response).to have_http_status(400)
end
end
describe 'PUT /projects/:id/milestones/:milestone_id' do
- it 'should update a project milestone' do
+ it 'updates a project milestone' do
put api("/projects/#{project.id}/milestones/#{milestone.id}", user),
title: 'updated title'
expect(response).to have_http_status(200)
expect(json_response['title']).to eq('updated title')
end
- it 'should return a 404 error if milestone id not found' do
+ it 'returns a 404 error if milestone id not found' do
put api("/projects/#{project.id}/milestones/1234", user),
title: 'updated title'
expect(response).to have_http_status(404)
@@ -107,7 +107,7 @@ describe API::API, api: true do
end
describe 'PUT /projects/:id/milestones/:milestone_id to close milestone' do
- it 'should update a project milestone' do
+ it 'updates a project milestone' do
put api("/projects/#{project.id}/milestones/#{milestone.id}", user),
state_event: 'close'
expect(response).to have_http_status(200)
@@ -117,7 +117,7 @@ describe API::API, api: true do
end
describe 'PUT /projects/:id/milestones/:milestone_id to test observer on close' do
- it 'should create an activity event when an milestone is closed' do
+ it 'creates an activity event when an milestone is closed' do
expect(Event).to receive(:create)
put api("/projects/#{project.id}/milestones/#{milestone.id}", user),
@@ -129,14 +129,14 @@ describe API::API, api: true do
before do
milestone.issues << create(:issue, project: project)
end
- it 'should return project issues for a particular milestone' do
+ it 'returns project issues for a particular milestone' do
get api("/projects/#{project.id}/milestones/#{milestone.id}/issues", user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.first['milestone']['title']).to eq(milestone.title)
end
- it 'should return a 401 error if user not authenticated' do
+ it 'returns a 401 error if user not authenticated' do
get api("/projects/#{project.id}/milestones/#{milestone.id}/issues")
expect(response).to have_http_status(401)
end
diff --git a/spec/requests/api/namespaces_spec.rb b/spec/requests/api/namespaces_spec.rb
index 237b4b17eb5..5347cf4f7bc 100644
--- a/spec/requests/api/namespaces_spec.rb
+++ b/spec/requests/api/namespaces_spec.rb
@@ -9,14 +9,14 @@ describe API::API, api: true do
describe "GET /namespaces" do
context "when unauthenticated" do
- it "should return authentication error" do
+ it "returns authentication error" do
get api("/namespaces")
expect(response).to have_http_status(401)
end
end
context "when authenticated as admin" do
- it "admin: should return an array of all namespaces" do
+ it "admin: returns an array of all namespaces" do
get api("/namespaces", admin)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
@@ -24,7 +24,7 @@ describe API::API, api: true do
expect(json_response.length).to eq(Namespace.count)
end
- it "admin: should return an array of matched namespaces" do
+ it "admin: returns an array of matched namespaces" do
get api("/namespaces?search=#{group1.name}", admin)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
@@ -34,7 +34,7 @@ describe API::API, api: true do
end
context "when authenticated as a regular user" do
- it "user: should return an array of namespaces" do
+ it "user: returns an array of namespaces" do
get api("/namespaces", user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
@@ -42,7 +42,7 @@ describe API::API, api: true do
expect(json_response.length).to eq(1)
end
- it "admin: should return an array of matched namespaces" do
+ it "admin: returns an array of matched namespaces" do
get api("/namespaces?search=#{user.username}", user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
diff --git a/spec/requests/api/notes_spec.rb b/spec/requests/api/notes_spec.rb
index 65c53211dd3..223444ea39f 100644
--- a/spec/requests/api/notes_spec.rb
+++ b/spec/requests/api/notes_spec.rb
@@ -25,7 +25,7 @@ describe API::API, api: true do
let!(:cross_reference_note) do
create :note,
noteable: ext_issue, project: ext_proj,
- note: "mentioned in issue #{private_issue.to_reference(ext_proj)}",
+ note: "Mentioned in issue #{private_issue.to_reference(ext_proj)}",
system: true
end
@@ -37,7 +37,7 @@ describe API::API, api: true do
end
context "when noteable is an Issue" do
- it "should return an array of issue notes" do
+ it "returns an array of issue notes" do
get api("/projects/#{project.id}/issues/#{issue.id}/notes", user)
expect(response).to have_http_status(200)
@@ -45,14 +45,14 @@ describe API::API, api: true do
expect(json_response.first['body']).to eq(issue_note.note)
end
- it "should return a 404 error when issue id not found" do
+ it "returns a 404 error when issue id not found" do
get api("/projects/#{project.id}/issues/12345/notes", user)
expect(response).to have_http_status(404)
end
context "and current user cannot view the notes" do
- it "should return an empty array" do
+ it "returns an empty array" do
get api("/projects/#{ext_proj.id}/issues/#{ext_issue.id}/notes", user)
expect(response).to have_http_status(200)
@@ -71,7 +71,7 @@ describe API::API, api: true do
end
context "and current user can view the note" do
- it "should return an empty array" do
+ it "returns an empty array" do
get api("/projects/#{ext_proj.id}/issues/#{ext_issue.id}/notes", private_user)
expect(response).to have_http_status(200)
@@ -83,7 +83,7 @@ describe API::API, api: true do
end
context "when noteable is a Snippet" do
- it "should return an array of snippet notes" do
+ it "returns an array of snippet notes" do
get api("/projects/#{project.id}/snippets/#{snippet.id}/notes", user)
expect(response).to have_http_status(200)
@@ -91,7 +91,7 @@ describe API::API, api: true do
expect(json_response.first['body']).to eq(snippet_note.note)
end
- it "should return a 404 error when snippet id not found" do
+ it "returns a 404 error when snippet id not found" do
get api("/projects/#{project.id}/snippets/42/notes", user)
expect(response).to have_http_status(404)
@@ -105,7 +105,7 @@ describe API::API, api: true do
end
context "when noteable is a Merge Request" do
- it "should return an array of merge_requests notes" do
+ it "returns an array of merge_requests notes" do
get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/notes", user)
expect(response).to have_http_status(200)
@@ -113,7 +113,7 @@ describe API::API, api: true do
expect(json_response.first['body']).to eq(merge_request_note.note)
end
- it "should return a 404 error if merge request id not found" do
+ it "returns a 404 error if merge request id not found" do
get api("/projects/#{project.id}/merge_requests/4444/notes", user)
expect(response).to have_http_status(404)
@@ -129,21 +129,21 @@ describe API::API, api: true do
describe "GET /projects/:id/noteable/:noteable_id/notes/:note_id" do
context "when noteable is an Issue" do
- it "should return an issue note by id" do
+ it "returns an issue note by id" do
get api("/projects/#{project.id}/issues/#{issue.id}/notes/#{issue_note.id}", user)
expect(response).to have_http_status(200)
expect(json_response['body']).to eq(issue_note.note)
end
- it "should return a 404 error if issue note not found" do
+ it "returns a 404 error if issue note not found" do
get api("/projects/#{project.id}/issues/#{issue.id}/notes/12345", user)
expect(response).to have_http_status(404)
end
context "and current user cannot view the note" do
- it "should return a 404 error" do
+ it "returns a 404 error" do
get api("/projects/#{ext_proj.id}/issues/#{ext_issue.id}/notes/#{cross_reference_note.id}", user)
expect(response).to have_http_status(404)
@@ -160,7 +160,7 @@ describe API::API, api: true do
end
context "and current user can view the note" do
- it "should return an issue note by id" do
+ it "returns 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).to have_http_status(200)
@@ -171,14 +171,14 @@ describe API::API, api: true do
end
context "when noteable is a Snippet" do
- it "should return a snippet note by id" do
+ it "returns a snippet note by id" do
get api("/projects/#{project.id}/snippets/#{snippet.id}/notes/#{snippet_note.id}", user)
expect(response).to have_http_status(200)
expect(json_response['body']).to eq(snippet_note.note)
end
- it "should return a 404 error if snippet note not found" do
+ it "returns a 404 error if snippet note not found" do
get api("/projects/#{project.id}/snippets/#{snippet.id}/notes/12345", user)
expect(response).to have_http_status(404)
@@ -188,7 +188,7 @@ describe API::API, api: true do
describe "POST /projects/:id/noteable/:noteable_id/notes" do
context "when noteable is an Issue" do
- it "should create a new issue note" do
+ it "creates a new issue note" do
post api("/projects/#{project.id}/issues/#{issue.id}/notes", user), body: 'hi!'
expect(response).to have_http_status(201)
@@ -196,13 +196,13 @@ describe API::API, api: true do
expect(json_response['author']['username']).to eq(user.username)
end
- it "should return a 400 bad request error if body not given" do
+ it "returns a 400 bad request error if body not given" do
post api("/projects/#{project.id}/issues/#{issue.id}/notes", user)
expect(response).to have_http_status(400)
end
- it "should return a 401 unauthorized error if user not authenticated" do
+ it "returns a 401 unauthorized error if user not authenticated" do
post api("/projects/#{project.id}/issues/#{issue.id}/notes"), body: 'hi!'
expect(response).to have_http_status(401)
@@ -223,7 +223,7 @@ describe API::API, api: true do
end
context "when noteable is a Snippet" do
- it "should create a new snippet note" do
+ it "creates a new snippet note" do
post api("/projects/#{project.id}/snippets/#{snippet.id}/notes", user), body: 'hi!'
expect(response).to have_http_status(201)
@@ -231,13 +231,13 @@ describe API::API, api: true do
expect(json_response['author']['username']).to eq(user.username)
end
- it "should return a 400 bad request error if body not given" do
+ it "returns a 400 bad request error if body not given" do
post api("/projects/#{project.id}/snippets/#{snippet.id}/notes", user)
expect(response).to have_http_status(400)
end
- it "should return a 401 unauthorized error if user not authenticated" do
+ it "returns a 401 unauthorized error if user not authenticated" do
post api("/projects/#{project.id}/snippets/#{snippet.id}/notes"), body: 'hi!'
expect(response).to have_http_status(401)
@@ -267,7 +267,7 @@ describe API::API, api: true do
end
describe "POST /projects/:id/noteable/:noteable_id/notes to test observer on create" do
- it "should create an activity event when an issue note is created" do
+ it "creates an activity event when an issue note is created" do
expect(Event).to receive(:create)
post api("/projects/#{project.id}/issues/#{issue.id}/notes", user), body: 'hi!'
@@ -276,7 +276,7 @@ describe API::API, api: true do
describe 'PUT /projects/:id/noteable/:noteable_id/notes/:note_id' do
context 'when noteable is an Issue' do
- it 'should return modified note' do
+ it 'returns modified note' do
put api("/projects/#{project.id}/issues/#{issue.id}/"\
"notes/#{issue_note.id}", user), body: 'Hello!'
@@ -284,14 +284,14 @@ describe API::API, api: true do
expect(json_response['body']).to eq('Hello!')
end
- it 'should return a 404 error when note id not found' do
+ it 'returns a 404 error when note id not found' do
put api("/projects/#{project.id}/issues/#{issue.id}/notes/12345", user),
body: 'Hello!'
expect(response).to have_http_status(404)
end
- it 'should return a 400 bad request error if body not given' do
+ it 'returns a 400 bad request error if body not given' do
put api("/projects/#{project.id}/issues/#{issue.id}/"\
"notes/#{issue_note.id}", user)
@@ -300,7 +300,7 @@ describe API::API, api: true do
end
context 'when noteable is a Snippet' do
- it 'should return modified note' do
+ it 'returns modified note' do
put api("/projects/#{project.id}/snippets/#{snippet.id}/"\
"notes/#{snippet_note.id}", user), body: 'Hello!'
@@ -308,7 +308,7 @@ describe API::API, api: true do
expect(json_response['body']).to eq('Hello!')
end
- it 'should return a 404 error when note id not found' do
+ it 'returns a 404 error when note id not found' do
put api("/projects/#{project.id}/snippets/#{snippet.id}/"\
"notes/12345", user), body: "Hello!"
@@ -317,7 +317,7 @@ describe API::API, api: true do
end
context 'when noteable is a Merge Request' do
- it 'should return modified note' do
+ it 'returns modified note' do
put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/"\
"notes/#{merge_request_note.id}", user), body: 'Hello!'
@@ -325,7 +325,7 @@ describe API::API, api: true do
expect(json_response['body']).to eq('Hello!')
end
- it 'should return a 404 error when note id not found' do
+ it 'returns a 404 error when note id not found' do
put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/"\
"notes/12345", user), body: "Hello!"
diff --git a/spec/requests/api/oauth_tokens_spec.rb b/spec/requests/api/oauth_tokens_spec.rb
new file mode 100644
index 00000000000..7e2cc50e591
--- /dev/null
+++ b/spec/requests/api/oauth_tokens_spec.rb
@@ -0,0 +1,33 @@
+require 'spec_helper'
+
+describe API::API, api: true do
+ include ApiHelpers
+
+ context 'Resource Owner Password Credentials' do
+ def request_oauth_token(user)
+ post '/oauth/token', username: user.username, password: user.password, grant_type: 'password'
+ end
+
+ context 'when user has 2FA enabled' do
+ it 'does not create an access token' do
+ user = create(:user, :two_factor)
+
+ request_oauth_token(user)
+
+ expect(response).to have_http_status(401)
+ expect(json_response['error']).to eq('invalid_grant')
+ end
+ end
+
+ context 'when user does not have 2FA enabled' do
+ it 'creates an access token' do
+ user = create(:user)
+
+ request_oauth_token(user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['access_token']).not_to be_nil
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/pipelines_spec.rb b/spec/requests/api/pipelines_spec.rb
new file mode 100644
index 00000000000..7011bdc9ec0
--- /dev/null
+++ b/spec/requests/api/pipelines_spec.rb
@@ -0,0 +1,133 @@
+require 'spec_helper'
+
+describe API::API, api: true do
+ include ApiHelpers
+
+ let(:user) { create(:user) }
+ let(:non_member) { create(:user) }
+ let(:project) { create(:project, creator_id: user.id) }
+
+ let!(:pipeline) do
+ create(:ci_empty_pipeline, project: project, sha: project.commit.id,
+ ref: project.default_branch)
+ end
+
+ before { project.team << [user, :master] }
+
+ describe 'GET /projects/:id/pipelines ' do
+ it_behaves_like 'a paginated resources' do
+ let(:request) { get api("/projects/#{project.id}/pipelines", user) }
+ end
+
+ context 'authorized user' do
+ it 'returns project pipelines' do
+ get api("/projects/#{project.id}/pipelines", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.first['sha']).to match /\A\h{40}\z/
+ expect(json_response.first['id']).to eq pipeline.id
+ end
+ end
+
+ context 'unauthorized user' do
+ it 'does not return project pipelines' do
+ get api("/projects/#{project.id}/pipelines", non_member)
+
+ expect(response).to have_http_status(404)
+ expect(json_response['message']).to eq '404 Project Not Found'
+ expect(json_response).not_to be_an Array
+ end
+ end
+ end
+
+ describe 'GET /projects/:id/pipelines/:pipeline_id' do
+ context 'authorized user' do
+ it 'returns project pipelines' do
+ get api("/projects/#{project.id}/pipelines/#{pipeline.id}", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['sha']).to match /\A\h{40}\z/
+ end
+
+ it 'returns 404 when it does not exist' do
+ get api("/projects/#{project.id}/pipelines/123456", user)
+
+ expect(response).to have_http_status(404)
+ expect(json_response['message']).to eq '404 Not found'
+ expect(json_response['id']).to be nil
+ end
+ end
+
+ context 'unauthorized user' do
+ it 'should not return a project pipeline' do
+ get api("/projects/#{project.id}/pipelines/#{pipeline.id}", non_member)
+
+ expect(response).to have_http_status(404)
+ expect(json_response['message']).to eq '404 Project Not Found'
+ expect(json_response['id']).to be nil
+ end
+ end
+ end
+
+ describe 'POST /projects/:id/pipelines/:pipeline_id/retry' do
+ context 'authorized user' do
+ let!(:pipeline) do
+ create(:ci_pipeline, project: project, sha: project.commit.id,
+ ref: project.default_branch)
+ end
+
+ let!(:build) { create(:ci_build, :failed, pipeline: pipeline) }
+
+ it 'retries failed builds' do
+ expect do
+ post api("/projects/#{project.id}/pipelines/#{pipeline.id}/retry", user)
+ end.to change { pipeline.builds.count }.from(1).to(2)
+
+ expect(response).to have_http_status(201)
+ expect(build.reload.retried?).to be true
+ end
+ end
+
+ context 'unauthorized user' do
+ it 'should not return a project pipeline' do
+ post api("/projects/#{project.id}/pipelines/#{pipeline.id}/retry", non_member)
+
+ expect(response).to have_http_status(404)
+ expect(json_response['message']).to eq '404 Project Not Found'
+ expect(json_response['id']).to be nil
+ end
+ end
+ end
+
+ describe 'POST /projects/:id/pipelines/:pipeline_id/cancel' do
+ let!(:pipeline) do
+ create(:ci_empty_pipeline, project: project, sha: project.commit.id,
+ ref: project.default_branch)
+ end
+
+ let!(:build) { create(:ci_build, :running, pipeline: pipeline) }
+
+ context 'authorized user' do
+ it 'retries failed builds' do
+ post api("/projects/#{project.id}/pipelines/#{pipeline.id}/cancel", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['status']).to eq('canceled')
+ end
+ end
+
+ context 'user without proper access rights' do
+ let!(:reporter) { create(:user) }
+
+ before { project.team << [reporter, :reporter] }
+
+ it 'rejects the action' do
+ post api("/projects/#{project.id}/pipelines/#{pipeline.id}/cancel", reporter)
+
+ expect(response).to have_http_status(403)
+ expect(pipeline.reload.status).to eq('pending')
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/project_hooks_spec.rb b/spec/requests/api/project_hooks_spec.rb
index fd1fffa6223..765dc8a8f66 100644
--- a/spec/requests/api/project_hooks_spec.rb
+++ b/spec/requests/api/project_hooks_spec.rb
@@ -7,9 +7,9 @@ describe API::API, 'ProjectHooks', api: true do
let!(:project) { create(:project, creator_id: user.id, namespace: user.namespace) }
let!(:hook) do
create(:project_hook,
- project: project, url: "http://example.com",
- push_events: true, merge_requests_events: true, tag_push_events: true,
- issues_events: true, note_events: true, build_events: true,
+ :all_events_enabled,
+ project: project,
+ url: 'http://example.com',
enable_ssl_verification: true)
end
@@ -20,7 +20,7 @@ describe API::API, 'ProjectHooks', api: true do
describe "GET /projects/:id/hooks" do
context "authorized user" do
- it "should return project hooks" do
+ it "returns project hooks" do
get api("/projects/#{project.id}/hooks", user)
expect(response).to have_http_status(200)
@@ -33,12 +33,14 @@ describe API::API, 'ProjectHooks', api: true do
expect(json_response.first['tag_push_events']).to eq(true)
expect(json_response.first['note_events']).to eq(true)
expect(json_response.first['build_events']).to eq(true)
+ expect(json_response.first['pipeline_events']).to eq(true)
+ expect(json_response.first['wiki_page_events']).to eq(true)
expect(json_response.first['enable_ssl_verification']).to eq(true)
end
end
context "unauthorized user" do
- it "should not access project hooks" do
+ it "does not access project hooks" do
get api("/projects/#{project.id}/hooks", user3)
expect(response).to have_http_status(403)
end
@@ -47,7 +49,7 @@ describe API::API, 'ProjectHooks', api: true do
describe "GET /projects/:id/hooks/:hook_id" do
context "authorized user" do
- it "should return a project hook" do
+ it "returns a project hook" do
get api("/projects/#{project.id}/hooks/#{hook.id}", user)
expect(response).to have_http_status(200)
expect(json_response['url']).to eq(hook.url)
@@ -56,30 +58,33 @@ describe API::API, 'ProjectHooks', api: true do
expect(json_response['merge_requests_events']).to eq(hook.merge_requests_events)
expect(json_response['tag_push_events']).to eq(hook.tag_push_events)
expect(json_response['note_events']).to eq(hook.note_events)
+ expect(json_response['build_events']).to eq(hook.build_events)
+ expect(json_response['pipeline_events']).to eq(hook.pipeline_events)
+ expect(json_response['wiki_page_events']).to eq(hook.wiki_page_events)
expect(json_response['enable_ssl_verification']).to eq(hook.enable_ssl_verification)
end
- it "should return a 404 error if hook id is not available" do
+ it "returns a 404 error if hook id is not available" do
get api("/projects/#{project.id}/hooks/1234", user)
expect(response).to have_http_status(404)
end
end
context "unauthorized user" do
- it "should not access an existing hook" do
+ it "does not access an existing hook" do
get api("/projects/#{project.id}/hooks/#{hook.id}", user3)
expect(response).to have_http_status(403)
end
end
- it "should return a 404 error if hook id is not available" do
+ it "returns a 404 error if hook id is not available" do
get api("/projects/#{project.id}/hooks/1234", user)
expect(response).to have_http_status(404)
end
end
describe "POST /projects/:id/hooks" do
- it "should add hook to project" do
+ it "adds hook to project" do
expect do
post api("/projects/#{project.id}/hooks", user), url: "http://example.com", issues_events: true
end.to change {project.hooks.count}.by(1)
@@ -91,22 +96,24 @@ describe API::API, 'ProjectHooks', api: true do
expect(json_response['tag_push_events']).to eq(false)
expect(json_response['note_events']).to eq(false)
expect(json_response['build_events']).to eq(false)
+ expect(json_response['pipeline_events']).to eq(false)
+ expect(json_response['wiki_page_events']).to eq(false)
expect(json_response['enable_ssl_verification']).to eq(true)
end
- it "should return a 400 error if url not given" do
+ it "returns a 400 error if url not given" do
post api("/projects/#{project.id}/hooks", user)
expect(response).to have_http_status(400)
end
- it "should return a 422 error if url not valid" do
+ it "returns a 422 error if url not valid" do
post api("/projects/#{project.id}/hooks", user), "url" => "ftp://example.com"
expect(response).to have_http_status(422)
end
end
describe "PUT /projects/:id/hooks/:hook_id" do
- it "should update an existing project hook" do
+ it "updates an existing project hook" do
put api("/projects/#{project.id}/hooks/#{hook.id}", user),
url: 'http://example.org', push_events: false
expect(response).to have_http_status(200)
@@ -116,49 +123,52 @@ describe API::API, 'ProjectHooks', api: true do
expect(json_response['merge_requests_events']).to eq(hook.merge_requests_events)
expect(json_response['tag_push_events']).to eq(hook.tag_push_events)
expect(json_response['note_events']).to eq(hook.note_events)
+ expect(json_response['build_events']).to eq(hook.build_events)
+ expect(json_response['pipeline_events']).to eq(hook.pipeline_events)
+ expect(json_response['wiki_page_events']).to eq(hook.wiki_page_events)
expect(json_response['enable_ssl_verification']).to eq(hook.enable_ssl_verification)
end
- it "should return 404 error if hook id not found" do
+ it "returns 404 error if hook id not found" do
put api("/projects/#{project.id}/hooks/1234", user), url: 'http://example.org'
expect(response).to have_http_status(404)
end
- it "should return 400 error if url is not given" do
+ it "returns 400 error if url is not given" do
put api("/projects/#{project.id}/hooks/#{hook.id}", user)
expect(response).to have_http_status(400)
end
- it "should return a 422 error if url is not valid" do
+ it "returns a 422 error if url is not valid" do
put api("/projects/#{project.id}/hooks/#{hook.id}", user), url: 'ftp://example.com'
expect(response).to have_http_status(422)
end
end
describe "DELETE /projects/:id/hooks/:hook_id" do
- it "should delete hook from project" do
+ it "deletes hook from project" do
expect do
delete api("/projects/#{project.id}/hooks/#{hook.id}", user)
end.to change {project.hooks.count}.by(-1)
expect(response).to have_http_status(200)
end
- it "should return success when deleting hook" do
+ it "returns success when deleting hook" do
delete api("/projects/#{project.id}/hooks/#{hook.id}", user)
expect(response).to have_http_status(200)
end
- it "should return a 404 error when deleting non existent hook" do
+ it "returns a 404 error when deleting non existent hook" do
delete api("/projects/#{project.id}/hooks/42", user)
expect(response).to have_http_status(404)
end
- it "should return a 405 error if hook id not given" do
+ it "returns a 405 error if hook id not given" do
delete api("/projects/#{project.id}/hooks", user)
expect(response).to have_http_status(405)
end
- it "shold return a 404 if a user attempts to delete project hooks he/she does not own" do
+ it "returns a 404 if a user attempts to delete project hooks he/she does not own" do
test_user = create(:user)
other_project = create(:project)
other_project.team << [test_user, :master]
diff --git a/spec/requests/api/project_members_spec.rb b/spec/requests/api/project_members_spec.rb
deleted file mode 100644
index 9a7c1da4401..00000000000
--- a/spec/requests/api/project_members_spec.rb
+++ /dev/null
@@ -1,166 +0,0 @@
-require 'spec_helper'
-
-describe API::API, api: true do
- include ApiHelpers
- let(:user) { create(:user) }
- let(:user2) { create(:user) }
- let(:user3) { create(:user) }
- let(:project) { create(:project, creator_id: user.id, namespace: user.namespace) }
- let(:project_member) { create(:project_member, :master, user: user, project: project) }
- let(:project_member2) { create(:project_member, :developer, user: user3, project: project) }
-
- describe "GET /projects/:id/members" do
- before { project_member }
- before { project_member2 }
-
- it "should return project team members" do
- get api("/projects/#{project.id}/members", user)
- expect(response).to have_http_status(200)
- expect(json_response).to be_an Array
- expect(json_response.count).to eq(2)
- expect(json_response.map { |u| u['username'] }).to include user.username
- end
-
- it "finds team members with query string" do
- get api("/projects/#{project.id}/members", user), query: user.username
- expect(response).to have_http_status(200)
- expect(json_response).to be_an Array
- expect(json_response.count).to eq(1)
- expect(json_response.first['username']).to eq(user.username)
- end
-
- it "should return a 404 error if id not found" do
- get api("/projects/9999/members", user)
- expect(response).to have_http_status(404)
- end
- end
-
- describe "GET /projects/:id/members/:user_id" do
- before { project_member }
-
- it "should return project team member" do
- get api("/projects/#{project.id}/members/#{user.id}", user)
- expect(response).to have_http_status(200)
- expect(json_response['username']).to eq(user.username)
- expect(json_response['access_level']).to eq(ProjectMember::MASTER)
- end
-
- it "should return a 404 error if user id not found" do
- get api("/projects/#{project.id}/members/1234", user)
- expect(response).to have_http_status(404)
- end
- end
-
- describe "POST /projects/:id/members" do
- it "should add user to project team" do
- expect do
- post api("/projects/#{project.id}/members", user), user_id: user2.id, access_level: ProjectMember::DEVELOPER
- end.to change { ProjectMember.count }.by(1)
-
- expect(response).to have_http_status(201)
- expect(json_response['username']).to eq(user2.username)
- expect(json_response['access_level']).to eq(ProjectMember::DEVELOPER)
- end
-
- it "should return a 201 status if user is already project member" do
- post api("/projects/#{project.id}/members", user),
- user_id: user2.id,
- access_level: ProjectMember::DEVELOPER
- expect do
- post api("/projects/#{project.id}/members", user), user_id: user2.id, access_level: ProjectMember::DEVELOPER
- end.not_to change { ProjectMember.count }
-
- expect(response).to have_http_status(201)
- expect(json_response['username']).to eq(user2.username)
- expect(json_response['access_level']).to eq(ProjectMember::DEVELOPER)
- end
-
- it "should return a 400 error when user id is not given" do
- post api("/projects/#{project.id}/members", user), access_level: ProjectMember::MASTER
- expect(response).to have_http_status(400)
- end
-
- it "should return a 400 error when access level is not given" do
- post api("/projects/#{project.id}/members", user), user_id: user2.id
- expect(response).to have_http_status(400)
- end
-
- it "should return a 422 error when access level is not known" do
- post api("/projects/#{project.id}/members", user), user_id: user2.id, access_level: 1234
- expect(response).to have_http_status(422)
- end
- end
-
- describe "PUT /projects/:id/members/:user_id" do
- before { project_member2 }
-
- it "should update project team member" do
- put api("/projects/#{project.id}/members/#{user3.id}", user), access_level: ProjectMember::MASTER
- expect(response).to have_http_status(200)
- expect(json_response['username']).to eq(user3.username)
- expect(json_response['access_level']).to eq(ProjectMember::MASTER)
- end
-
- it "should return a 404 error if user_id is not found" do
- put api("/projects/#{project.id}/members/1234", user), access_level: ProjectMember::MASTER
- expect(response).to have_http_status(404)
- end
-
- it "should return a 400 error when access level is not given" do
- put api("/projects/#{project.id}/members/#{user3.id}", user)
- expect(response).to have_http_status(400)
- end
-
- it "should return a 422 error when access level is not known" do
- put api("/projects/#{project.id}/members/#{user3.id}", user), access_level: 123
- expect(response).to have_http_status(422)
- end
- end
-
- describe "DELETE /projects/:id/members/:user_id" do
- before do
- project_member
- project_member2
- end
-
- it "should remove user from project team" do
- expect do
- delete api("/projects/#{project.id}/members/#{user3.id}", user)
- end.to change { ProjectMember.count }.by(-1)
- end
-
- it "should return 200 if team member is not part of a project" do
- delete api("/projects/#{project.id}/members/#{user3.id}", user)
- expect do
- delete api("/projects/#{project.id}/members/#{user3.id}", user)
- end.not_to change { ProjectMember.count }
- expect(response).to have_http_status(200)
- end
-
- it "should return 200 if team member already removed" do
- delete api("/projects/#{project.id}/members/#{user3.id}", user)
- delete api("/projects/#{project.id}/members/#{user3.id}", user)
- expect(response).to have_http_status(200)
- end
-
- it "should return 200 OK when the user was not member" do
- expect do
- delete api("/projects/#{project.id}/members/1000000", user)
- end.to change { ProjectMember.count }.by(0)
- expect(response).to have_http_status(200)
- expect(json_response['id']).to eq(1000000)
- expect(json_response['message']).to eq('Access revoked')
- end
-
- context 'when the user is not an admin or owner' do
- it 'can leave the project' do
- expect do
- delete api("/projects/#{project.id}/members/#{user3.id}", user3)
- end.to change { ProjectMember.count }.by(-1)
-
- expect(response).to have_http_status(200)
- expect(json_response['id']).to eq(project_member2.id)
- end
- end
- end
-end
diff --git a/spec/requests/api/project_snippets_spec.rb b/spec/requests/api/project_snippets_spec.rb
index 4ebde201941..01148f0a05e 100644
--- a/spec/requests/api/project_snippets_spec.rb
+++ b/spec/requests/api/project_snippets_spec.rb
@@ -17,7 +17,7 @@ describe API::API, api: true do
end
describe 'GET /projects/:project_id/snippets/' do
- it 'all snippets available to team member' do
+ it 'returns all snippets available to team member' do
project = create(:project, :public)
user = create(:user)
project.team << [user, :developer]
@@ -30,6 +30,7 @@ describe API::API, api: true do
expect(response).to have_http_status(200)
expect(json_response.size).to eq(3)
expect(json_response.map{ |snippet| snippet['id']} ).to include(public_snippet.id, internal_snippet.id, private_snippet.id)
+ expect(json_response.last).to have_key('web_url')
end
it 'hides private snippets from regular user' do
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index 8a52725a893..63f2467be63 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -43,14 +43,14 @@ describe API::API, api: true do
before { project }
context 'when unauthenticated' do
- it 'should return authentication error' do
+ it 'returns authentication error' do
get api('/projects')
expect(response).to have_http_status(401)
end
end
context 'when authenticated' do
- it 'should return an array of projects' do
+ it 'returns an array of projects' do
get api('/projects', user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
@@ -58,21 +58,21 @@ describe API::API, api: true do
expect(json_response.first['owner']['username']).to eq(user.username)
end
- it 'should include the project labels as the tag_list' do
+ it 'includes the project labels as the tag_list' do
get api('/projects', user)
expect(response.status).to eq 200
expect(json_response).to be_an Array
expect(json_response.first.keys).to include('tag_list')
end
- it 'should include open_issues_count' do
+ it 'includes open_issues_count' do
get api('/projects', user)
expect(response.status).to eq 200
expect(json_response).to be_an Array
expect(json_response.first.keys).to include('open_issues_count')
end
- it 'should not include open_issues_count' do
+ it 'does not include open_issues_count' do
project.update_attributes( { issues_enabled: false } )
get api('/projects', user)
@@ -81,8 +81,20 @@ describe API::API, api: true do
expect(json_response.first.keys).not_to include('open_issues_count')
end
+ context 'GET /projects?simple=true' do
+ it 'returns a simplified version of all the projects' do
+ expected_keys = ["id", "http_url_to_repo", "web_url", "name", "name_with_namespace", "path", "path_with_namespace"]
+
+ get api('/projects?simple=true', user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.first.keys).to match_array expected_keys
+ end
+ end
+
context 'and using search' do
- it 'should return searched project' do
+ it 'returns searched project' do
get api('/projects', user), { search: project.name }
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
@@ -91,21 +103,21 @@ describe API::API, api: true do
end
context 'and using the visibility filter' do
- it 'should filter based on private visibility param' do
+ it 'filters based on private visibility param' do
get api('/projects', user), { visibility: 'private' }
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(user.namespace.projects.where(visibility_level: Gitlab::VisibilityLevel::PRIVATE).count)
end
- it 'should filter based on internal visibility param' do
+ it 'filters based on internal visibility param' do
get api('/projects', user), { visibility: 'internal' }
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(user.namespace.projects.where(visibility_level: Gitlab::VisibilityLevel::INTERNAL).count)
end
- it 'should filter based on public visibility param' do
+ it 'filters based on public visibility param' do
get api('/projects', user), { visibility: 'public' }
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
@@ -119,7 +131,7 @@ describe API::API, api: true do
project3
end
- it 'should return the correct order when sorted by id' do
+ it 'returns the correct order when sorted by id' do
get api('/projects', user), { order_by: 'id', sort: 'desc' }
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
@@ -133,21 +145,21 @@ describe API::API, api: true do
before { project }
context 'when unauthenticated' do
- it 'should return authentication error' do
+ it 'returns authentication error' do
get api('/projects/all')
expect(response).to have_http_status(401)
end
end
context 'when authenticated as regular user' do
- it 'should return authentication error' do
+ it 'returns authentication error' do
get api('/projects/all', user)
expect(response).to have_http_status(403)
end
end
context 'when authenticated as admin' do
- it 'should return an array of all projects' do
+ it 'returns an array of all projects' do
get api('/projects/all', admin)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
@@ -171,7 +183,7 @@ describe API::API, api: true do
user3.update_attributes(starred_projects: [project, project2, project3, public_project])
end
- it 'should return the starred projects viewable by the user' do
+ it 'returns the starred projects viewable by the user' do
get api('/projects/starred', user3)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
@@ -181,7 +193,7 @@ describe API::API, api: true do
describe 'POST /projects' do
context 'maximum number of projects reached' do
- it 'should not create new project and respond with 403' do
+ it 'does not create new project and respond with 403' do
allow_any_instance_of(User).to receive(:projects_limit_left).and_return(0)
expect { post api('/projects', user2), name: 'foo' }.
to change {Project.count}.by(0)
@@ -189,30 +201,31 @@ describe API::API, api: true do
end
end
- it 'should create new project without path and return 201' do
+ it 'creates new project without path and return 201' do
expect { post api('/projects', user), name: 'foo' }.
to change { Project.count }.by(1)
expect(response).to have_http_status(201)
end
- it 'should create last project before reaching project limit' do
+ it 'creates last project before reaching project limit' do
allow_any_instance_of(User).to receive(:projects_limit_left).and_return(1)
post api('/projects', user2), name: 'foo'
expect(response).to have_http_status(201)
end
- it 'should not create new project without name and return 400' do
+ it 'does not create new project without name and return 400' do
expect { post api('/projects', user) }.not_to change { Project.count }
expect(response).to have_http_status(400)
end
- it "should assign attributes to project" do
+ it "assigns attributes to project" do
project = attributes_for(:project, {
path: 'camelCasePath',
description: FFaker::Lorem.sentence,
issues_enabled: false,
merge_requests_enabled: false,
- wiki_enabled: false
+ wiki_enabled: false,
+ only_allow_merge_if_build_succeeds: false
})
post api('/projects', user), project
@@ -222,55 +235,67 @@ describe API::API, api: true do
end
end
- it 'should set a project as public' do
+ it 'sets a project as public' do
project = attributes_for(:project, :public)
post api('/projects', user), project
expect(json_response['public']).to be_truthy
expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PUBLIC)
end
- it 'should set a project as public using :public' do
+ it 'sets a project as public using :public' do
project = attributes_for(:project, { public: true })
post api('/projects', user), project
expect(json_response['public']).to be_truthy
expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PUBLIC)
end
- it 'should set a project as internal' do
+ it 'sets a project as internal' do
project = attributes_for(:project, :internal)
post api('/projects', user), project
expect(json_response['public']).to be_falsey
expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::INTERNAL)
end
- it 'should set a project as internal overriding :public' do
+ it 'sets a project as internal overriding :public' do
project = attributes_for(:project, :internal, { public: true })
post api('/projects', user), project
expect(json_response['public']).to be_falsey
expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::INTERNAL)
end
- it 'should set a project as private' do
+ it 'sets a project as private' do
project = attributes_for(:project, :private)
post api('/projects', user), project
expect(json_response['public']).to be_falsey
expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PRIVATE)
end
- it 'should set a project as private using :public' do
+ it 'sets a project as private using :public' do
project = attributes_for(:project, { public: false })
post api('/projects', user), project
expect(json_response['public']).to be_falsey
expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PRIVATE)
end
+ it 'sets a project as allowing merge even if build fails' do
+ project = attributes_for(:project, { only_allow_merge_if_build_succeeds: false })
+ post api('/projects', user), project
+ expect(json_response['only_allow_merge_if_build_succeeds']).to be_falsey
+ end
+
+ it 'sets a project as allowing merge only if build succeeds' do
+ project = attributes_for(:project, { only_allow_merge_if_build_succeeds: true })
+ post api('/projects', user), project
+ expect(json_response['only_allow_merge_if_build_succeeds']).to be_truthy
+ end
+
context 'when a visibility level is restricted' do
before do
@project = attributes_for(:project, { public: true })
stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC])
end
- it 'should not allow a non-admin to use a restricted visibility level' do
+ it 'does not allow a non-admin to use a restricted visibility level' do
post api('/projects', user), @project
expect(response).to have_http_status(400)
@@ -279,7 +304,7 @@ describe API::API, api: true do
)
end
- it 'should allow an admin to override restricted visibility settings' do
+ it 'allows an admin to override restricted visibility settings' do
post api('/projects', admin), @project
expect(json_response['public']).to be_truthy
expect(json_response['visibility_level']).to(
@@ -298,7 +323,7 @@ describe API::API, api: true do
expect(response).to have_http_status(201)
end
- it 'should respond with 400 on failure and not project' do
+ it 'responds with 400 on failure and not project' do
expect { post api("/projects/user/#{user.id}", admin) }.
not_to change { Project.count }
@@ -315,7 +340,7 @@ describe API::API, api: true do
])
end
- it 'should assign attributes to project' do
+ it 'assigns attributes to project' do
project = attributes_for(:project, {
description: FFaker::Lorem.sentence,
issues_enabled: false,
@@ -331,47 +356,59 @@ describe API::API, api: true do
end
end
- it 'should set a project as public' do
+ it 'sets a project as public' do
project = attributes_for(:project, :public)
post api("/projects/user/#{user.id}", admin), project
expect(json_response['public']).to be_truthy
expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PUBLIC)
end
- it 'should set a project as public using :public' do
+ it 'sets a project as public using :public' do
project = attributes_for(:project, { public: true })
post api("/projects/user/#{user.id}", admin), project
expect(json_response['public']).to be_truthy
expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PUBLIC)
end
- it 'should set a project as internal' do
+ it 'sets a project as internal' do
project = attributes_for(:project, :internal)
post api("/projects/user/#{user.id}", admin), project
expect(json_response['public']).to be_falsey
expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::INTERNAL)
end
- it 'should set a project as internal overriding :public' do
+ it 'sets a project as internal overriding :public' do
project = attributes_for(:project, :internal, { public: true })
post api("/projects/user/#{user.id}", admin), project
expect(json_response['public']).to be_falsey
expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::INTERNAL)
end
- it 'should set a project as private' do
+ it 'sets a project as private' do
project = attributes_for(:project, :private)
post api("/projects/user/#{user.id}", admin), project
expect(json_response['public']).to be_falsey
expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PRIVATE)
end
- it 'should set a project as private using :public' do
+ it 'sets a project as private using :public' do
project = attributes_for(:project, { public: false })
post api("/projects/user/#{user.id}", admin), project
expect(json_response['public']).to be_falsey
expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PRIVATE)
end
+
+ it 'sets a project as allowing merge even if build fails' do
+ project = attributes_for(:project, { only_allow_merge_if_build_succeeds: false })
+ post api("/projects/user/#{user.id}", admin), project
+ expect(json_response['only_allow_merge_if_build_succeeds']).to be_falsey
+ end
+
+ it 'sets a project as allowing merge only if build succeeds' do
+ project = attributes_for(:project, { only_allow_merge_if_build_succeeds: true })
+ post api("/projects/user/#{user.id}", admin), project
+ expect(json_response['only_allow_merge_if_build_succeeds']).to be_truthy
+ end
end
describe "POST /projects/:id/uploads" do
@@ -384,7 +421,6 @@ describe API::API, api: true do
expect(json_response['alt']).to eq("dk")
expect(json_response['url']).to start_with("/uploads/")
expect(json_response['url']).to end_with("/dk.png")
- expect(json_response['is_image']).to eq(true)
end
end
@@ -433,27 +469,28 @@ describe API::API, api: true do
expect(json_response['shared_with_groups'][0]['group_id']).to eq(group.id)
expect(json_response['shared_with_groups'][0]['group_name']).to eq(group.name)
expect(json_response['shared_with_groups'][0]['group_access_level']).to eq(link.group_access)
+ expect(json_response['only_allow_merge_if_build_succeeds']).to eq(project.only_allow_merge_if_build_succeeds)
end
- it 'should return a project by path name' do
+ it 'returns a project by path name' do
get api("/projects/#{project.id}", user)
expect(response).to have_http_status(200)
expect(json_response['name']).to eq(project.name)
end
- it 'should return a 404 error if not found' do
+ it 'returns a 404 error if not found' do
get api('/projects/42', user)
expect(response).to have_http_status(404)
expect(json_response['message']).to eq('404 Project Not Found')
end
- it 'should return a 404 error if user is not a member' do
+ it 'returns a 404 error if user is not a member' do
other_user = create(:user)
get api("/projects/#{project.id}", other_user)
expect(response).to have_http_status(404)
end
- it 'should handle users with dots' do
+ it 'handles users with dots' do
dot_user = create(:user, username: 'dot.user')
project = create(:project, creator_id: dot_user.id, namespace: dot_user.namespace)
@@ -493,7 +530,7 @@ describe API::API, api: true do
before { project2.group.add_owner(user) }
- it 'should set the owner and return 200' do
+ it 'sets the owner and return 200' do
get api("/projects/#{project2.id}", user)
expect(response).to have_http_status(200)
@@ -534,13 +571,13 @@ describe API::API, api: true do
end
end
- it 'should return a 404 error if not found' do
+ it 'returns a 404 error if not found' do
get api('/projects/42/events', user)
expect(response).to have_http_status(404)
expect(json_response['message']).to eq('404 Project Not Found')
end
- it 'should return a 404 error if user is not a member' do
+ it 'returns a 404 error if user is not a member' do
other_user = create(:user)
get api("/projects/#{project.id}/events", other_user)
expect(response).to have_http_status(404)
@@ -550,7 +587,7 @@ describe API::API, api: true do
describe 'GET /projects/:id/snippets' do
before { snippet }
- it 'should return an array of project snippets' do
+ it 'returns an array of project snippets' do
get api("/projects/#{project.id}/snippets", user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
@@ -559,20 +596,20 @@ describe API::API, api: true do
end
describe 'GET /projects/:id/snippets/:snippet_id' do
- it 'should return a project snippet' do
+ it 'returns a project snippet' do
get api("/projects/#{project.id}/snippets/#{snippet.id}", user)
expect(response).to have_http_status(200)
expect(json_response['title']).to eq(snippet.title)
end
- it 'should return a 404 error if snippet id not found' do
+ it 'returns a 404 error if snippet id not found' do
get api("/projects/#{project.id}/snippets/1234", user)
expect(response).to have_http_status(404)
end
end
describe 'POST /projects/:id/snippets' do
- it 'should create a new project snippet' do
+ it 'creates a new project snippet' do
post api("/projects/#{project.id}/snippets", user),
title: 'api test', file_name: 'sample.rb', code: 'test',
visibility_level: '0'
@@ -580,14 +617,14 @@ describe API::API, api: true do
expect(json_response['title']).to eq('api test')
end
- it 'should return a 400 error if invalid snippet is given' do
+ it 'returns a 400 error if invalid snippet is given' do
post api("/projects/#{project.id}/snippets", user)
expect(status).to eq(400)
end
end
describe 'PUT /projects/:id/snippets/:snippet_id' do
- it 'should update an existing project snippet' do
+ it 'updates an existing project snippet' do
put api("/projects/#{project.id}/snippets/#{snippet.id}", user),
code: 'updated code'
expect(response).to have_http_status(200)
@@ -595,7 +632,7 @@ describe API::API, api: true do
expect(snippet.reload.content).to eq('updated code')
end
- it 'should update an existing project snippet with new title' do
+ it 'updates an existing project snippet with new title' do
put api("/projects/#{project.id}/snippets/#{snippet.id}", user),
title: 'other api test'
expect(response).to have_http_status(200)
@@ -606,103 +643,31 @@ describe API::API, api: true do
describe 'DELETE /projects/:id/snippets/:snippet_id' do
before { snippet }
- it 'should delete existing project snippet' do
+ it 'deletes existing project snippet' do
expect do
delete api("/projects/#{project.id}/snippets/#{snippet.id}", user)
end.to change { Snippet.count }.by(-1)
expect(response).to have_http_status(200)
end
- it 'should return 404 when deleting unknown snippet id' do
+ it 'returns 404 when deleting unknown snippet id' do
delete api("/projects/#{project.id}/snippets/1234", user)
expect(response).to have_http_status(404)
end
end
describe 'GET /projects/:id/snippets/:snippet_id/raw' do
- it 'should get a raw project snippet' do
+ it 'gets a raw project snippet' do
get api("/projects/#{project.id}/snippets/#{snippet.id}/raw", user)
expect(response).to have_http_status(200)
end
- it 'should return a 404 error if raw project snippet not found' do
+ it 'returns a 404 error if raw project snippet not found' do
get api("/projects/#{project.id}/snippets/5555/raw", user)
expect(response).to have_http_status(404)
end
end
- describe :deploy_keys do
- let(:deploy_keys_project) { create(:deploy_keys_project, project: project) }
- let(:deploy_key) { deploy_keys_project.deploy_key }
-
- describe 'GET /projects/:id/keys' do
- before { deploy_key }
-
- it 'should return array of ssh keys' do
- get api("/projects/#{project.id}/keys", user)
- expect(response).to have_http_status(200)
- expect(json_response).to be_an Array
- expect(json_response.first['title']).to eq(deploy_key.title)
- end
- end
-
- describe 'GET /projects/:id/keys/:key_id' do
- it 'should return a single key' do
- get api("/projects/#{project.id}/keys/#{deploy_key.id}", user)
- expect(response).to have_http_status(200)
- expect(json_response['title']).to eq(deploy_key.title)
- end
-
- it 'should return 404 Not Found with invalid ID' do
- get api("/projects/#{project.id}/keys/404", user)
- expect(response).to have_http_status(404)
- end
- end
-
- describe 'POST /projects/:id/keys' do
- it 'should not create an invalid ssh key' do
- post api("/projects/#{project.id}/keys", user), { title: 'invalid key' }
- expect(response).to have_http_status(400)
- expect(json_response['message']['key']).to eq([
- 'can\'t be blank',
- 'is too short (minimum is 0 characters)',
- 'is invalid'
- ])
- end
-
- it 'should not create a key without title' do
- post api("/projects/#{project.id}/keys", user), key: 'some key'
- expect(response).to have_http_status(400)
- expect(json_response['message']['title']).to eq([
- 'can\'t be blank',
- 'is too short (minimum is 0 characters)'
- ])
- end
-
- it 'should create new ssh key' do
- key_attrs = attributes_for :key
- expect do
- post api("/projects/#{project.id}/keys", user), key_attrs
- end.to change{ project.deploy_keys.count }.by(1)
- end
- end
-
- describe 'DELETE /projects/:id/keys/:key_id' do
- before { deploy_key }
-
- it 'should delete existing key' do
- expect do
- delete api("/projects/#{project.id}/keys/#{deploy_key.id}", user)
- end.to change{ project.deploy_keys.count }.by(-1)
- end
-
- it 'should return 404 Not Found with invalid ID' do
- delete api("/projects/#{project.id}/keys/404", user)
- expect(response).to have_http_status(404)
- end
- end
- end
-
describe :fork_admin do
let(:project_fork_target) { create(:project) }
let(:project_fork_source) { create(:project, :public) }
@@ -710,12 +675,12 @@ describe API::API, api: true do
describe 'POST /projects/:id/fork/:forked_from_id' do
let(:new_project_fork_source) { create(:project, :public) }
- it "shouldn't available for non admin users" do
+ it "is not available for non admin users" do
post api("/projects/#{project_fork_target.id}/fork/#{project_fork_source.id}", user)
expect(response).to have_http_status(403)
end
- it 'should allow project to be forked from an existing project' do
+ it 'allows project to be forked from an existing project' do
expect(project_fork_target.forked?).not_to be_truthy
post api("/projects/#{project_fork_target.id}/fork/#{project_fork_source.id}", admin)
expect(response).to have_http_status(201)
@@ -725,12 +690,12 @@ describe API::API, api: true do
expect(project_fork_target.forked?).to be_truthy
end
- it 'should fail if forked_from project which does not exist' do
+ it 'fails if forked_from project which does not exist' do
post api("/projects/#{project_fork_target.id}/fork/9999", admin)
expect(response).to have_http_status(404)
end
- it 'should fail with 409 if already forked' do
+ it 'fails with 409 if already forked' do
post api("/projects/#{project_fork_target.id}/fork/#{project_fork_source.id}", admin)
project_fork_target.reload
expect(project_fork_target.forked_from_project.id).to eq(project_fork_source.id)
@@ -743,7 +708,7 @@ describe API::API, api: true do
end
describe 'DELETE /projects/:id/fork' do
- it "shouldn't be visible to users outside group" do
+ it "is not visible to users outside group" do
delete api("/projects/#{project_fork_target.id}/fork", user)
expect(response).to have_http_status(404)
end
@@ -756,12 +721,12 @@ describe API::API, api: true do
project_fork_target.group.add_developer user2
end
- it 'should be forbidden to non-owner users' do
+ it 'is forbidden to non-owner users' do
delete api("/projects/#{project_fork_target.id}/fork", user2)
expect(response).to have_http_status(403)
end
- it 'should make forked project unforked' do
+ it 'makes forked project unforked' do
post api("/projects/#{project_fork_target.id}/fork/#{project_fork_source.id}", admin)
project_fork_target.reload
expect(project_fork_target.forked_from_project).not_to be_nil
@@ -773,7 +738,7 @@ describe API::API, api: true do
expect(project_fork_target.forked?).not_to be_truthy
end
- it 'should be idempotent if not forked' do
+ it 'is idempotent if not forked' do
expect(project_fork_target.forked_from_project).to be_nil
delete api("/projects/#{project_fork_target.id}/fork", admin)
expect(response).to have_http_status(200)
@@ -786,7 +751,7 @@ describe API::API, api: true do
describe "POST /projects/:id/share" do
let(:group) { create(:group) }
- it "should share project with group" do
+ it "shares project with group" do
expect do
post api("/projects/#{project.id}/share", user), group_id: group.id, group_access: Gitlab::Access::DEVELOPER
end.to change { ProjectGroupLink.count }.by(1)
@@ -796,23 +761,23 @@ describe API::API, api: true do
expect(json_response['group_access']).to eq Gitlab::Access::DEVELOPER
end
- it "should return a 400 error when group id is not given" do
+ it "returns a 400 error when group id is not given" do
post api("/projects/#{project.id}/share", user), group_access: Gitlab::Access::DEVELOPER
expect(response.status).to eq 400
end
- it "should return a 400 error when access level is not given" do
+ it "returns a 400 error when access level is not given" do
post api("/projects/#{project.id}/share", user), group_id: group.id
expect(response.status).to eq 400
end
- it "should return a 400 error when sharing is disabled" do
+ it "returns a 400 error when sharing is disabled" do
project.namespace.update(share_with_group_lock: true)
post api("/projects/#{project.id}/share", user), group_id: group.id, group_access: Gitlab::Access::DEVELOPER
expect(response.status).to eq 400
end
- it "should return a 409 error when wrong params passed" do
+ it "returns a 409 error when wrong params passed" do
post api("/projects/#{project.id}/share", user), group_id: group.id, group_access: 1234
expect(response.status).to eq 409
expect(json_response['message']).to eq 'Group access is not included in the list'
@@ -832,14 +797,14 @@ describe API::API, api: true do
let!(:unfound_public) { create(:empty_project, :public, name: 'unfound public') }
context 'when unauthenticated' do
- it 'should return authentication error' do
+ it 'returns authentication error' do
get api("/projects/search/#{query}")
expect(response).to have_http_status(401)
end
end
context 'when authenticated' do
- it 'should return an array of projects' do
+ it 'returns an array of projects' do
get api("/projects/search/#{query}", user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
@@ -849,7 +814,7 @@ describe API::API, api: true do
end
context 'when authenticated as a different user' do
- it 'should return matching public projects' do
+ it 'returns matching public projects' do
get api("/projects/search/#{query}", user2)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
@@ -870,7 +835,7 @@ describe API::API, api: true do
before { project_member2 }
context 'when unauthenticated' do
- it 'should return authentication error' do
+ it 'returns authentication error' do
project_param = { name: 'bar' }
put api("/projects/#{project.id}"), project_param
expect(response).to have_http_status(401)
@@ -878,7 +843,7 @@ describe API::API, api: true do
end
context 'when authenticated as project owner' do
- it 'should update name' do
+ it 'updates name' do
project_param = { name: 'bar' }
put api("/projects/#{project.id}", user), project_param
expect(response).to have_http_status(200)
@@ -887,7 +852,7 @@ describe API::API, api: true do
end
end
- it 'should update visibility_level' do
+ it 'updates visibility_level' do
project_param = { visibility_level: 20 }
put api("/projects/#{project3.id}", user), project_param
expect(response).to have_http_status(200)
@@ -896,7 +861,7 @@ describe API::API, api: true do
end
end
- it 'should update visibility_level from public to private' do
+ it 'updates visibility_level from public to private' do
project3.update_attributes({ visibility_level: Gitlab::VisibilityLevel::PUBLIC })
project_param = { public: false }
@@ -908,14 +873,14 @@ describe API::API, api: true do
expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PRIVATE)
end
- it 'should not update name to existing name' do
+ it 'does not update name to existing name' do
project_param = { name: project3.name }
put api("/projects/#{project.id}", user), project_param
expect(response).to have_http_status(400)
expect(json_response['message']['name']).to eq(['has already been taken'])
end
- it 'should update path & name to existing path & name in different namespace' do
+ it 'updates path & name to existing path & name in different namespace' do
project_param = { path: project4.path, name: project4.name }
put api("/projects/#{project3.id}", user), project_param
expect(response).to have_http_status(200)
@@ -926,7 +891,7 @@ describe API::API, api: true do
end
context 'when authenticated as project master' do
- it 'should update path' do
+ it 'updates path' do
project_param = { path: 'bar' }
put api("/projects/#{project3.id}", user4), project_param
expect(response).to have_http_status(200)
@@ -935,7 +900,7 @@ describe API::API, api: true do
end
end
- it 'should update other attributes' do
+ it 'updates other attributes' do
project_param = { issues_enabled: true,
wiki_enabled: true,
snippets_enabled: true,
@@ -949,20 +914,20 @@ describe API::API, api: true do
end
end
- it 'should not update path to existing path' do
+ it 'does not update path to existing path' do
project_param = { path: project.path }
put api("/projects/#{project3.id}", user4), project_param
expect(response).to have_http_status(400)
expect(json_response['message']['path']).to eq(['has already been taken'])
end
- it 'should not update name' do
+ it 'does not update name' do
project_param = { name: 'bar' }
put api("/projects/#{project3.id}", user4), project_param
expect(response).to have_http_status(403)
end
- it 'should not update visibility_level' do
+ it 'does not update visibility_level' do
project_param = { visibility_level: 20 }
put api("/projects/#{project3.id}", user4), project_param
expect(response).to have_http_status(403)
@@ -970,7 +935,7 @@ describe API::API, api: true do
end
context 'when authenticated as project developer' do
- it 'should not update other attributes' do
+ it 'does not update other attributes' do
project_param = { path: 'bar',
issues_enabled: true,
wiki_enabled: true,
@@ -1105,36 +1070,36 @@ describe API::API, api: true do
describe 'DELETE /projects/:id' do
context 'when authenticated as user' do
- it 'should remove project' do
+ it 'removes project' do
delete api("/projects/#{project.id}", user)
expect(response).to have_http_status(200)
end
- it 'should not remove a project if not an owner' do
+ it 'does not remove a project if not an owner' do
user3 = create(:user)
project.team << [user3, :developer]
delete api("/projects/#{project.id}", user3)
expect(response).to have_http_status(403)
end
- it 'should not remove a non existing project' do
+ it 'does not remove a non existing project' do
delete api('/projects/1328', user)
expect(response).to have_http_status(404)
end
- it 'should not remove a project not attached to user' do
+ it 'does not remove a project not attached to user' do
delete api("/projects/#{project.id}", user2)
expect(response).to have_http_status(404)
end
end
context 'when authenticated as admin' do
- it 'should remove any existing project' do
+ it 'removes any existing project' do
delete api("/projects/#{project.id}", admin)
expect(response).to have_http_status(200)
end
- it 'should not remove a non existing project' do
+ it 'does not remove a non existing project' do
delete api('/projects/1328', admin)
expect(response).to have_http_status(404)
end
diff --git a/spec/requests/api/repositories_spec.rb b/spec/requests/api/repositories_spec.rb
index 5890e9c9d3d..80a856a6e90 100644
--- a/spec/requests/api/repositories_spec.rb
+++ b/spec/requests/api/repositories_spec.rb
@@ -16,7 +16,7 @@ describe API::API, api: true do
context "authorized user" do
before { project.team << [user2, :reporter] }
- it "should return project commits" do
+ it "returns project commits" do
get api("/projects/#{project.id}/repository/tree", user)
expect(response).to have_http_status(200)
@@ -26,7 +26,7 @@ describe API::API, api: true do
expect(json_response.first['mode']).to eq('040000')
end
- it 'should return a 404 for unknown ref' do
+ it 'returns a 404 for unknown ref' do
get api("/projects/#{project.id}/repository/tree?ref_name=foo", user)
expect(response).to have_http_status(404)
@@ -36,7 +36,7 @@ describe API::API, api: true do
end
context "unauthorized user" do
- it "should not return project commits" do
+ it "does not return project commits" do
get api("/projects/#{project.id}/repository/tree")
expect(response).to have_http_status(401)
end
@@ -44,41 +44,41 @@ describe API::API, api: true do
end
describe "GET /projects/:id/repository/blobs/:sha" do
- it "should get the raw file contents" do
+ it "gets the raw file contents" do
get api("/projects/#{project.id}/repository/blobs/master?filepath=README.md", user)
expect(response).to have_http_status(200)
end
- it "should return 404 for invalid branch_name" do
+ it "returns 404 for invalid branch_name" do
get api("/projects/#{project.id}/repository/blobs/invalid_branch_name?filepath=README.md", user)
expect(response).to have_http_status(404)
end
- it "should return 404 for invalid file" do
+ it "returns 404 for invalid file" do
get api("/projects/#{project.id}/repository/blobs/master?filepath=README.invalid", user)
expect(response).to have_http_status(404)
end
- it "should return a 400 error if filepath is missing" do
+ it "returns a 400 error if filepath is missing" do
get api("/projects/#{project.id}/repository/blobs/master", user)
expect(response).to have_http_status(400)
end
end
describe "GET /projects/:id/repository/commits/:sha/blob" do
- it "should get the raw file contents" do
+ it "gets the raw file contents" do
get api("/projects/#{project.id}/repository/commits/master/blob?filepath=README.md", user)
expect(response).to have_http_status(200)
end
end
describe "GET /projects/:id/repository/raw_blobs/:sha" do
- it "should get the raw file contents" do
+ it "gets the raw file contents" do
get api("/projects/#{project.id}/repository/raw_blobs/#{sample_blob.oid}", user)
expect(response).to have_http_status(200)
end
- it 'should return a 404 for unknown blob' do
+ it 'returns a 404 for unknown blob' do
get api("/projects/#{project.id}/repository/raw_blobs/123456", user)
expect(response).to have_http_status(404)
@@ -88,7 +88,7 @@ describe API::API, api: true do
end
describe "GET /projects/:id/repository/archive(.:format)?:sha" do
- it "should get the archive" do
+ it "gets the archive" do
get api("/projects/#{project.id}/repository/archive", user)
repo_name = project.repository.name.gsub("\.git", "")
expect(response).to have_http_status(200)
@@ -97,7 +97,7 @@ describe API::API, api: true do
expect(params['ArchivePath']).to match(/#{repo_name}\-[^\.]+\.tar.gz/)
end
- it "should get the archive.zip" do
+ it "gets the archive.zip" do
get api("/projects/#{project.id}/repository/archive.zip", user)
repo_name = project.repository.name.gsub("\.git", "")
expect(response).to have_http_status(200)
@@ -106,7 +106,7 @@ describe API::API, api: true do
expect(params['ArchivePath']).to match(/#{repo_name}\-[^\.]+\.zip/)
end
- it "should get the archive.tar.bz2" do
+ it "gets the archive.tar.bz2" do
get api("/projects/#{project.id}/repository/archive.tar.bz2", user)
repo_name = project.repository.name.gsub("\.git", "")
expect(response).to have_http_status(200)
@@ -115,28 +115,28 @@ describe API::API, api: true do
expect(params['ArchivePath']).to match(/#{repo_name}\-[^\.]+\.tar.bz2/)
end
- it "should return 404 for invalid sha" do
+ it "returns 404 for invalid sha" do
get api("/projects/#{project.id}/repository/archive/?sha=xxx", user)
expect(response).to have_http_status(404)
end
end
describe 'GET /projects/:id/repository/compare' do
- it "should compare branches" do
+ it "compares branches" do
get api("/projects/#{project.id}/repository/compare", user), from: 'master', to: 'feature'
expect(response).to have_http_status(200)
expect(json_response['commits']).to be_present
expect(json_response['diffs']).to be_present
end
- it "should compare tags" do
+ it "compares tags" do
get api("/projects/#{project.id}/repository/compare", user), from: 'v1.0.0', to: 'v1.1.0'
expect(response).to have_http_status(200)
expect(json_response['commits']).to be_present
expect(json_response['diffs']).to be_present
end
- it "should compare commits" do
+ it "compares commits" do
get api("/projects/#{project.id}/repository/compare", user), from: sample_commit.id, to: sample_commit.parent_id
expect(response).to have_http_status(200)
expect(json_response['commits']).to be_empty
@@ -144,14 +144,14 @@ describe API::API, api: true do
expect(json_response['compare_same_ref']).to be_falsey
end
- it "should compare commits in reverse order" do
+ it "compares commits in reverse order" do
get api("/projects/#{project.id}/repository/compare", user), from: sample_commit.parent_id, to: sample_commit.id
expect(response).to have_http_status(200)
expect(json_response['commits']).to be_present
expect(json_response['diffs']).to be_present
end
- it "should compare same refs" do
+ it "compares same refs" do
get api("/projects/#{project.id}/repository/compare", user), from: 'master', to: 'master'
expect(response).to have_http_status(200)
expect(json_response['commits']).to be_empty
@@ -161,7 +161,7 @@ describe API::API, api: true do
end
describe 'GET /projects/:id/repository/contributors' do
- it 'should return valid data' do
+ it 'returns valid data' do
get api("/projects/#{project.id}/repository/contributors", user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
diff --git a/spec/requests/api/runners_spec.rb b/spec/requests/api/runners_spec.rb
index 00a3c917b6a..f46f016135e 100644
--- a/spec/requests/api/runners_spec.rb
+++ b/spec/requests/api/runners_spec.rb
@@ -35,7 +35,7 @@ describe API::Runners, api: true do
describe 'GET /runners' do
context 'authorized user' do
- it 'should return user available runners' do
+ it 'returns user available runners' do
get api('/runners', user)
shared = json_response.any?{ |r| r['is_shared'] }
@@ -44,7 +44,7 @@ describe API::Runners, api: true do
expect(shared).to be_falsey
end
- it 'should filter runners by scope' do
+ it 'filters runners by scope' do
get api('/runners?scope=active', user)
shared = json_response.any?{ |r| r['is_shared'] }
@@ -53,14 +53,14 @@ describe API::Runners, api: true do
expect(shared).to be_falsey
end
- it 'should avoid filtering if scope is invalid' do
+ it 'avoids filtering if scope is invalid' do
get api('/runners?scope=unknown', user)
expect(response).to have_http_status(400)
end
end
context 'unauthorized user' do
- it 'should not return runners' do
+ it 'does not return runners' do
get api('/runners')
expect(response).to have_http_status(401)
@@ -71,7 +71,7 @@ describe API::Runners, api: true do
describe 'GET /runners/all' do
context 'authorized user' do
context 'with admin privileges' do
- it 'should return all runners' do
+ it 'returns all runners' do
get api('/runners/all', admin)
shared = json_response.any?{ |r| r['is_shared'] }
@@ -82,14 +82,14 @@ describe API::Runners, api: true do
end
context 'without admin privileges' do
- it 'should not return runners list' do
+ it 'does not return runners list' do
get api('/runners/all', user)
expect(response).to have_http_status(403)
end
end
- it 'should filter runners by scope' do
+ it 'filters runners by scope' do
get api('/runners/all?scope=specific', admin)
shared = json_response.any?{ |r| r['is_shared'] }
@@ -98,14 +98,14 @@ describe API::Runners, api: true do
expect(shared).to be_falsey
end
- it 'should avoid filtering if scope is invalid' do
+ it 'avoids filtering if scope is invalid' do
get api('/runners?scope=unknown', admin)
expect(response).to have_http_status(400)
end
end
context 'unauthorized user' do
- it 'should not return runners' do
+ it 'does not return runners' do
get api('/runners')
expect(response).to have_http_status(401)
@@ -116,7 +116,7 @@ describe API::Runners, api: true do
describe 'GET /runners/:id' do
context 'admin user' do
context 'when runner is shared' do
- it "should return runner's details" do
+ it "returns runner's details" do
get api("/runners/#{shared_runner.id}", admin)
expect(response).to have_http_status(200)
@@ -125,7 +125,7 @@ describe API::Runners, api: true do
end
context 'when runner is not shared' do
- it "should return runner's details" do
+ it "returns runner's details" do
get api("/runners/#{specific_runner.id}", admin)
expect(response).to have_http_status(200)
@@ -133,7 +133,7 @@ describe API::Runners, api: true do
end
end
- it 'should return 404 if runner does not exists' do
+ it 'returns 404 if runner does not exists' do
get api('/runners/9999', admin)
expect(response).to have_http_status(404)
@@ -142,7 +142,7 @@ describe API::Runners, api: true do
context "runner project's administrative user" do
context 'when runner is not shared' do
- it "should return runner's details" do
+ it "returns runner's details" do
get api("/runners/#{specific_runner.id}", user)
expect(response).to have_http_status(200)
@@ -151,7 +151,7 @@ describe API::Runners, api: true do
end
context 'when runner is shared' do
- it "should return runner's details" do
+ it "returns runner's details" do
get api("/runners/#{shared_runner.id}", user)
expect(response).to have_http_status(200)
@@ -161,7 +161,7 @@ describe API::Runners, api: true do
end
context 'other authorized user' do
- it "should not return runner's details" do
+ it "does not return runner's details" do
get api("/runners/#{specific_runner.id}", user2)
expect(response).to have_http_status(403)
@@ -169,7 +169,7 @@ describe API::Runners, api: true do
end
context 'unauthorized user' do
- it "should not return runner's details" do
+ it "does not return runner's details" do
get api("/runners/#{specific_runner.id}")
expect(response).to have_http_status(401)
@@ -180,7 +180,7 @@ describe API::Runners, api: true do
describe 'PUT /runners/:id' do
context 'admin user' do
context 'when runner is shared' do
- it 'should update runner' do
+ it 'updates runner' do
description = shared_runner.description
active = shared_runner.active
@@ -201,7 +201,7 @@ describe API::Runners, api: true do
end
context 'when runner is not shared' do
- it 'should update runner' do
+ it 'updates runner' do
description = specific_runner.description
update_runner(specific_runner.id, admin, description: 'test')
specific_runner.reload
@@ -212,7 +212,7 @@ describe API::Runners, api: true do
end
end
- it 'should return 404 if runner does not exists' do
+ it 'returns 404 if runner does not exists' do
update_runner(9999, admin, description: 'test')
expect(response).to have_http_status(404)
@@ -225,7 +225,7 @@ describe API::Runners, api: true do
context 'authorized user' do
context 'when runner is shared' do
- it 'should not update runner' do
+ it 'does not update runner' do
put api("/runners/#{shared_runner.id}", user)
expect(response).to have_http_status(403)
@@ -233,13 +233,13 @@ describe API::Runners, api: true do
end
context 'when runner is not shared' do
- it 'should not update runner without access to it' do
+ it 'does not update runner without access to it' do
put api("/runners/#{specific_runner.id}", user2)
expect(response).to have_http_status(403)
end
- it 'should update runner with access to it' do
+ it 'updates runner with access to it' do
description = specific_runner.description
put api("/runners/#{specific_runner.id}", admin), description: 'test'
specific_runner.reload
@@ -252,7 +252,7 @@ describe API::Runners, api: true do
end
context 'unauthorized user' do
- it 'should not delete runner' do
+ it 'does not delete runner' do
put api("/runners/#{specific_runner.id}")
expect(response).to have_http_status(401)
@@ -263,7 +263,7 @@ describe API::Runners, api: true do
describe 'DELETE /runners/:id' do
context 'admin user' do
context 'when runner is shared' do
- it 'should delete runner' do
+ it 'deletes runner' do
expect do
delete api("/runners/#{shared_runner.id}", admin)
end.to change{ Ci::Runner.shared.count }.by(-1)
@@ -272,14 +272,14 @@ describe API::Runners, api: true do
end
context 'when runner is not shared' do
- it 'should delete unused runner' do
+ it 'deletes unused runner' do
expect do
delete api("/runners/#{unused_specific_runner.id}", admin)
end.to change{ Ci::Runner.specific.count }.by(-1)
expect(response).to have_http_status(200)
end
- it 'should delete used runner' do
+ it 'deletes used runner' do
expect do
delete api("/runners/#{specific_runner.id}", admin)
end.to change{ Ci::Runner.specific.count }.by(-1)
@@ -287,7 +287,7 @@ describe API::Runners, api: true do
end
end
- it 'should return 404 if runner does not exists' do
+ it 'returns 404 if runner does not exists' do
delete api('/runners/9999', admin)
expect(response).to have_http_status(404)
@@ -296,24 +296,24 @@ describe API::Runners, api: true do
context 'authorized user' do
context 'when runner is shared' do
- it 'should not delete runner' do
+ it 'does not delete runner' do
delete api("/runners/#{shared_runner.id}", user)
expect(response).to have_http_status(403)
end
end
context 'when runner is not shared' do
- it 'should not delete runner without access to it' do
+ it 'does not delete runner without access to it' do
delete api("/runners/#{specific_runner.id}", user2)
expect(response).to have_http_status(403)
end
- it 'should not delete runner with more than one associated project' do
+ it 'does not delete runner with more than one associated project' do
delete api("/runners/#{two_projects_runner.id}", user)
expect(response).to have_http_status(403)
end
- it 'should delete runner for one owned project' do
+ it 'deletes runner for one owned project' do
expect do
delete api("/runners/#{specific_runner.id}", user)
end.to change{ Ci::Runner.specific.count }.by(-1)
@@ -323,7 +323,7 @@ describe API::Runners, api: true do
end
context 'unauthorized user' do
- it 'should not delete runner' do
+ it 'does not delete runner' do
delete api("/runners/#{specific_runner.id}")
expect(response).to have_http_status(401)
@@ -333,7 +333,7 @@ describe API::Runners, api: true do
describe 'GET /projects/:id/runners' do
context 'authorized user with master privileges' do
- it "should return project's runners" do
+ it "returns project's runners" do
get api("/projects/#{project.id}/runners", user)
shared = json_response.any?{ |r| r['is_shared'] }
@@ -344,7 +344,7 @@ describe API::Runners, api: true do
end
context 'authorized user without master privileges' do
- it "should not return project's runners" do
+ it "does not return project's runners" do
get api("/projects/#{project.id}/runners", user2)
expect(response).to have_http_status(403)
@@ -352,7 +352,7 @@ describe API::Runners, api: true do
end
context 'unauthorized user' do
- it "should not return project's runners" do
+ it "does not return project's runners" do
get api("/projects/#{project.id}/runners")
expect(response).to have_http_status(401)
@@ -368,21 +368,21 @@ describe API::Runners, api: true do
end
end
- it 'should enable specific runner' do
+ it 'enables specific runner' do
expect do
post api("/projects/#{project.id}/runners", user), runner_id: specific_runner2.id
end.to change{ project.runners.count }.by(+1)
expect(response).to have_http_status(201)
end
- it 'should avoid changes when enabling already enabled runner' do
+ it 'avoids changes when enabling already enabled runner' do
expect do
post api("/projects/#{project.id}/runners", user), runner_id: specific_runner.id
end.to change{ project.runners.count }.by(0)
expect(response).to have_http_status(409)
end
- it 'should not enable locked runner' do
+ it 'does not enable locked runner' do
specific_runner2.update(locked: true)
expect do
@@ -392,14 +392,14 @@ describe API::Runners, api: true do
expect(response).to have_http_status(403)
end
- it 'should not enable shared runner' do
+ it 'does not enable shared runner' do
post api("/projects/#{project.id}/runners", user), runner_id: shared_runner.id
expect(response).to have_http_status(403)
end
context 'user is admin' do
- it 'should enable any specific runner' do
+ it 'enables any specific runner' do
expect do
post api("/projects/#{project.id}/runners", admin), runner_id: unused_specific_runner.id
end.to change{ project.runners.count }.by(+1)
@@ -408,14 +408,14 @@ describe API::Runners, api: true do
end
context 'user is not admin' do
- it 'should not enable runner without access to' do
+ it 'does not enable runner without access to' do
post api("/projects/#{project.id}/runners", user), runner_id: unused_specific_runner.id
expect(response).to have_http_status(403)
end
end
- it 'should raise an error when no runner_id param is provided' do
+ it 'raises an error when no runner_id param is provided' do
post api("/projects/#{project.id}/runners", admin)
expect(response).to have_http_status(400)
@@ -423,7 +423,7 @@ describe API::Runners, api: true do
end
context 'authorized user without permissions' do
- it 'should not enable runner' do
+ it 'does not enable runner' do
post api("/projects/#{project.id}/runners", user2)
expect(response).to have_http_status(403)
@@ -431,7 +431,7 @@ describe API::Runners, api: true do
end
context 'unauthorized user' do
- it 'should not enable runner' do
+ it 'does not enable runner' do
post api("/projects/#{project.id}/runners")
expect(response).to have_http_status(401)
@@ -442,7 +442,7 @@ describe API::Runners, api: true do
describe 'DELETE /projects/:id/runners/:runner_id' do
context 'authorized user' do
context 'when runner have more than one associated projects' do
- it "should disable project's runner" do
+ it "disables project's runner" do
expect do
delete api("/projects/#{project.id}/runners/#{two_projects_runner.id}", user)
end.to change{ project.runners.count }.by(-1)
@@ -451,7 +451,7 @@ describe API::Runners, api: true do
end
context 'when runner have one associated projects' do
- it "should not disable project's runner" do
+ it "does not disable project's runner" do
expect do
delete api("/projects/#{project.id}/runners/#{specific_runner.id}", user)
end.to change{ project.runners.count }.by(0)
@@ -459,7 +459,7 @@ describe API::Runners, api: true do
end
end
- it 'should return 404 is runner is not found' do
+ it 'returns 404 is runner is not found' do
delete api("/projects/#{project.id}/runners/9999", user)
expect(response).to have_http_status(404)
@@ -467,7 +467,7 @@ describe API::Runners, api: true do
end
context 'authorized user without permissions' do
- it "should not disable project's runner" do
+ it "does not disable project's runner" do
delete api("/projects/#{project.id}/runners/#{specific_runner.id}", user2)
expect(response).to have_http_status(403)
@@ -475,7 +475,7 @@ describe API::Runners, api: true do
end
context 'unauthorized user' do
- it "should not disable project's runner" do
+ it "does not disable project's runner" do
delete api("/projects/#{project.id}/runners/#{specific_runner.id}")
expect(response).to have_http_status(401)
diff --git a/spec/requests/api/services_spec.rb b/spec/requests/api/services_spec.rb
index a2446e12804..375671bca4c 100644
--- a/spec/requests/api/services_spec.rb
+++ b/spec/requests/api/services_spec.rb
@@ -11,13 +11,13 @@ describe API::API, api: true do
describe "PUT /projects/:id/services/#{service.dasherize}" do
include_context service
- it "should update #{service} settings" do
+ it "updates #{service} settings" do
put api("/projects/#{project.id}/services/#{dashed_service}", user), service_attrs
expect(response).to have_http_status(200)
end
- it "should return if required fields missing" do
+ it "returns if required fields missing" do
attrs = service_attrs
required_attributes = service_attrs_list.select do |attr|
@@ -32,7 +32,7 @@ describe API::API, api: true do
attrs.delete(required_attributes.sample)
expected_code = 400
end
-
+
put api("/projects/#{project.id}/services/#{dashed_service}", user), attrs
expect(response.status).to eq(expected_code)
@@ -42,7 +42,7 @@ describe API::API, api: true do
describe "DELETE /projects/:id/services/#{service.dasherize}" do
include_context service
- it "should delete #{service}" do
+ it "deletes #{service}" do
delete api("/projects/#{project.id}/services/#{dashed_service}", user)
expect(response).to have_http_status(200)
@@ -62,29 +62,29 @@ describe API::API, api: true do
service_object.save
end
- it 'should return authentication error when unauthenticated' do
+ it 'returns authentication error when unauthenticated' do
get api("/projects/#{project.id}/services/#{dashed_service}")
expect(response).to have_http_status(401)
end
-
- it "should return all properties of service #{service} when authenticated as admin" do
+
+ it "returns all properties of service #{service} when authenticated as admin" do
get api("/projects/#{project.id}/services/#{dashed_service}", admin)
-
+
expect(response).to have_http_status(200)
expect(json_response['properties'].keys.map(&:to_sym)).to match_array(service_attrs_list.map)
end
- it "should return properties of service #{service} other than passwords when authenticated as project owner" do
+ it "returns properties of service #{service} other than passwords when authenticated as project owner" do
get api("/projects/#{project.id}/services/#{dashed_service}", user)
expect(response).to have_http_status(200)
expect(json_response['properties'].keys.map(&:to_sym)).to match_array(service_attrs_list_without_passwords)
end
- it "should return error when authenticated but not a project owner" do
+ it "returns error when authenticated but not a project owner" do
project.team << [user2, :developer]
get api("/projects/#{project.id}/services/#{dashed_service}", user2)
-
+
expect(response).to have_http_status(403)
end
end
diff --git a/spec/requests/api/session_spec.rb b/spec/requests/api/session_spec.rb
index c15b7ff9792..acad1365ace 100644
--- a/spec/requests/api/session_spec.rb
+++ b/spec/requests/api/session_spec.rb
@@ -7,7 +7,7 @@ describe API::API, api: true do
describe "POST /session" do
context "when valid password" do
- it "should return private token" do
+ it "returns private token" do
post api("/session"), email: user.email, password: '12345678'
expect(response).to have_http_status(201)
@@ -17,10 +17,21 @@ describe API::API, api: true do
expect(json_response['can_create_project']).to eq(user.can_create_project?)
expect(json_response['can_create_group']).to eq(user.can_create_group?)
end
+
+ context 'with 2FA enabled' do
+ it 'rejects sign in attempts' do
+ user = create(:user, :two_factor)
+
+ post api('/session'), email: user.email, password: user.password
+
+ expect(response).to have_http_status(401)
+ expect(response.body).to include('You have 2FA enabled.')
+ end
+ end
end
context 'when email has case-typo and password is valid' do
- it 'should return private token' do
+ it 'returns private token' do
post api('/session'), email: user.email.upcase, password: '12345678'
expect(response.status).to eq 201
@@ -33,7 +44,7 @@ describe API::API, api: true do
end
context 'when login has case-typo and password is valid' do
- it 'should return private token' do
+ it 'returns private token' do
post api('/session'), login: user.username.upcase, password: '12345678'
expect(response.status).to eq 201
@@ -46,7 +57,7 @@ describe API::API, api: true do
end
context "when invalid password" do
- it "should return authentication error" do
+ it "returns authentication error" do
post api("/session"), email: user.email, password: '123'
expect(response).to have_http_status(401)
@@ -56,7 +67,7 @@ describe API::API, api: true do
end
context "when empty password" do
- it "should return authentication error" do
+ it "returns authentication error" do
post api("/session"), email: user.email
expect(response).to have_http_status(401)
@@ -66,7 +77,7 @@ describe API::API, api: true do
end
context "when empty name" do
- it "should return authentication error" do
+ it "returns authentication error" do
post api("/session"), password: user.password
expect(response).to have_http_status(401)
diff --git a/spec/requests/api/settings_spec.rb b/spec/requests/api/settings_spec.rb
index 684c2cd8e24..54d096e8b7f 100644
--- a/spec/requests/api/settings_spec.rb
+++ b/spec/requests/api/settings_spec.rb
@@ -7,7 +7,7 @@ describe API::API, 'Settings', api: true do
let(:admin) { create(:admin) }
describe "GET /application/settings" do
- it "should return application settings" do
+ it "returns application settings" do
get api("/application/settings", admin)
expect(response).to have_http_status(200)
expect(json_response).to be_an Hash
@@ -23,7 +23,7 @@ describe API::API, 'Settings', api: true do
allow(Gitlab.config.repositories).to receive(:storages).and_return(storages)
end
- it "should update application settings" do
+ it "updates application settings" do
put api("/application/settings", admin),
default_projects_limit: 3, signin_enabled: false, repository_storage: 'custom'
expect(response).to have_http_status(200)
diff --git a/spec/requests/api/system_hooks_spec.rb b/spec/requests/api/system_hooks_spec.rb
index cf66f261ade..1ce2658569e 100644
--- a/spec/requests/api/system_hooks_spec.rb
+++ b/spec/requests/api/system_hooks_spec.rb
@@ -11,21 +11,21 @@ describe API::API, api: true do
describe "GET /hooks" do
context "when no user" do
- it "should return authentication error" do
+ it "returns authentication error" do
get api("/hooks")
expect(response).to have_http_status(401)
end
end
context "when not an admin" do
- it "should return forbidden error" do
+ it "returns forbidden error" do
get api("/hooks", user)
expect(response).to have_http_status(403)
end
end
context "when authenticated as admin" do
- it "should return an array of hooks" do
+ it "returns an array of hooks" do
get api("/hooks", admin)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
@@ -35,18 +35,18 @@ describe API::API, api: true do
end
describe "POST /hooks" do
- it "should create new hook" do
+ it "creates new hook" do
expect do
post api("/hooks", admin), url: 'http://example.com'
end.to change { SystemHook.count }.by(1)
end
- it "should respond with 400 if url not given" do
+ it "responds with 400 if url not given" do
post api("/hooks", admin)
expect(response).to have_http_status(400)
end
- it "should not create new hook without url" do
+ it "does not create new hook without url" do
expect do
post api("/hooks", admin)
end.not_to change { SystemHook.count }
@@ -54,26 +54,26 @@ describe API::API, api: true do
end
describe "GET /hooks/:id" do
- it "should return hook by id" do
+ it "returns hook by id" do
get api("/hooks/#{hook.id}", admin)
expect(response).to have_http_status(200)
expect(json_response['event_name']).to eq('project_create')
end
- it "should return 404 on failure" do
+ it "returns 404 on failure" do
get api("/hooks/404", admin)
expect(response).to have_http_status(404)
end
end
describe "DELETE /hooks/:id" do
- it "should delete a hook" do
+ it "deletes a hook" do
expect do
delete api("/hooks/#{hook.id}", admin)
end.to change { SystemHook.count }.by(-1)
end
- it "should return success if hook id not found" do
+ it "returns success if hook id not found" do
delete api("/hooks/12345", admin)
expect(response).to have_http_status(200)
end
diff --git a/spec/requests/api/tags_spec.rb b/spec/requests/api/tags_spec.rb
index fa700ab7343..d563883cd47 100644
--- a/spec/requests/api/tags_spec.rb
+++ b/spec/requests/api/tags_spec.rb
@@ -16,7 +16,7 @@ describe API::API, api: true do
let(:description) { 'Awesome release!' }
context 'without releases' do
- it "should return an array of project tags" do
+ it "returns an array of project tags" do
get api("/projects/#{project.id}/repository/tags", user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
@@ -30,7 +30,7 @@ describe API::API, api: true do
release.update_attributes(description: description)
end
- it "should return an array of project tags with release info" do
+ it "returns an array of project tags with release info" do
get api("/projects/#{project.id}/repository/tags", user)
expect(response).to have_http_status(200)
@@ -61,7 +61,7 @@ describe API::API, api: true do
describe 'POST /projects/:id/repository/tags' do
context 'lightweight tags' do
- it 'should create a new tag' do
+ it 'creates a new tag' do
post api("/projects/#{project.id}/repository/tags", user),
tag_name: 'v7.0.1',
ref: 'master'
@@ -72,7 +72,7 @@ describe API::API, api: true do
end
context 'lightweight tags with release notes' do
- it 'should create a new tag' do
+ it 'creates a new tag' do
post api("/projects/#{project.id}/repository/tags", user),
tag_name: 'v7.0.1',
ref: 'master',
@@ -92,13 +92,13 @@ describe API::API, api: true do
end
context 'delete tag' do
- it 'should delete an existing tag' do
+ it 'deletes an existing tag' do
delete api("/projects/#{project.id}/repository/tags/#{tag_name}", user)
expect(response).to have_http_status(200)
expect(json_response['tag_name']).to eq(tag_name)
end
- it 'should raise 404 if the tag does not exist' do
+ it 'raises 404 if the tag does not exist' do
delete api("/projects/#{project.id}/repository/tags/foobar", user)
expect(response).to have_http_status(404)
end
@@ -106,7 +106,7 @@ describe API::API, api: true do
end
context 'annotated tag' do
- it 'should create a new annotated tag' do
+ it 'creates a new annotated tag' do
# Identity must be set in .gitconfig to create annotated tag.
repo_path = project.repository.path_to_repo
system(*%W(#{Gitlab.config.git.bin_path} --git-dir=#{repo_path} config user.name #{user.name}))
@@ -123,14 +123,14 @@ describe API::API, api: true do
end
end
- it 'should deny for user without push access' do
+ it 'denies for user without push access' do
post api("/projects/#{project.id}/repository/tags", user2),
tag_name: 'v1.9.0',
ref: '621491c677087aa243f165eab467bfdfbee00be1'
expect(response).to have_http_status(403)
end
- it 'should return 400 if tag name is invalid' do
+ it 'returns 400 if tag name is invalid' do
post api("/projects/#{project.id}/repository/tags", user),
tag_name: 'v 1.0.0',
ref: 'master'
@@ -138,7 +138,7 @@ describe API::API, api: true do
expect(json_response['message']).to eq('Tag name invalid')
end
- it 'should return 400 if tag already exists' do
+ it 'returns 400 if tag already exists' do
post api("/projects/#{project.id}/repository/tags", user),
tag_name: 'v8.0.0',
ref: 'master'
@@ -150,7 +150,7 @@ describe API::API, api: true do
expect(json_response['message']).to eq('Tag v8.0.0 already exists')
end
- it 'should return 400 if ref name is invalid' do
+ it 'returns 400 if ref name is invalid' do
post api("/projects/#{project.id}/repository/tags", user),
tag_name: 'mytag',
ref: 'foo'
@@ -163,7 +163,7 @@ describe API::API, api: true do
let(:tag_name) { project.repository.tag_names.first }
let(:description) { 'Awesome release!' }
- it 'should create description for existing git tag' do
+ it 'creates description for existing git tag' do
post api("/projects/#{project.id}/repository/tags/#{tag_name}/release", user),
description: description
@@ -172,7 +172,7 @@ describe API::API, api: true do
expect(json_response['description']).to eq(description)
end
- it 'should return 404 if the tag does not exist' do
+ it 'returns 404 if the tag does not exist' do
post api("/projects/#{project.id}/repository/tags/foobar/release", user),
description: description
@@ -186,7 +186,7 @@ describe API::API, api: true do
release.update_attributes(description: description)
end
- it 'should return 409 if there is already a release' do
+ it 'returns 409 if there is already a release' do
post api("/projects/#{project.id}/repository/tags/#{tag_name}/release", user),
description: description
@@ -207,7 +207,7 @@ describe API::API, api: true do
release.update_attributes(description: description)
end
- it 'should update the release description' do
+ it 'updates the release description' do
put api("/projects/#{project.id}/repository/tags/#{tag_name}/release", user),
description: new_description
@@ -217,7 +217,7 @@ describe API::API, api: true do
end
end
- it 'should return 404 if the tag does not exist' do
+ it 'returns 404 if the tag does not exist' do
put api("/projects/#{project.id}/repository/tags/foobar/release", user),
description: new_description
@@ -225,7 +225,7 @@ describe API::API, api: true do
expect(json_response['message']).to eq('Tag does not exist')
end
- it 'should return 404 if the release does not exist' do
+ it 'returns 404 if the release does not exist' do
put api("/projects/#{project.id}/repository/tags/#{tag_name}/release", user),
description: new_description
diff --git a/spec/requests/api/templates_spec.rb b/spec/requests/api/templates_spec.rb
index 68d0f41b489..5bd5b861792 100644
--- a/spec/requests/api/templates_spec.rb
+++ b/spec/requests/api/templates_spec.rb
@@ -3,50 +3,53 @@ require 'spec_helper'
describe API::Templates, api: true do
include ApiHelpers
- describe 'the Template Entity' do
- before { get api('/gitignores/Ruby') }
+ context 'global templates' do
+ describe 'the Template Entity' do
+ before { get api('/gitignores/Ruby') }
- it { expect(json_response['name']).to eq('Ruby') }
- it { expect(json_response['content']).to include('*.gem') }
- end
+ it { expect(json_response['name']).to eq('Ruby') }
+ it { expect(json_response['content']).to include('*.gem') }
+ end
- describe 'the TemplateList Entity' do
- before { get api('/gitignores') }
+ describe 'the TemplateList Entity' 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
+ it { expect(json_response.first['name']).not_to be_nil }
+ it { expect(json_response.first['content']).to be_nil }
+ end
- context 'requesting gitignores' do
- describe 'GET /gitignores' do
- it 'returns a list of available gitignore templates' do
- get api('/gitignores')
+ context 'requesting gitignores' do
+ describe 'GET /gitignores' do
+ it 'returns a list of available gitignore templates' do
+ get api('/gitignores')
- expect(response).to have_http_status(200)
- expect(json_response).to be_an Array
- expect(json_response.size).to be > 15
+ expect(response.status).to eq(200)
+ expect(json_response).to be_an Array
+ expect(json_response.size).to be > 15
+ end
end
end
- end
- context 'requesting gitlab-ci-ymls' do
- describe 'GET /gitlab_ci_ymls' do
- it 'returns a list of available gitlab_ci_ymls' do
- get api('/gitlab_ci_ymls')
+ context 'requesting gitlab-ci-ymls' do
+ describe 'GET /gitlab_ci_ymls' do
+ it 'returns a list of available gitlab_ci_ymls' do
+ get api('/gitlab_ci_ymls')
- expect(response).to have_http_status(200)
- expect(json_response).to be_an Array
- expect(json_response.first['name']).not_to be_nil
+ expect(response.status).to eq(200)
+ expect(json_response).to be_an Array
+ expect(json_response.first['name']).not_to be_nil
+ end
end
end
- end
- describe 'GET /gitlab_ci_ymls/Ruby' do
- it 'adds a disclaimer on the top' do
- get api('/gitlab_ci_ymls/Ruby')
+ describe 'GET /gitlab_ci_ymls/Ruby' do
+ it 'adds a disclaimer on the top' do
+ get api('/gitlab_ci_ymls/Ruby')
- expect(response).to have_http_status(200)
- expect(json_response['content']).to start_with("# This file is a template,")
+ expect(response).to have_http_status(200)
+ expect(json_response['name']).not_to be_nil
+ expect(json_response['content']).to start_with("# This file is a template,")
+ end
end
end
end
diff --git a/spec/requests/api/todos_spec.rb b/spec/requests/api/todos_spec.rb
index 92a4fa216cd..887a2ba5b84 100644
--- a/spec/requests/api/todos_spec.rb
+++ b/spec/requests/api/todos_spec.rb
@@ -117,6 +117,12 @@ describe API::Todos, api: true do
expect(response.status).to eq(200)
expect(pending_1.reload).to be_done
end
+
+ it 'updates todos cache' do
+ expect_any_instance_of(User).to receive(:update_todos_count_cache).and_call_original
+
+ delete api("/todos/#{pending_1.id}", john_doe)
+ end
end
end
@@ -134,12 +140,17 @@ describe API::Todos, api: true do
delete api('/todos', john_doe)
expect(response.status).to eq(200)
- expect(json_response).to be_an Array
- expect(json_response.length).to eq(3)
+ expect(response.body).to eq('3')
expect(pending_1.reload).to be_done
expect(pending_2.reload).to be_done
expect(pending_3.reload).to be_done
end
+
+ it 'updates todos cache' do
+ expect_any_instance_of(User).to receive(:update_todos_count_cache).and_call_original
+
+ delete api("/todos", john_doe)
+ end
end
end
diff --git a/spec/requests/api/triggers_spec.rb b/spec/requests/api/triggers_spec.rb
index 8992996c30a..82bba1ce8a4 100644
--- a/spec/requests/api/triggers_spec.rb
+++ b/spec/requests/api/triggers_spec.rb
@@ -27,17 +27,17 @@ describe API::API do
end
context 'Handles errors' do
- it 'should return bad request if token is missing' do
+ it 'returns bad request if token is missing' do
post api("/projects/#{project.id}/trigger/builds"), ref: 'master'
expect(response).to have_http_status(400)
end
- it 'should return not found if project is not found' do
+ it 'returns not found if project is not found' do
post api('/projects/0/trigger/builds'), options.merge(ref: 'master')
expect(response).to have_http_status(404)
end
- it 'should return unauthorized if token is for different project' do
+ it 'returns unauthorized if token is for different project' do
post api("/projects/#{project2.id}/trigger/builds"), options.merge(ref: 'master')
expect(response).to have_http_status(401)
end
@@ -46,14 +46,15 @@ describe API::API do
context 'Have a commit' do
let(:pipeline) { project.pipelines.last }
- it 'should create builds' do
+ it 'creates builds' do
post api("/projects/#{project.id}/trigger/builds"), options.merge(ref: 'master')
expect(response).to have_http_status(201)
pipeline.builds.reload
- expect(pipeline.builds.size).to eq(2)
+ expect(pipeline.builds.pending.size).to eq(2)
+ expect(pipeline.builds.size).to eq(5)
end
- it 'should return bad request with no builds created if there\'s no commit for that ref' do
+ it 'returns bad request with no builds created if there\'s no commit for that ref' do
post api("/projects/#{project.id}/trigger/builds"), options.merge(ref: 'other-branch')
expect(response).to have_http_status(400)
expect(json_response['message']).to eq('No builds created')
@@ -64,19 +65,19 @@ describe API::API do
{ 'TRIGGER_KEY' => 'TRIGGER_VALUE' }
end
- it 'should validate variables to be a hash' do
+ it 'validates variables to be a hash' do
post api("/projects/#{project.id}/trigger/builds"), options.merge(variables: 'value', ref: 'master')
expect(response).to have_http_status(400)
expect(json_response['message']).to eq('variables needs to be a hash')
end
- it 'should validate variables needs to be a map of key-valued strings' do
+ it 'validates variables needs to be a map of key-valued strings' do
post api("/projects/#{project.id}/trigger/builds"), options.merge(variables: { key: %w(1 2) }, ref: 'master')
expect(response).to have_http_status(400)
expect(json_response['message']).to eq('variables needs to be a map of key-valued strings')
end
- it 'create trigger request with variables' do
+ it 'creates trigger request with variables' do
post api("/projects/#{project.id}/trigger/builds"), options.merge(variables: variables, ref: 'master')
expect(response).to have_http_status(201)
pipeline.builds.reload
@@ -88,7 +89,7 @@ describe API::API do
describe 'GET /projects/:id/triggers' do
context 'authenticated user with valid permissions' do
- it 'should return list of triggers' do
+ it 'returns list of triggers' do
get api("/projects/#{project.id}/triggers", user)
expect(response).to have_http_status(200)
@@ -98,7 +99,7 @@ describe API::API do
end
context 'authenticated user with invalid permissions' do
- it 'should not return triggers list' do
+ it 'does not return triggers list' do
get api("/projects/#{project.id}/triggers", user2)
expect(response).to have_http_status(403)
@@ -106,7 +107,7 @@ describe API::API do
end
context 'unauthenticated user' do
- it 'should not return triggers list' do
+ it 'does not return triggers list' do
get api("/projects/#{project.id}/triggers")
expect(response).to have_http_status(401)
@@ -116,14 +117,14 @@ describe API::API do
describe 'GET /projects/:id/triggers/:token' do
context 'authenticated user with valid permissions' do
- it 'should return trigger details' do
+ it 'returns trigger details' do
get api("/projects/#{project.id}/triggers/#{trigger.token}", user)
expect(response).to have_http_status(200)
expect(json_response).to be_a(Hash)
end
- it 'should respond with 404 Not Found if requesting non-existing trigger' do
+ it 'responds with 404 Not Found if requesting non-existing trigger' do
get api("/projects/#{project.id}/triggers/abcdef012345", user)
expect(response).to have_http_status(404)
@@ -131,7 +132,7 @@ describe API::API do
end
context 'authenticated user with invalid permissions' do
- it 'should not return triggers list' do
+ it 'does not return triggers list' do
get api("/projects/#{project.id}/triggers/#{trigger.token}", user2)
expect(response).to have_http_status(403)
@@ -139,7 +140,7 @@ describe API::API do
end
context 'unauthenticated user' do
- it 'should not return triggers list' do
+ it 'does not return triggers list' do
get api("/projects/#{project.id}/triggers/#{trigger.token}")
expect(response).to have_http_status(401)
@@ -149,7 +150,7 @@ describe API::API do
describe 'POST /projects/:id/triggers' do
context 'authenticated user with valid permissions' do
- it 'should create trigger' do
+ it 'creates trigger' do
expect do
post api("/projects/#{project.id}/triggers", user)
end.to change{project.triggers.count}.by(1)
@@ -160,7 +161,7 @@ describe API::API do
end
context 'authenticated user with invalid permissions' do
- it 'should not create trigger' do
+ it 'does not create trigger' do
post api("/projects/#{project.id}/triggers", user2)
expect(response).to have_http_status(403)
@@ -168,7 +169,7 @@ describe API::API do
end
context 'unauthenticated user' do
- it 'should not create trigger' do
+ it 'does not create trigger' do
post api("/projects/#{project.id}/triggers")
expect(response).to have_http_status(401)
@@ -178,14 +179,14 @@ describe API::API do
describe 'DELETE /projects/:id/triggers/:token' do
context 'authenticated user with valid permissions' do
- it 'should delete trigger' do
+ it 'deletes trigger' do
expect do
delete api("/projects/#{project.id}/triggers/#{trigger.token}", user)
end.to change{project.triggers.count}.by(-1)
expect(response).to have_http_status(200)
end
- it 'should respond with 404 Not Found if requesting non-existing trigger' do
+ it 'responds with 404 Not Found if requesting non-existing trigger' do
delete api("/projects/#{project.id}/triggers/abcdef012345", user)
expect(response).to have_http_status(404)
@@ -193,7 +194,7 @@ describe API::API do
end
context 'authenticated user with invalid permissions' do
- it 'should not delete trigger' do
+ it 'does not delete trigger' do
delete api("/projects/#{project.id}/triggers/#{trigger.token}", user2)
expect(response).to have_http_status(403)
@@ -201,7 +202,7 @@ describe API::API do
end
context 'unauthenticated user' do
- it 'should not delete trigger' do
+ it 'does not delete trigger' do
delete api("/projects/#{project.id}/triggers/#{trigger.token}")
expect(response).to have_http_status(401)
diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb
index e43e3e269bf..0bbba64a6d5 100644
--- a/spec/requests/api/users_spec.rb
+++ b/spec/requests/api/users_spec.rb
@@ -13,7 +13,7 @@ describe API::API, api: true do
describe "GET /users" do
context "when unauthenticated" do
- it "should return authentication error" do
+ it "returns authentication error" do
get api("/users")
expect(response).to have_http_status(401)
end
@@ -38,7 +38,7 @@ describe API::API, api: true do
end
end
- it "should return an array of users" do
+ it "returns an array of users" do
get api("/users", user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
@@ -48,7 +48,7 @@ describe API::API, api: true do
end['username']).to eq(username)
end
- it "should return one user" do
+ it "returns one user" do
get api("/users?username=#{omniauth_user.username}", user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
@@ -57,7 +57,7 @@ describe API::API, api: true do
end
context "when admin" do
- it "should return an array of users" do
+ it "returns an array of users" do
get api("/users", admin)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
@@ -72,24 +72,24 @@ describe API::API, api: true do
end
describe "GET /users/:id" do
- it "should return a user by id" do
+ it "returns a user by id" do
get api("/users/#{user.id}", user)
expect(response).to have_http_status(200)
expect(json_response['username']).to eq(user.username)
end
- it "should return a 401 if unauthenticated" do
+ it "returns a 401 if unauthenticated" do
get api("/users/9998")
expect(response).to have_http_status(401)
end
- it "should return a 404 error if user id not found" do
+ it "returns a 404 error if user id not found" do
get api("/users/9999", user)
expect(response).to have_http_status(404)
expect(json_response['message']).to eq('404 Not found')
end
- it "should return a 404 if invalid ID" do
+ it "returns a 404 if invalid ID" do
get api("/users/1ASDF", user)
expect(response).to have_http_status(404)
end
@@ -98,13 +98,13 @@ describe API::API, api: true do
describe "POST /users" do
before{ admin }
- it "should create user" do
+ it "creates user" do
expect do
post api("/users", admin), attributes_for(:user, projects_limit: 3)
end.to change { User.count }.by(1)
end
- it "should create user with correct attributes" do
+ it "creates user with correct attributes" do
post api('/users', admin), attributes_for(:user, admin: true, can_create_group: true)
expect(response).to have_http_status(201)
user_id = json_response['id']
@@ -114,7 +114,7 @@ describe API::API, api: true do
expect(new_user.can_create_group).to eq(true)
end
- it "should create non-admin user" do
+ it "creates non-admin user" do
post api('/users', admin), attributes_for(:user, admin: false, can_create_group: false)
expect(response).to have_http_status(201)
user_id = json_response['id']
@@ -124,7 +124,7 @@ describe API::API, api: true do
expect(new_user.can_create_group).to eq(false)
end
- it "should create non-admin users by default" do
+ it "creates non-admin users by default" do
post api('/users', admin), attributes_for(:user)
expect(response).to have_http_status(201)
user_id = json_response['id']
@@ -133,7 +133,7 @@ describe API::API, api: true do
expect(new_user.admin).to eq(false)
end
- it "should return 201 Created on success" do
+ it "returns 201 Created on success" do
post api("/users", admin), attributes_for(:user, projects_limit: 3)
expect(response).to have_http_status(201)
end
@@ -148,7 +148,7 @@ describe API::API, api: true do
expect(new_user.external).to be_falsy
end
- it 'should allow an external user to be created' do
+ it 'allows an external user to be created' do
post api("/users", admin), attributes_for(:user, external: true)
expect(response).to have_http_status(201)
@@ -158,7 +158,7 @@ describe API::API, api: true do
expect(new_user.external).to be_truthy
end
- it "should not create user with invalid email" do
+ it "does not create user with invalid email" do
post api('/users', admin),
email: 'invalid email',
password: 'password',
@@ -166,27 +166,27 @@ describe API::API, api: true do
expect(response).to have_http_status(400)
end
- it 'should return 400 error if name not given' do
+ it 'returns 400 error if name not given' do
post api('/users', admin), attributes_for(:user).except(:name)
expect(response).to have_http_status(400)
end
- it 'should return 400 error if password not given' do
+ it 'returns 400 error if password not given' do
post api('/users', admin), attributes_for(:user).except(:password)
expect(response).to have_http_status(400)
end
- it 'should return 400 error if email not given' do
+ it 'returns 400 error if email not given' do
post api('/users', admin), attributes_for(:user).except(:email)
expect(response).to have_http_status(400)
end
- it 'should return 400 error if username not given' do
+ it 'returns 400 error if username not given' do
post api('/users', admin), attributes_for(:user).except(:username)
expect(response).to have_http_status(400)
end
- it 'should return 400 error if user does not validate' do
+ it 'returns 400 error if user does not validate' do
post api('/users', admin),
password: 'pass',
email: 'test@example.com',
@@ -205,7 +205,7 @@ describe API::API, api: true do
to eq([Gitlab::Regex.namespace_regex_message])
end
- it "shouldn't available for non admin users" do
+ it "is not available for non admin users" do
post api("/users", user), attributes_for(:user)
expect(response).to have_http_status(403)
end
@@ -219,7 +219,7 @@ describe API::API, api: true do
name: 'foo'
end
- it 'should return 409 conflict error if user with same email exists' do
+ it 'returns 409 conflict error if user with same email exists' do
expect do
post api('/users', admin),
name: 'foo',
@@ -231,7 +231,7 @@ describe API::API, api: true do
expect(json_response['message']).to eq('Email has already been taken')
end
- it 'should return 409 conflict error if same username exists' do
+ it 'returns 409 conflict error if same username exists' do
expect do
post api('/users', admin),
name: 'foo',
@@ -246,7 +246,7 @@ describe API::API, api: true do
end
describe "GET /users/sign_up" do
- it "should redirect to sign in page" do
+ it "redirects to sign in page" do
get "/users/sign_up"
expect(response).to have_http_status(302)
expect(response).to redirect_to(new_user_session_path)
@@ -258,55 +258,55 @@ describe API::API, api: true do
before { admin }
- it "should update user with new bio" do
+ it "updates user with new bio" do
put api("/users/#{user.id}", admin), { bio: 'new test bio' }
expect(response).to have_http_status(200)
expect(json_response['bio']).to eq('new test bio')
expect(user.reload.bio).to eq('new test bio')
end
- it 'should update user with his own email' do
+ it 'updates user with his own email' do
put api("/users/#{user.id}", admin), email: user.email
expect(response).to have_http_status(200)
expect(json_response['email']).to eq(user.email)
expect(user.reload.email).to eq(user.email)
end
- it 'should update user with his own username' do
+ it 'updates user with his own username' do
put api("/users/#{user.id}", admin), username: user.username
expect(response).to have_http_status(200)
expect(json_response['username']).to eq(user.username)
expect(user.reload.username).to eq(user.username)
end
- it "should update user's existing identity" do
+ it "updates user's existing identity" do
put api("/users/#{omniauth_user.id}", admin), provider: 'ldapmain', extern_uid: '654321'
expect(response).to have_http_status(200)
expect(omniauth_user.reload.identities.first.extern_uid).to eq('654321')
end
- it 'should update user with new identity' do
+ it 'updates user with new identity' do
put api("/users/#{user.id}", admin), provider: 'github', extern_uid: '67890'
expect(response).to have_http_status(200)
expect(user.reload.identities.first.extern_uid).to eq('67890')
expect(user.reload.identities.first.provider).to eq('github')
end
- it "should update admin status" do
+ it "updates admin status" do
put api("/users/#{user.id}", admin), { admin: true }
expect(response).to have_http_status(200)
expect(json_response['is_admin']).to eq(true)
expect(user.reload.admin).to eq(true)
end
- it "should update external status" do
+ it "updates external status" do
put api("/users/#{user.id}", admin), { external: true }
expect(response.status).to eq 200
expect(json_response['external']).to eq(true)
expect(user.reload.external?).to be_truthy
end
- it "should not update admin status" do
+ it "does not update admin status" do
put api("/users/#{admin_user.id}", admin), { can_create_group: false }
expect(response).to have_http_status(200)
expect(json_response['is_admin']).to eq(true)
@@ -314,28 +314,28 @@ describe API::API, api: true do
expect(admin_user.can_create_group).to eq(false)
end
- it "should not allow invalid update" do
+ it "does not allow invalid update" do
put api("/users/#{user.id}", admin), { email: 'invalid email' }
expect(response).to have_http_status(400)
expect(user.reload.email).not_to eq('invalid email')
end
- it "shouldn't available for non admin users" do
+ it "is not available for non admin users" do
put api("/users/#{user.id}", user), attributes_for(:user)
expect(response).to have_http_status(403)
end
- it "should return 404 for non-existing user" do
+ it "returns 404 for non-existing user" do
put api("/users/999999", admin), { bio: 'update should fail' }
expect(response).to have_http_status(404)
expect(json_response['message']).to eq('404 Not found')
end
- it "should raise error for invalid ID" do
+ it "raises error for invalid ID" do
expect{put api("/users/ASDF", admin) }.to raise_error(ActionController::RoutingError)
end
- it 'should return 400 error if user does not validate' do
+ it 'returns 400 error if user does not validate' do
put api("/users/#{user.id}", admin),
password: 'pass',
email: 'test@example.com',
@@ -361,13 +361,13 @@ describe API::API, api: true do
@user = User.all.last
end
- it 'should return 409 conflict error if email address exists' do
+ it 'returns 409 conflict error if email address exists' do
put api("/users/#{@user.id}", admin), email: 'test@example.com'
expect(response).to have_http_status(409)
expect(@user.reload.email).to eq(@user.email)
end
- it 'should return 409 conflict error if username taken' do
+ it 'returns 409 conflict error if username taken' do
@user_id = User.all.last.id
put api("/users/#{@user.id}", admin), username: 'test'
expect(response).to have_http_status(409)
@@ -379,28 +379,28 @@ describe API::API, api: true do
describe "POST /users/:id/keys" do
before { admin }
- it "should not create invalid ssh key" do
+ it "does not create invalid ssh key" do
post api("/users/#{user.id}/keys", admin), { title: "invalid key" }
expect(response).to have_http_status(400)
expect(json_response['message']).to eq('400 (Bad request) "key" not given')
end
- it 'should not create key without title' do
+ it 'does not create key without title' do
post api("/users/#{user.id}/keys", admin), key: 'some key'
expect(response).to have_http_status(400)
expect(json_response['message']).to eq('400 (Bad request) "title" not given')
end
- it "should create ssh key" do
+ it "creates ssh key" do
key_attrs = attributes_for :key
expect do
post api("/users/#{user.id}/keys", admin), key_attrs
end.to change{ user.keys.count }.by(1)
end
- it "should return 405 for invalid ID" do
- post api("/users/ASDF/keys", admin)
- expect(response).to have_http_status(405)
+ it "returns 400 for invalid ID" do
+ post api("/users/999999/keys", admin)
+ expect(response).to have_http_status(400)
end
end
@@ -408,20 +408,20 @@ describe API::API, api: true do
before { admin }
context 'when unauthenticated' do
- it 'should return authentication error' do
+ it 'returns authentication error' do
get api("/users/#{user.id}/keys")
expect(response).to have_http_status(401)
end
end
context 'when authenticated' do
- it 'should return 404 for non-existing user' do
+ it 'returns 404 for non-existing user' do
get api('/users/999999/keys', admin)
expect(response).to have_http_status(404)
expect(json_response['message']).to eq('404 User Not Found')
end
- it 'should return array of ssh keys' do
+ it 'returns array of ssh keys' do
user.keys << key
user.save
get api("/users/#{user.id}/keys", admin)
@@ -429,11 +429,6 @@ describe API::API, api: true do
expect(json_response).to be_an Array
expect(json_response.first['title']).to eq(key.title)
end
-
- it "should return 405 for invalid ID" do
- get api("/users/ASDF/keys", admin)
- expect(response).to have_http_status(405)
- end
end
end
@@ -441,14 +436,14 @@ describe API::API, api: true do
before { admin }
context 'when unauthenticated' do
- it 'should return authentication error' do
+ it 'returns authentication error' do
delete api("/users/#{user.id}/keys/42")
expect(response).to have_http_status(401)
end
end
context 'when authenticated' do
- it 'should delete existing key' do
+ it 'deletes existing key' do
user.keys << key
user.save
expect do
@@ -457,7 +452,7 @@ describe API::API, api: true do
expect(response).to have_http_status(200)
end
- it 'should return 404 error if user not found' do
+ it 'returns 404 error if user not found' do
user.keys << key
user.save
delete api("/users/999999/keys/#{key.id}", admin)
@@ -465,7 +460,7 @@ describe API::API, api: true do
expect(json_response['message']).to eq('404 User Not Found')
end
- it 'should return 404 error if key not foud' do
+ it 'returns 404 error if key not foud' do
delete api("/users/#{user.id}/keys/42", admin)
expect(response).to have_http_status(404)
expect(json_response['message']).to eq('404 Key Not Found')
@@ -476,22 +471,22 @@ describe API::API, api: true do
describe "POST /users/:id/emails" do
before { admin }
- it "should not create invalid email" do
+ it "does not create invalid email" do
post api("/users/#{user.id}/emails", admin), {}
expect(response).to have_http_status(400)
expect(json_response['message']).to eq('400 (Bad request) "email" not given')
end
- it "should create email" do
+ it "creates email" do
email_attrs = attributes_for :email
expect do
post api("/users/#{user.id}/emails", admin), email_attrs
end.to change{ user.emails.count }.by(1)
end
- it "should raise error for invalid ID" do
- post api("/users/ASDF/emails", admin)
- expect(response).to have_http_status(405)
+ it "raises error for invalid ID" do
+ post api("/users/999999/emails", admin)
+ expect(response).to have_http_status(400)
end
end
@@ -499,20 +494,20 @@ describe API::API, api: true do
before { admin }
context 'when unauthenticated' do
- it 'should return authentication error' do
+ it 'returns authentication error' do
get api("/users/#{user.id}/emails")
expect(response).to have_http_status(401)
end
end
context 'when authenticated' do
- it 'should return 404 for non-existing user' do
+ it 'returns 404 for non-existing user' do
get api('/users/999999/emails', admin)
expect(response).to have_http_status(404)
expect(json_response['message']).to eq('404 User Not Found')
end
- it 'should return array of emails' do
+ it 'returns array of emails' do
user.emails << email
user.save
get api("/users/#{user.id}/emails", admin)
@@ -521,7 +516,7 @@ describe API::API, api: true do
expect(json_response.first['email']).to eq(email.email)
end
- it "should raise error for invalid ID" do
+ it "raises error for invalid ID" do
put api("/users/ASDF/emails", admin)
expect(response).to have_http_status(405)
end
@@ -532,14 +527,14 @@ describe API::API, api: true do
before { admin }
context 'when unauthenticated' do
- it 'should return authentication error' do
+ it 'returns authentication error' do
delete api("/users/#{user.id}/emails/42")
expect(response).to have_http_status(401)
end
end
context 'when authenticated' do
- it 'should delete existing email' do
+ it 'deletes existing email' do
user.emails << email
user.save
expect do
@@ -548,7 +543,7 @@ describe API::API, api: true do
expect(response).to have_http_status(200)
end
- it 'should return 404 error if user not found' do
+ it 'returns 404 error if user not found' do
user.emails << email
user.save
delete api("/users/999999/emails/#{email.id}", admin)
@@ -556,51 +551,53 @@ describe API::API, api: true do
expect(json_response['message']).to eq('404 User Not Found')
end
- it 'should return 404 error if email not foud' do
+ it 'returns 404 error if email not foud' do
delete api("/users/#{user.id}/emails/42", admin)
expect(response).to have_http_status(404)
expect(json_response['message']).to eq('404 Email Not Found')
end
- it "should raise error for invalid ID" do
+ it "raises error for invalid ID" do
expect{delete api("/users/ASDF/emails/bar", admin) }.to raise_error(ActionController::RoutingError)
end
end
end
describe "DELETE /users/:id" do
+ let!(:namespace) { user.namespace }
before { admin }
- it "should delete user" do
+ it "deletes user" do
delete api("/users/#{user.id}", admin)
expect(response).to have_http_status(200)
expect { User.find(user.id) }.to raise_error ActiveRecord::RecordNotFound
+ expect { Namespace.find(namespace.id) }.to raise_error ActiveRecord::RecordNotFound
expect(json_response['email']).to eq(user.email)
end
- it "should not delete for unauthenticated user" do
+ it "does not delete for unauthenticated user" do
delete api("/users/#{user.id}")
expect(response).to have_http_status(401)
end
- it "shouldn't available for non admin users" do
+ it "is not available for non admin users" do
delete api("/users/#{user.id}", user)
expect(response).to have_http_status(403)
end
- it "should return 404 for non-existing user" do
+ it "returns 404 for non-existing user" do
delete api("/users/999999", admin)
expect(response).to have_http_status(404)
expect(json_response['message']).to eq('404 User Not Found')
end
- it "should raise error for invalid ID" do
+ it "raises error for invalid ID" do
expect{delete api("/users/ASDF", admin) }.to raise_error(ActionController::RoutingError)
end
end
describe "GET /user" do
- it "should return current user" do
+ it "returns current user" do
get api("/user", user)
expect(response).to have_http_status(200)
expect(json_response['email']).to eq(user.email)
@@ -610,7 +607,7 @@ describe API::API, api: true do
expect(json_response['projects_limit']).to eq(user.projects_limit)
end
- it "should return 401 error if user is unauthenticated" do
+ it "returns 401 error if user is unauthenticated" do
get api("/user")
expect(response).to have_http_status(401)
end
@@ -618,14 +615,14 @@ describe API::API, api: true do
describe "GET /user/keys" do
context "when unauthenticated" do
- it "should return authentication error" do
+ it "returns authentication error" do
get api("/user/keys")
expect(response).to have_http_status(401)
end
end
context "when authenticated" do
- it "should return array of ssh keys" do
+ it "returns array of ssh keys" do
user.keys << key
user.save
get api("/user/keys", user)
@@ -637,7 +634,7 @@ describe API::API, api: true do
end
describe "GET /user/keys/:id" do
- it "should return single key" do
+ it "returns single key" do
user.keys << key
user.save
get api("/user/keys/#{key.id}", user)
@@ -645,13 +642,13 @@ describe API::API, api: true do
expect(json_response["title"]).to eq(key.title)
end
- it "should return 404 Not Found within invalid ID" do
+ it "returns 404 Not Found within invalid ID" do
get api("/user/keys/42", user)
expect(response).to have_http_status(404)
expect(json_response['message']).to eq('404 Not found')
end
- it "should return 404 error if admin accesses user's ssh key" do
+ it "returns 404 error if admin accesses user's ssh key" do
user.keys << key
user.save
admin
@@ -660,14 +657,14 @@ describe API::API, api: true do
expect(json_response['message']).to eq('404 Not found')
end
- it "should return 404 for invalid ID" do
+ it "returns 404 for invalid ID" do
get api("/users/keys/ASDF", admin)
expect(response).to have_http_status(404)
end
end
describe "POST /user/keys" do
- it "should create ssh key" do
+ it "creates ssh key" do
key_attrs = attributes_for :key
expect do
post api("/user/keys", user), key_attrs
@@ -675,31 +672,31 @@ describe API::API, api: true do
expect(response).to have_http_status(201)
end
- it "should return a 401 error if unauthorized" do
+ it "returns a 401 error if unauthorized" do
post api("/user/keys"), title: 'some title', key: 'some key'
expect(response).to have_http_status(401)
end
- it "should not create ssh key without key" do
+ it "does not create ssh key without key" do
post api("/user/keys", user), title: 'title'
expect(response).to have_http_status(400)
expect(json_response['message']).to eq('400 (Bad request) "key" not given')
end
- it 'should not create ssh key without title' do
+ it 'does not create ssh key without title' do
post api('/user/keys', user), key: 'some key'
expect(response).to have_http_status(400)
expect(json_response['message']).to eq('400 (Bad request) "title" not given')
end
- it "should not create ssh key without title" do
+ it "does not create ssh key without title" do
post api("/user/keys", user), key: "somekey"
expect(response).to have_http_status(400)
end
end
describe "DELETE /user/keys/:id" do
- it "should delete existed key" do
+ it "deletes existed key" do
user.keys << key
user.save
expect do
@@ -708,33 +705,33 @@ describe API::API, api: true do
expect(response).to have_http_status(200)
end
- it "should return success if key ID not found" do
+ it "returns success if key ID not found" do
delete api("/user/keys/42", user)
expect(response).to have_http_status(200)
end
- it "should return 401 error if unauthorized" do
+ it "returns 401 error if unauthorized" do
user.keys << key
user.save
delete api("/user/keys/#{key.id}")
expect(response).to have_http_status(401)
end
- it "should raise error for invalid ID" do
+ it "raises error for invalid ID" do
expect{delete api("/users/keys/ASDF", admin) }.to raise_error(ActionController::RoutingError)
end
end
describe "GET /user/emails" do
context "when unauthenticated" do
- it "should return authentication error" do
+ it "returns authentication error" do
get api("/user/emails")
expect(response).to have_http_status(401)
end
end
context "when authenticated" do
- it "should return array of emails" do
+ it "returns array of emails" do
user.emails << email
user.save
get api("/user/emails", user)
@@ -746,7 +743,7 @@ describe API::API, api: true do
end
describe "GET /user/emails/:id" do
- it "should return single email" do
+ it "returns single email" do
user.emails << email
user.save
get api("/user/emails/#{email.id}", user)
@@ -754,13 +751,13 @@ describe API::API, api: true do
expect(json_response["email"]).to eq(email.email)
end
- it "should return 404 Not Found within invalid ID" do
+ it "returns 404 Not Found within invalid ID" do
get api("/user/emails/42", user)
expect(response).to have_http_status(404)
expect(json_response['message']).to eq('404 Not found')
end
- it "should return 404 error if admin accesses user's email" do
+ it "returns 404 error if admin accesses user's email" do
user.emails << email
user.save
admin
@@ -769,14 +766,14 @@ describe API::API, api: true do
expect(json_response['message']).to eq('404 Not found')
end
- it "should return 404 for invalid ID" do
+ it "returns 404 for invalid ID" do
get api("/users/emails/ASDF", admin)
expect(response).to have_http_status(404)
end
end
describe "POST /user/emails" do
- it "should create email" do
+ it "creates email" do
email_attrs = attributes_for :email
expect do
post api("/user/emails", user), email_attrs
@@ -784,12 +781,12 @@ describe API::API, api: true do
expect(response).to have_http_status(201)
end
- it "should return a 401 error if unauthorized" do
+ it "returns a 401 error if unauthorized" do
post api("/user/emails"), email: 'some email'
expect(response).to have_http_status(401)
end
- it "should not create email with invalid email" do
+ it "does not create email with invalid email" do
post api("/user/emails", user), {}
expect(response).to have_http_status(400)
expect(json_response['message']).to eq('400 (Bad request) "email" not given')
@@ -797,7 +794,7 @@ describe API::API, api: true do
end
describe "DELETE /user/emails/:id" do
- it "should delete existed email" do
+ it "deletes existed email" do
user.emails << email
user.save
expect do
@@ -806,44 +803,44 @@ describe API::API, api: true do
expect(response).to have_http_status(200)
end
- it "should return success if email ID not found" do
+ it "returns success if email ID not found" do
delete api("/user/emails/42", user)
expect(response).to have_http_status(200)
end
- it "should return 401 error if unauthorized" do
+ it "returns 401 error if unauthorized" do
user.emails << email
user.save
delete api("/user/emails/#{email.id}")
expect(response).to have_http_status(401)
end
- it "should raise error for invalid ID" do
+ it "raises error for invalid ID" do
expect{delete api("/users/emails/ASDF", admin) }.to raise_error(ActionController::RoutingError)
end
end
describe 'PUT /user/:id/block' do
before { admin }
- it 'should block existing user' do
+ it 'blocks existing user' do
put api("/users/#{user.id}/block", admin)
expect(response).to have_http_status(200)
expect(user.reload.state).to eq('blocked')
end
- it 'should not re-block ldap blocked users' do
+ it 'does not re-block ldap blocked users' do
put api("/users/#{ldap_blocked_user.id}/block", admin)
expect(response).to have_http_status(403)
expect(ldap_blocked_user.reload.state).to eq('ldap_blocked')
end
- it 'should not be available for non admin users' do
+ it 'does not be available for non admin users' do
put api("/users/#{user.id}/block", user)
expect(response).to have_http_status(403)
expect(user.reload.state).to eq('active')
end
- it 'should return a 404 error if user id not found' do
+ it 'returns a 404 error if user id not found' do
put api('/users/9999/block', admin)
expect(response).to have_http_status(404)
expect(json_response['message']).to eq('404 User Not Found')
@@ -854,37 +851,37 @@ describe API::API, api: true do
let(:blocked_user) { create(:user, state: 'blocked') }
before { admin }
- it 'should unblock existing user' do
+ it 'unblocks existing user' do
put api("/users/#{user.id}/unblock", admin)
expect(response).to have_http_status(200)
expect(user.reload.state).to eq('active')
end
- it 'should unblock a blocked user' do
+ it 'unblocks a blocked user' do
put api("/users/#{blocked_user.id}/unblock", admin)
expect(response).to have_http_status(200)
expect(blocked_user.reload.state).to eq('active')
end
- it 'should not unblock ldap blocked users' do
+ it 'does not unblock ldap blocked users' do
put api("/users/#{ldap_blocked_user.id}/unblock", admin)
expect(response).to have_http_status(403)
expect(ldap_blocked_user.reload.state).to eq('ldap_blocked')
end
- it 'should not be available for non admin users' do
+ it 'does not be available for non admin users' do
put api("/users/#{user.id}/unblock", user)
expect(response).to have_http_status(403)
expect(user.reload.state).to eq('active')
end
- it 'should return a 404 error if user id not found' do
+ it 'returns a 404 error if user id not found' do
put api('/users/9999/block', admin)
expect(response).to have_http_status(404)
expect(json_response['message']).to eq('404 User Not Found')
end
- it "should raise error for invalid ID" do
+ it "raises error for invalid ID" do
expect{put api("/users/ASDF/block", admin) }.to raise_error(ActionController::RoutingError)
end
end
diff --git a/spec/requests/api/variables_spec.rb b/spec/requests/api/variables_spec.rb
index ddba18245f8..05fbdb909dc 100644
--- a/spec/requests/api/variables_spec.rb
+++ b/spec/requests/api/variables_spec.rb
@@ -12,7 +12,7 @@ describe API::API, api: true do
describe 'GET /projects/:id/variables' do
context 'authorized user with proper permissions' do
- it 'should return project variables' do
+ it 'returns project variables' do
get api("/projects/#{project.id}/variables", user)
expect(response).to have_http_status(200)
@@ -21,7 +21,7 @@ describe API::API, api: true do
end
context 'authorized user with invalid permissions' do
- it 'should not return project variables' do
+ it 'does not return project variables' do
get api("/projects/#{project.id}/variables", user2)
expect(response).to have_http_status(403)
@@ -29,7 +29,7 @@ describe API::API, api: true do
end
context 'unauthorized user' do
- it 'should not return project variables' do
+ it 'does not return project variables' do
get api("/projects/#{project.id}/variables")
expect(response).to have_http_status(401)
@@ -39,14 +39,14 @@ describe API::API, api: true do
describe 'GET /projects/:id/variables/:key' do
context 'authorized user with proper permissions' do
- it 'should return project variable details' do
+ it 'returns project variable details' do
get api("/projects/#{project.id}/variables/#{variable.key}", user)
expect(response).to have_http_status(200)
expect(json_response['value']).to eq(variable.value)
end
- it 'should respond with 404 Not Found if requesting non-existing variable' do
+ it 'responds with 404 Not Found if requesting non-existing variable' do
get api("/projects/#{project.id}/variables/non_existing_variable", user)
expect(response).to have_http_status(404)
@@ -54,7 +54,7 @@ describe API::API, api: true do
end
context 'authorized user with invalid permissions' do
- it 'should not return project variable details' do
+ it 'does not return project variable details' do
get api("/projects/#{project.id}/variables/#{variable.key}", user2)
expect(response).to have_http_status(403)
@@ -62,7 +62,7 @@ describe API::API, api: true do
end
context 'unauthorized user' do
- it 'should not return project variable details' do
+ it 'does not return project variable details' do
get api("/projects/#{project.id}/variables/#{variable.key}")
expect(response).to have_http_status(401)
@@ -72,7 +72,7 @@ describe API::API, api: true do
describe 'POST /projects/:id/variables' do
context 'authorized user with proper permissions' do
- it 'should create variable' do
+ it 'creates variable' do
expect do
post api("/projects/#{project.id}/variables", user), key: 'TEST_VARIABLE_2', value: 'VALUE_2'
end.to change{project.variables.count}.by(1)
@@ -82,7 +82,7 @@ describe API::API, api: true do
expect(json_response['value']).to eq('VALUE_2')
end
- it 'should not allow to duplicate variable key' do
+ it 'does not allow to duplicate variable key' do
expect do
post api("/projects/#{project.id}/variables", user), key: variable.key, value: 'VALUE_2'
end.to change{project.variables.count}.by(0)
@@ -92,7 +92,7 @@ describe API::API, api: true do
end
context 'authorized user with invalid permissions' do
- it 'should not create variable' do
+ it 'does not create variable' do
post api("/projects/#{project.id}/variables", user2)
expect(response).to have_http_status(403)
@@ -100,7 +100,7 @@ describe API::API, api: true do
end
context 'unauthorized user' do
- it 'should not create variable' do
+ it 'does not create variable' do
post api("/projects/#{project.id}/variables")
expect(response).to have_http_status(401)
@@ -110,7 +110,7 @@ describe API::API, api: true do
describe 'PUT /projects/:id/variables/:key' do
context 'authorized user with proper permissions' do
- it 'should update variable data' do
+ it 'updates variable data' do
initial_variable = project.variables.first
value_before = initial_variable.value
@@ -123,7 +123,7 @@ describe API::API, api: true do
expect(updated_variable.value).to eq('VALUE_1_UP')
end
- it 'should responde with 404 Not Found if requesting non-existing variable' do
+ it 'responds with 404 Not Found if requesting non-existing variable' do
put api("/projects/#{project.id}/variables/non_existing_variable", user)
expect(response).to have_http_status(404)
@@ -131,7 +131,7 @@ describe API::API, api: true do
end
context 'authorized user with invalid permissions' do
- it 'should not update variable' do
+ it 'does not update variable' do
put api("/projects/#{project.id}/variables/#{variable.key}", user2)
expect(response).to have_http_status(403)
@@ -139,7 +139,7 @@ describe API::API, api: true do
end
context 'unauthorized user' do
- it 'should not update variable' do
+ it 'does not update variable' do
put api("/projects/#{project.id}/variables/#{variable.key}")
expect(response).to have_http_status(401)
@@ -149,14 +149,14 @@ describe API::API, api: true do
describe 'DELETE /projects/:id/variables/:key' do
context 'authorized user with proper permissions' do
- it 'should delete variable' do
+ it 'deletes variable' do
expect do
delete api("/projects/#{project.id}/variables/#{variable.key}", user)
end.to change{project.variables.count}.by(-1)
expect(response).to have_http_status(200)
end
- it 'should responde with 404 Not Found if requesting non-existing variable' do
+ it 'responds with 404 Not Found if requesting non-existing variable' do
delete api("/projects/#{project.id}/variables/non_existing_variable", user)
expect(response).to have_http_status(404)
@@ -164,7 +164,7 @@ describe API::API, api: true do
end
context 'authorized user with invalid permissions' do
- it 'should not delete variable' do
+ it 'does not delete variable' do
delete api("/projects/#{project.id}/variables/#{variable.key}", user2)
expect(response).to have_http_status(403)
@@ -172,7 +172,7 @@ describe API::API, api: true do
end
context 'unauthorized user' do
- it 'should not delete variable' do
+ it 'does not delete variable' do
delete api("/projects/#{project.id}/variables/#{variable.key}")
expect(response).to have_http_status(401)
diff --git a/spec/requests/ci/api/builds_spec.rb b/spec/requests/ci/api/builds_spec.rb
index e7cbc3dd3a7..ca7932dc5da 100644
--- a/spec/requests/ci/api/builds_spec.rb
+++ b/spec/requests/ci/api/builds_spec.rb
@@ -6,112 +6,102 @@ describe Ci::API::API do
let(:runner) { FactoryGirl.create(:ci_runner, tag_list: ["mysql", "ruby"]) }
let(:project) { FactoryGirl.create(:empty_project) }
- before do
- stub_ci_pipeline_to_return_yaml_file
- end
-
describe "Builds API for runners" do
- let(:shared_runner) { FactoryGirl.create(:ci_runner, token: "SharedRunner") }
- let(:shared_project) { FactoryGirl.create(:empty_project, name: "SharedProject") }
+ let(:pipeline) { create(:ci_pipeline_without_jobs, project: project, ref: 'master') }
before do
- FactoryGirl.create :ci_runner_project, project: project, runner: runner
+ project.runners << runner
end
describe "POST /builds/register" do
- it "should start a build" do
- pipeline = FactoryGirl.create(:ci_pipeline, project: project, ref: 'master')
- pipeline.create_builds(nil)
- build = pipeline.builds.first
+ let!(:build) { create(:ci_build, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) }
- post ci_api("/builds/register"), token: runner.token, info: { platform: :darwin }
+ it "starts a build" do
+ register_builds info: { platform: :darwin }
expect(response).to have_http_status(201)
expect(json_response['sha']).to eq(build.sha)
expect(runner.reload.platform).to eq("darwin")
+ expect(json_response["options"]).to eq({ "image" => "ruby:2.1", "services" => ["postgres"] })
+ expect(json_response["variables"]).to include(
+ { "key" => "CI_BUILD_NAME", "value" => "spinach", "public" => true },
+ { "key" => "CI_BUILD_STAGE", "value" => "test", "public" => true },
+ { "key" => "DB_NAME", "value" => "postgres", "public" => true }
+ )
end
- it "should return 404 error if no pending build found" do
- post ci_api("/builds/register"), token: runner.token
-
- expect(response).to have_http_status(404)
- end
-
- it "should return 404 error if no builds for specific runner" do
- pipeline = FactoryGirl.create(:ci_pipeline, project: shared_project)
- FactoryGirl.create(:ci_build, pipeline: pipeline, status: 'pending')
+ context 'when builds are finished' do
+ before do
+ build.success
+ end
- post ci_api("/builds/register"), token: runner.token
+ it "returns 404 error if no builds for specific runner" do
+ register_builds
- expect(response).to have_http_status(404)
+ expect(response).to have_http_status(404)
+ end
end
- it "should return 404 error if no builds for shared runner" do
- pipeline = FactoryGirl.create(:ci_pipeline, project: project)
- FactoryGirl.create(:ci_build, pipeline: pipeline, status: 'pending')
+ context 'for other project with builds' do
+ before do
+ build.success
+ create(:ci_build, :pending)
+ end
- post ci_api("/builds/register"), token: shared_runner.token
+ it "returns 404 error if no builds for shared runner" do
+ register_builds
- expect(response).to have_http_status(404)
+ expect(response).to have_http_status(404)
+ end
end
- it "returns options" do
- pipeline = FactoryGirl.create(:ci_pipeline, project: project, ref: 'master')
- pipeline.create_builds(nil)
+ context 'for shared runner' do
+ let(:shared_runner) { create(:ci_runner, token: "SharedRunner") }
- post ci_api("/builds/register"), token: runner.token, info: { platform: :darwin }
+ it "should return 404 error if no builds for shared runner" do
+ register_builds shared_runner.token
- expect(response).to have_http_status(201)
- expect(json_response["options"]).to eq({ "image" => "ruby:2.1", "services" => ["postgres"] })
+ expect(response).to have_http_status(404)
+ end
end
- it "returns variables" do
- 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 }
+ context 'for triggered build' do
+ before do
+ trigger = create(:ci_trigger, project: project)
+ create(:ci_trigger_request_with_variables, pipeline: pipeline, builds: [build], trigger: trigger)
+ project.variables << Ci::Variable.new(key: "SECRET_KEY", value: "secret_value")
+ end
- expect(response).to have_http_status(201)
- expect(json_response["variables"]).to eq([
- { "key" => "CI_BUILD_NAME", "value" => "spinach", "public" => true },
- { "key" => "CI_BUILD_STAGE", "value" => "test", "public" => true },
- { "key" => "DB_NAME", "value" => "postgres", "public" => true },
- { "key" => "SECRET_KEY", "value" => "secret_value", "public" => false }
- ])
+ it "returns variables for triggers" do
+ register_builds info: { platform: :darwin }
+
+ expect(response).to have_http_status(201)
+ expect(json_response["variables"]).to include(
+ { "key" => "CI_BUILD_NAME", "value" => "spinach", "public" => true },
+ { "key" => "CI_BUILD_STAGE", "value" => "test", "public" => true },
+ { "key" => "CI_BUILD_TRIGGERED", "value" => "true", "public" => true },
+ { "key" => "DB_NAME", "value" => "postgres", "public" => true },
+ { "key" => "SECRET_KEY", "value" => "secret_value", "public" => false },
+ { "key" => "TRIGGER_KEY_1", "value" => "TRIGGER_VALUE_1", "public" => false },
+ )
+ end
end
- it "returns variables for triggers" do
- trigger = FactoryGirl.create(:ci_trigger, project: project)
- pipeline = FactoryGirl.create(:ci_pipeline, project: project, ref: 'master')
-
- 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 }
-
- expect(response).to have_http_status(201)
- expect(json_response["variables"]).to eq([
- { "key" => "CI_BUILD_NAME", "value" => "spinach", "public" => true },
- { "key" => "CI_BUILD_STAGE", "value" => "test", "public" => true },
- { "key" => "CI_BUILD_TRIGGERED", "value" => "true", "public" => true },
- { "key" => "DB_NAME", "value" => "postgres", "public" => true },
- { "key" => "SECRET_KEY", "value" => "secret_value", "public" => false },
- { "key" => "TRIGGER_KEY", "value" => "TRIGGER_VALUE", "public" => false },
- ])
- end
+ context 'with multiple builds' do
+ before do
+ build.success
+ end
- it "returns dependent builds" do
- pipeline = FactoryGirl.create(:ci_pipeline, project: project, ref: 'master')
- pipeline.create_builds(nil, nil)
- pipeline.builds.where(stage: 'test').each(&:success)
+ let!(:test_build) { create(:ci_build, pipeline: pipeline, name: 'deploy', stage: 'deploy', stage_idx: 1) }
- post ci_api("/builds/register"), token: runner.token, info: { platform: :darwin }
+ it "returns dependent builds" do
+ register_builds info: { platform: :darwin }
- expect(response).to have_http_status(201)
- expect(json_response["depends_on_builds"].count).to eq(2)
- expect(json_response["depends_on_builds"][0]["name"]).to eq("rspec")
+ expect(response).to have_http_status(201)
+ expect(json_response["id"]).to eq(test_build.id)
+ expect(json_response["depends_on_builds"].count).to eq(1)
+ expect(json_response["depends_on_builds"][0]).to include('id' => build.id, 'name' => 'spinach')
+ end
end
%w(name version revision platform architecture).each do |param|
@@ -121,8 +111,9 @@ describe Ci::API::API do
subject { runner.read_attribute(param.to_sym) }
it do
- post ci_api("/builds/register"), token: runner.token, info: { param => value }
- expect(response).to have_http_status(404)
+ register_builds info: { param => value }
+
+ expect(response).to have_http_status(201)
runner.reload
is_expected.to eq(value)
end
@@ -131,8 +122,7 @@ describe Ci::API::API do
context 'when build has no tags' do
before do
- pipeline = create(:ci_pipeline, project: project)
- create(:ci_build, pipeline: pipeline, tags: [])
+ build.update(tags: [])
end
context 'when runner is allowed to pick untagged builds' do
@@ -154,42 +144,40 @@ describe Ci::API::API do
expect(response).to have_http_status 404
end
end
+ end
- def register_builds
- post ci_api("/builds/register"), token: runner.token,
- info: { platform: :darwin }
- end
+ def register_builds(token = runner.token, **params)
+ post ci_api("/builds/register"), params.merge(token: token)
end
end
describe "PUT /builds/:id" do
- let(:pipeline) {create(:ci_pipeline, project: project)}
- let(:build) { create(:ci_build, :trace, pipeline: pipeline, runner_id: runner.id) }
+ let(:build) { create(:ci_build, :pending, :trace, pipeline: pipeline, runner_id: runner.id) }
before do
build.run!
put ci_api("/builds/#{build.id}"), token: runner.token
end
- it "should update a running build" do
+ it "updates a running build" do
expect(response).to have_http_status(200)
end
- it 'should not override trace information when no trace is given' do
+ it 'does not override trace information when no trace is given' do
expect(build.reload.trace).to eq 'BUILD TRACE'
end
context 'build has been erased' do
let(:build) { create(:ci_build, runner_id: runner.id, erased_at: Time.now) }
- it 'should respond with forbidden' do
+ it 'responds with forbidden' do
expect(response.status).to eq 403
end
end
end
describe 'PATCH /builds/:id/trace.txt' do
- let(:build) { create(:ci_build, :trace, runner_id: runner.id) }
+ let(:build) { create(:ci_build, :pending, :trace, runner_id: runner.id) }
let(:headers) { { Ci::API::Helpers::BUILD_TOKEN_HEADER => build.token, 'Content-Type' => 'text/plain' } }
let(:headers_with_range) { headers.merge({ 'Content-Range' => '11-20' }) }
@@ -237,8 +225,7 @@ 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(:pipeline) { create(:ci_pipeline, project: project) }
- let(:build) { create(:ci_build, pipeline: pipeline, runner_id: runner.id) }
+ let(:build) { create(:ci_build, :pending, 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") }
@@ -280,7 +267,7 @@ describe Ci::API::API do
context 'authorization token is invalid' do
before { post authorize_url, { token: 'invalid', filesize: 100 } }
- it 'should respond with forbidden' do
+ it 'responds with forbidden' do
expect(response).to have_http_status(403)
end
end
@@ -300,7 +287,7 @@ describe Ci::API::API do
upload_artifacts(file_upload, headers_with_token)
end
- it 'should respond with forbidden' do
+ it 'responds with forbidden' do
expect(response.status).to eq 403
end
end
@@ -342,7 +329,7 @@ describe Ci::API::API do
end
end
- context 'should post artifacts file and metadata file' do
+ context 'posts artifacts file and metadata file' do
let!(:artifacts) { file_upload }
let!(:metadata) { file_upload2 }
@@ -354,7 +341,7 @@ describe Ci::API::API do
post(post_url, post_data, headers_with_token)
end
- context 'post data accelerated by workhorse is correct' do
+ context 'posts data accelerated by workhorse is correct' do
let(:post_data) do
{ 'file.path' => artifacts.path,
'file.name' => artifacts.original_filename,
@@ -422,7 +409,7 @@ describe Ci::API::API do
end
context "artifacts file is too large" do
- it "should fail to post too large artifact" do
+ it "fails to post too large artifact" do
stub_application_setting(max_artifacts_size: 0)
upload_artifacts(file_upload, headers_with_token)
expect(response).to have_http_status(413)
@@ -430,14 +417,14 @@ describe Ci::API::API do
end
context "artifacts post request does not contain file" do
- it "should fail to post artifacts without file" do
+ it "fails to post artifacts without file" do
post post_url, {}, headers_with_token
expect(response).to have_http_status(400)
end
end
context 'GitLab Workhorse is not configured' do
- it "should fail to post artifacts without GitLab-Workhorse" do
+ it "fails to post artifacts without GitLab-Workhorse" do
post post_url, { token: build.token }, {}
expect(response).to have_http_status(403)
end
@@ -456,7 +443,7 @@ describe Ci::API::API do
FileUtils.remove_entry @tmpdir
end
- it "should fail to post artifacts for outside of tmp path" do
+ it "fails to post artifacts for outside of tmp path" do
upload_artifacts(file_upload, headers_with_token)
expect(response).to have_http_status(400)
end
@@ -482,7 +469,7 @@ describe Ci::API::API do
build.reload
end
- it 'should remove build artifacts' do
+ it 'removes build artifacts' do
expect(response).to have_http_status(200)
expect(build.artifacts_file.exists?).to be_falsy
expect(build.artifacts_metadata.exists?).to be_falsy
@@ -500,14 +487,14 @@ describe Ci::API::API do
'Content-Disposition' => 'attachment; filename=ci_build_artifacts.zip' }
end
- it 'should download artifact' do
+ it 'downloads artifact' do
expect(response).to have_http_status(200)
expect(response.headers).to include download_headers
end
end
context 'build does not has artifacts' do
- it 'should respond with not found' do
+ it 'responds with not found' do
expect(response).to have_http_status(404)
end
end
diff --git a/spec/requests/ci/api/triggers_spec.rb b/spec/requests/ci/api/triggers_spec.rb
index f12678e5a8e..0a0f979f57d 100644
--- a/spec/requests/ci/api/triggers_spec.rb
+++ b/spec/requests/ci/api/triggers_spec.rb
@@ -19,17 +19,17 @@ describe Ci::API::API do
end
context 'Handles errors' do
- it 'should return bad request if token is missing' do
+ it 'returns bad request if token is missing' do
post ci_api("/projects/#{project.ci_id}/refs/master/trigger")
expect(response).to have_http_status(400)
end
- it 'should return not found if project is not found' do
+ it 'returns not found if project is not found' do
post ci_api('/projects/0/refs/master/trigger'), options
expect(response).to have_http_status(404)
end
- it 'should return unauthorized if token is for different project' do
+ it 'returns unauthorized if token is for different project' do
post ci_api("/projects/#{project2.ci_id}/refs/master/trigger"), options
expect(response).to have_http_status(401)
end
@@ -38,14 +38,15 @@ describe Ci::API::API do
context 'Have a commit' do
let(:pipeline) { project.pipelines.last }
- it 'should create builds' do
+ it 'creates builds' do
post ci_api("/projects/#{project.ci_id}/refs/master/trigger"), options
expect(response).to have_http_status(201)
pipeline.builds.reload
- expect(pipeline.builds.size).to eq(2)
+ expect(pipeline.builds.pending.size).to eq(2)
+ expect(pipeline.builds.size).to eq(5)
end
- it 'should return bad request with no builds created if there\'s no commit for that ref' do
+ it 'returns bad request with no builds created if there\'s no commit for that ref' do
post ci_api("/projects/#{project.ci_id}/refs/other-branch/trigger"), options
expect(response).to have_http_status(400)
expect(json_response['message']).to eq('No builds created')
@@ -56,19 +57,19 @@ describe Ci::API::API do
{ 'TRIGGER_KEY' => 'TRIGGER_VALUE' }
end
- it 'should validate variables to be a hash' do
+ it 'validates variables to be a hash' do
post ci_api("/projects/#{project.ci_id}/refs/master/trigger"), options.merge(variables: 'value')
expect(response).to have_http_status(400)
expect(json_response['message']).to eq('variables needs to be a hash')
end
- it 'should validate variables needs to be a map of key-valued strings' do
+ it 'validates variables needs to be a map of key-valued strings' do
post ci_api("/projects/#{project.ci_id}/refs/master/trigger"), options.merge(variables: { key: %w(1 2) })
expect(response).to have_http_status(400)
expect(json_response['message']).to eq('variables needs to be a map of key-valued strings')
end
- it 'create trigger request with variables' do
+ it 'creates trigger request with variables' do
post ci_api("/projects/#{project.ci_id}/refs/master/trigger"), options.merge(variables: variables)
expect(response).to have_http_status(201)
pipeline.builds.reload
diff --git a/spec/requests/git_http_spec.rb b/spec/requests/git_http_spec.rb
index 82ab582beac..afaf4b7cefb 100644
--- a/spec/requests/git_http_spec.rb
+++ b/spec/requests/git_http_spec.rb
@@ -75,9 +75,9 @@ describe 'Git HTTP requests', lib: true do
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
+ it "uploads get status 403" do
upload(path, env) do |response|
- expect(response).to have_http_status(200)
+ expect(response).to have_http_status(403)
end
end
@@ -86,7 +86,7 @@ describe 'Git HTTP requests', lib: true do
allow(Gitlab.config.gitlab_shell).to receive(:receive_pack).and_return(false)
upload(path, env) do |response|
- expect(response).to have_http_status(404)
+ expect(response).to have_http_status(403)
end
end
end
@@ -198,6 +198,45 @@ describe 'Git HTTP requests', lib: true do
end
end
+ context 'when user has 2FA enabled' do
+ let(:user) { create(:user, :two_factor) }
+ let(:access_token) { create(:personal_access_token, user: user) }
+
+ before do
+ project.team << [user, :master]
+ end
+
+ context 'when username and password are provided' do
+ it 'rejects the clone attempt' do
+ download("#{project.path_with_namespace}.git", user: user.username, password: user.password) do |response|
+ expect(response).to have_http_status(401)
+ expect(response.body).to include('You have 2FA enabled, please use a personal access token for Git over HTTP')
+ end
+ end
+
+ it 'rejects the push attempt' do
+ upload("#{project.path_with_namespace}.git", user: user.username, password: user.password) do |response|
+ expect(response).to have_http_status(401)
+ expect(response.body).to include('You have 2FA enabled, please use a personal access token for Git over HTTP')
+ end
+ end
+ end
+
+ context 'when username and personal access token are provided' do
+ it 'allows clones' do
+ download("#{project.path_with_namespace}.git", user: user.username, password: access_token.token) do |response|
+ expect(response).to have_http_status(200)
+ end
+ end
+
+ it 'allows pushes' do
+ upload("#{project.path_with_namespace}.git", user: user.username, password: access_token.token) do |response|
+ expect(response).to have_http_status(200)
+ end
+ end
+ end
+ end
+
context "when blank password attempts follow a valid login" do
def attempt_login(include_password)
password = include_password ? user.password : ""
@@ -236,9 +275,9 @@ describe 'Git HTTP requests', lib: true do
end
end
- it "uploads get status 200 (because Git hooks do the real check)" do
+ it "uploads get status 404" do
upload(path, user: user.username, password: user.password) do |response|
- expect(response).to have_http_status(200)
+ expect(response).to have_http_status(404)
end
end
end
@@ -349,19 +388,19 @@ describe 'Git HTTP requests', lib: true do
end
end
- def clone_get(project, options={})
+ def clone_get(project, options = {})
get "/#{project}/info/refs", { service: 'git-upload-pack' }, auth_env(*options.values_at(:user, :password, :spnego_request_token))
end
- def clone_post(project, options={})
+ def clone_post(project, options = {})
post "/#{project}/git-upload-pack", {}, auth_env(*options.values_at(:user, :password, :spnego_request_token))
end
- def push_get(project, options={})
+ def push_get(project, options = {})
get "/#{project}/info/refs", { service: 'git-receive-pack' }, auth_env(*options.values_at(:user, :password, :spnego_request_token))
end
- def push_post(project, options={})
+ def push_post(project, options = {})
post "/#{project}/git-receive-pack", {}, auth_env(*options.values_at(:user, :password, :spnego_request_token))
end
diff --git a/spec/requests/lfs_http_spec.rb b/spec/requests/lfs_http_spec.rb
new file mode 100644
index 00000000000..4c9b4a8ba42
--- /dev/null
+++ b/spec/requests/lfs_http_spec.rb
@@ -0,0 +1,797 @@
+require 'spec_helper'
+
+describe 'Git LFS API and storage' do
+ let(:user) { create(:user) }
+ let!(:lfs_object) { create(:lfs_object, :with_file) }
+
+ let(:headers) do
+ {
+ 'Authorization' => authorization,
+ 'X-Sendfile-Type' => sendfile
+ }.compact
+ end
+ let(:authorization) { }
+ let(:sendfile) { }
+
+ let(:sample_oid) { lfs_object.oid }
+ let(:sample_size) { lfs_object.size }
+
+ describe 'when lfs is disabled' do
+ let(:project) { create(:empty_project) }
+ let(:body) do
+ {
+ 'objects' => [
+ { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897',
+ 'size' => 1575078
+ },
+ { 'oid' => sample_oid,
+ 'size' => sample_size
+ }
+ ],
+ 'operation' => 'upload'
+ }
+ end
+ let(:authorization) { authorize_user }
+
+ before do
+ allow(Gitlab.config.lfs).to receive(:enabled).and_return(false)
+ post_lfs_json "#{project.http_url_to_repo}/info/lfs/objects/batch", body, headers
+ end
+
+ it 'responds with 501' do
+ expect(response).to have_http_status(501)
+ expect(json_response).to include('message' => 'Git LFS is not enabled on this GitLab server, contact your admin.')
+ end
+ end
+
+ describe 'deprecated API' do
+ let(:project) { create(:empty_project) }
+
+ before do
+ enable_lfs
+ end
+
+ shared_examples 'a deprecated' do
+ it 'responds with 501' do
+ expect(response).to have_http_status(501)
+ end
+
+ it 'returns deprecated message' do
+ expect(json_response).to include('message' => 'Server supports batch API only, please update your Git LFS client to version 1.0.1 and up.')
+ end
+ end
+
+ context 'when fetching lfs object using deprecated API' do
+ let(:authorization) { authorize_user }
+
+ before do
+ get "#{project.http_url_to_repo}/info/lfs/objects/#{sample_oid}", nil, headers
+ end
+
+ it_behaves_like 'a deprecated'
+ end
+
+ context 'when handling lfs request using deprecated API' do
+ let(:authorization) { authorize_user }
+ before do
+ post_lfs_json "#{project.http_url_to_repo}/info/lfs/objects", nil, headers
+ end
+
+ it_behaves_like 'a deprecated'
+ end
+ end
+
+ describe 'when fetching lfs object' do
+ let(:project) { create(:empty_project) }
+ let(:update_permissions) { }
+
+ before do
+ enable_lfs
+ update_permissions
+ get "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}", nil, headers
+ end
+
+ context 'and request comes from gitlab-workhorse' do
+ context 'without user being authorized' do
+ it 'responds with status 401' do
+ expect(response).to have_http_status(401)
+ end
+ end
+
+ context 'with required headers' do
+ shared_examples 'responds with a file' do
+ let(:sendfile) { 'X-Sendfile' }
+
+ it 'responds with status 200' do
+ expect(response).to have_http_status(200)
+ end
+
+ it 'responds with the file location' do
+ expect(response.headers['Content-Type']).to eq('application/octet-stream')
+ expect(response.headers['X-Sendfile']).to eq(lfs_object.file.path)
+ end
+ end
+
+ context 'with user is authorized' do
+ let(:authorization) { authorize_user }
+
+ context 'and does not have project access' do
+ let(:update_permissions) do
+ project.lfs_objects << lfs_object
+ end
+
+ it 'responds with status 404' do
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ context 'and does have project access' do
+ let(:update_permissions) do
+ project.team << [user, :master]
+ project.lfs_objects << lfs_object
+ end
+
+ it_behaves_like 'responds with a file'
+ end
+ end
+
+ context 'when CI is authorized' do
+ let(:authorization) { authorize_ci_project }
+
+ let(:update_permissions) do
+ project.lfs_objects << lfs_object
+ end
+
+ it_behaves_like 'responds with a file'
+ end
+ end
+
+ context 'without required headers' do
+ let(:authorization) { authorize_user }
+
+ it 'responds with status 404' do
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
+ end
+
+ describe 'when handling lfs batch request' do
+ let(:update_lfs_permissions) { }
+ let(:update_user_permissions) { }
+
+ before do
+ enable_lfs
+ update_lfs_permissions
+ update_user_permissions
+ post_lfs_json "#{project.http_url_to_repo}/info/lfs/objects/batch", body, headers
+ end
+
+ describe 'download' do
+ let(:project) { create(:empty_project) }
+ let(:body) do
+ { 'operation' => 'download',
+ 'objects' => [
+ { 'oid' => sample_oid,
+ 'size' => sample_size
+ }]
+ }
+ end
+
+ shared_examples 'an authorized requests' do
+ context 'when downloading an lfs object that is assigned to our project' do
+ let(:update_lfs_permissions) do
+ project.lfs_objects << lfs_object
+ end
+
+ it 'responds with status 200' do
+ expect(response).to have_http_status(200)
+ end
+
+ it 'with href to download' do
+ expect(json_response).to eq('objects' => [
+ { 'oid' => sample_oid,
+ 'size' => sample_size,
+ 'actions' => {
+ 'download' => {
+ 'href' => "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}",
+ 'header' => { 'Authorization' => authorization }
+ }
+ }
+ }])
+ end
+ end
+
+ context 'when downloading an lfs object that is assigned to other project' do
+ let(:other_project) { create(:empty_project) }
+ let(:update_lfs_permissions) do
+ other_project.lfs_objects << lfs_object
+ end
+
+ it 'responds with status 200' do
+ expect(response).to have_http_status(200)
+ end
+
+ it 'with href to download' do
+ expect(json_response).to eq('objects' => [
+ { 'oid' => sample_oid,
+ 'size' => sample_size,
+ 'error' => {
+ 'code' => 404,
+ 'message' => "Object does not exist on the server or you don't have permissions to access it",
+ }
+ }])
+ end
+ end
+
+ context 'when downloading a lfs object that does not exist' do
+ let(:body) do
+ { 'operation' => 'download',
+ 'objects' => [
+ { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897',
+ 'size' => 1575078
+ }]
+ }
+ end
+
+ it 'responds with status 200' do
+ expect(response).to have_http_status(200)
+ end
+
+ it 'with an 404 for specific object' do
+ expect(json_response).to eq('objects' => [
+ { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897',
+ 'size' => 1575078,
+ 'error' => {
+ 'code' => 404,
+ 'message' => "Object does not exist on the server or you don't have permissions to access it",
+ }
+ }])
+ end
+ end
+
+ context 'when downloading one new and one existing lfs object' do
+ let(:body) do
+ { 'operation' => 'download',
+ 'objects' => [
+ { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897',
+ 'size' => 1575078
+ },
+ { 'oid' => sample_oid,
+ 'size' => sample_size
+ }
+ ]
+ }
+ end
+
+ let(:update_lfs_permissions) do
+ project.lfs_objects << lfs_object
+ end
+
+ it 'responds with status 200' do
+ expect(response).to have_http_status(200)
+ end
+
+ it 'responds with upload hypermedia link for the new object' do
+ expect(json_response).to eq('objects' => [
+ { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897',
+ 'size' => 1575078,
+ 'error' => {
+ 'code' => 404,
+ 'message' => "Object does not exist on the server or you don't have permissions to access it",
+ }
+ },
+ { 'oid' => sample_oid,
+ 'size' => sample_size,
+ 'actions' => {
+ 'download' => {
+ 'href' => "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}",
+ 'header' => { 'Authorization' => authorization }
+ }
+ }
+ }])
+ end
+ end
+ end
+
+ context 'when user is authenticated' do
+ let(:authorization) { authorize_user }
+
+ let(:update_user_permissions) do
+ project.team << [user, role]
+ end
+
+ it_behaves_like 'an authorized requests' do
+ let(:role) { :reporter }
+ end
+
+ context 'when user does is not member of the project' do
+ let(:update_user_permissions) { nil }
+
+ it 'responds with 404' do
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ context 'when user does not have download access' do
+ let(:role) { :guest }
+
+ it 'responds with 403' do
+ expect(response).to have_http_status(403)
+ end
+ end
+ end
+
+ context 'when CI is authorized' do
+ let(:authorization) { authorize_ci_project }
+
+ it_behaves_like 'an authorized requests'
+ end
+
+ context 'when user is not authenticated' do
+ describe 'is accessing public project' do
+ let(:project) { create(:project, :public) }
+
+ let(:update_lfs_permissions) do
+ project.lfs_objects << lfs_object
+ end
+
+ it 'responds with status 200 and href to download' do
+ expect(response).to have_http_status(200)
+ end
+
+ it 'responds with status 200 and href to download' do
+ expect(json_response).to eq('objects' => [
+ { 'oid' => sample_oid,
+ 'size' => sample_size,
+ 'actions' => {
+ 'download' => {
+ 'href' => "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}",
+ 'header' => {}
+ }
+ }
+ }])
+ end
+ end
+
+ describe 'is accessing non-public project' do
+ let(:update_lfs_permissions) do
+ project.lfs_objects << lfs_object
+ end
+
+ it 'responds with authorization required' do
+ expect(response).to have_http_status(401)
+ end
+ end
+ end
+ end
+
+ describe 'upload' do
+ let(:project) { create(:project, :public) }
+ let(:body) do
+ { 'operation' => 'upload',
+ 'objects' => [
+ { 'oid' => sample_oid,
+ 'size' => sample_size
+ }]
+ }
+ end
+
+ describe 'when request is authenticated' do
+ describe 'when user has project push access' do
+ let(:authorization) { authorize_user }
+
+ let(:update_user_permissions) do
+ project.team << [user, :developer]
+ end
+
+ context 'when pushing an lfs object that already exists' do
+ let(:other_project) { create(:empty_project) }
+ let(:update_lfs_permissions) do
+ other_project.lfs_objects << lfs_object
+ end
+
+ it 'responds with status 200' do
+ expect(response).to have_http_status(200)
+ end
+
+ it 'responds with links the object to the project' do
+ expect(json_response['objects']).to be_kind_of(Array)
+ expect(json_response['objects'].first['oid']).to eq(sample_oid)
+ expect(json_response['objects'].first['size']).to eq(sample_size)
+ expect(lfs_object.projects.pluck(:id)).not_to include(project.id)
+ expect(lfs_object.projects.pluck(:id)).to include(other_project.id)
+ expect(json_response['objects'].first['actions']['upload']['href']).to eq("#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}/#{sample_size}")
+ expect(json_response['objects'].first['actions']['upload']['header']).to eq('Authorization' => authorization)
+ end
+ end
+
+ context 'when pushing a lfs object that does not exist' do
+ let(:body) do
+ { 'operation' => 'upload',
+ 'objects' => [
+ { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897',
+ 'size' => 1575078
+ }]
+ }
+ end
+
+ it 'responds with status 200' do
+ expect(response).to have_http_status(200)
+ end
+
+ it 'responds with upload hypermedia link' do
+ expect(json_response['objects']).to be_kind_of(Array)
+ expect(json_response['objects'].first['oid']).to eq("91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897")
+ expect(json_response['objects'].first['size']).to eq(1575078)
+ expect(json_response['objects'].first['actions']['upload']['href']).to eq("#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}.git/gitlab-lfs/objects/91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897/1575078")
+ expect(json_response['objects'].first['actions']['upload']['header']).to eq('Authorization' => authorization)
+ end
+ end
+
+ context 'when pushing one new and one existing lfs object' do
+ let(:body) do
+ { 'operation' => 'upload',
+ 'objects' => [
+ { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897',
+ 'size' => 1575078
+ },
+ { 'oid' => sample_oid,
+ 'size' => sample_size
+ }
+ ]
+ }
+ end
+
+ let(:update_lfs_permissions) do
+ project.lfs_objects << lfs_object
+ end
+
+ it 'responds with status 200' do
+ expect(response).to have_http_status(200)
+ end
+
+ it 'responds with upload hypermedia link for the new object' do
+ expect(json_response['objects']).to be_kind_of(Array)
+
+ expect(json_response['objects'].first['oid']).to eq("91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897")
+ expect(json_response['objects'].first['size']).to eq(1575078)
+ expect(json_response['objects'].first['actions']['upload']['href']).to eq("#{project.http_url_to_repo}/gitlab-lfs/objects/91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897/1575078")
+ expect(json_response['objects'].first['actions']['upload']['header']).to eq("Authorization" => authorization)
+
+ expect(json_response['objects'].last['oid']).to eq(sample_oid)
+ expect(json_response['objects'].last['size']).to eq(sample_size)
+ expect(json_response['objects'].last).not_to have_key('actions')
+ end
+ end
+ end
+
+ context 'when user does not have push access' do
+ let(:authorization) { authorize_user }
+
+ it 'responds with 403' do
+ expect(response).to have_http_status(403)
+ end
+ end
+
+ context 'when CI is authorized' do
+ let(:authorization) { authorize_ci_project }
+
+ it 'responds with 401' do
+ expect(response).to have_http_status(401)
+ end
+ end
+ end
+
+ context 'when user is not authenticated' do
+ context 'when user has push access' do
+ let(:update_user_permissions) do
+ project.team << [user, :master]
+ end
+
+ it 'responds with status 401' do
+ expect(response).to have_http_status(401)
+ end
+ end
+
+ context 'when user does not have push access' do
+ it 'responds with status 401' do
+ expect(response).to have_http_status(401)
+ end
+ end
+ end
+
+ context 'when CI is authorized' do
+ let(:authorization) { authorize_ci_project }
+
+ it 'responds with status 403' do
+ expect(response).to have_http_status(401)
+ end
+ end
+ end
+
+ describe 'unsupported' do
+ let(:project) { create(:empty_project) }
+ let(:authorization) { authorize_user }
+ let(:body) do
+ { 'operation' => 'other',
+ 'objects' => [
+ { 'oid' => sample_oid,
+ 'size' => sample_size
+ }]
+ }
+ end
+
+ it 'responds with status 404' do
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
+
+ describe 'when pushing a lfs object' do
+ before do
+ enable_lfs
+ end
+
+ shared_examples 'unauthorized' do
+ context 'and request is sent by gitlab-workhorse to authorize the request' do
+ before do
+ put_authorize
+ end
+
+ it 'responds with status 401' do
+ expect(response).to have_http_status(401)
+ end
+ end
+
+ context 'and request is sent by gitlab-workhorse to finalize the upload' do
+ before do
+ put_finalize
+ end
+
+ it 'responds with status 401' do
+ expect(response).to have_http_status(401)
+ end
+ end
+
+ context 'and request is sent with a malformed headers' do
+ before do
+ put_finalize('/etc/passwd')
+ end
+
+ it 'does not recognize it as a valid lfs command' do
+ expect(response).to have_http_status(401)
+ end
+ end
+ end
+
+ shared_examples 'forbidden' do
+ context 'and request is sent by gitlab-workhorse to authorize the request' do
+ before do
+ put_authorize
+ end
+
+ it 'responds with 403' do
+ expect(response).to have_http_status(403)
+ end
+ end
+
+ context 'and request is sent by gitlab-workhorse to finalize the upload' do
+ before do
+ put_finalize
+ end
+
+ it 'responds with 403' do
+ expect(response).to have_http_status(403)
+ end
+ end
+
+ context 'and request is sent with a malformed headers' do
+ before do
+ put_finalize('/etc/passwd')
+ end
+
+ it 'does not recognize it as a valid lfs command' do
+ expect(response).to have_http_status(403)
+ end
+ end
+ end
+
+ describe 'to one project' do
+ let(:project) { create(:empty_project) }
+
+ describe 'when user is authenticated' do
+ let(:authorization) { authorize_user }
+
+ describe 'when user has push access to the project' do
+ before do
+ project.team << [user, :developer]
+ end
+
+ context 'and request is sent by gitlab-workhorse to authorize the request' do
+ before do
+ put_authorize
+ end
+
+ it 'responds with status 200' do
+ expect(response).to have_http_status(200)
+ end
+
+ it 'responds with status 200, location of lfs store and object details' do
+ expect(json_response['StoreLFSPath']).to eq("#{Gitlab.config.shared.path}/lfs-objects/tmp/upload")
+ expect(json_response['LfsOid']).to eq(sample_oid)
+ expect(json_response['LfsSize']).to eq(sample_size)
+ end
+ end
+
+ context 'and request is sent by gitlab-workhorse to finalize the upload' do
+ before do
+ put_finalize
+ end
+
+ it 'responds with status 200' do
+ expect(response).to have_http_status(200)
+ end
+
+ it 'lfs object is linked to the project' do
+ expect(lfs_object.projects.pluck(:id)).to include(project.id)
+ end
+ end
+
+ context 'invalid tempfiles' do
+ it 'rejects slashes in the tempfile name (path traversal' do
+ put_finalize('foo/bar')
+ expect(response).to have_http_status(403)
+ end
+
+ it 'rejects tempfile names that do not start with the oid' do
+ put_finalize("foo#{sample_oid}")
+ expect(response).to have_http_status(403)
+ end
+ end
+ end
+
+ describe 'and user does not have push access' do
+ before do
+ project.team << [user, :reporter]
+ end
+
+ it_behaves_like 'forbidden'
+ end
+ end
+
+ context 'when CI is authenticated' do
+ let(:authorization) { authorize_ci_project }
+
+ it_behaves_like 'unauthorized'
+ end
+
+ context 'for unauthenticated' do
+ it_behaves_like 'unauthorized'
+ end
+ end
+
+ describe 'to a forked project' do
+ let(:upstream_project) { create(:project, :public) }
+ let(:project_owner) { create(:user) }
+ let(:project) { fork_project(upstream_project, project_owner) }
+
+ describe 'when user is authenticated' do
+ let(:authorization) { authorize_user }
+
+ describe 'when user has push access to the project' do
+ before do
+ project.team << [user, :developer]
+ end
+
+ context 'and request is sent by gitlab-workhorse to authorize the request' do
+ before do
+ put_authorize
+ end
+
+ it 'responds with status 200' do
+ expect(response).to have_http_status(200)
+ end
+
+ it 'with location of lfs store and object details' do
+ expect(json_response['StoreLFSPath']).to eq("#{Gitlab.config.shared.path}/lfs-objects/tmp/upload")
+ expect(json_response['LfsOid']).to eq(sample_oid)
+ expect(json_response['LfsSize']).to eq(sample_size)
+ end
+ end
+
+ context 'and request is sent by gitlab-workhorse to finalize the upload' do
+ before do
+ put_finalize
+ end
+
+ it 'responds with status 200' do
+ expect(response).to have_http_status(200)
+ end
+
+ it 'lfs object is linked to the source project' do
+ expect(lfs_object.projects.pluck(:id)).to include(upstream_project.id)
+ end
+ end
+ end
+
+ describe 'and user does not have push access' do
+ it_behaves_like 'forbidden'
+ end
+ end
+
+ context 'when CI is authenticated' do
+ let(:authorization) { authorize_ci_project }
+
+ it_behaves_like 'unauthorized'
+ end
+
+ context 'for unauthenticated' do
+ it_behaves_like 'unauthorized'
+ end
+
+ describe 'and second project not related to fork or a source project' do
+ let(:second_project) { create(:empty_project) }
+ let(:authorization) { authorize_user }
+
+ before do
+ second_project.team << [user, :master]
+ upstream_project.lfs_objects << lfs_object
+ end
+
+ context 'when pushing the same lfs object to the second project' do
+ before do
+ put "#{second_project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}/#{sample_size}", nil,
+ headers.merge('X-Gitlab-Lfs-Tmp' => lfs_tmp_file).compact
+ end
+
+ it 'responds with status 200' do
+ expect(response).to have_http_status(200)
+ end
+
+ it 'links the lfs object to the project' do
+ expect(lfs_object.projects.pluck(:id)).to include(second_project.id, upstream_project.id)
+ end
+ end
+ end
+ end
+
+ def put_authorize
+ put "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}/#{sample_size}/authorize", nil, headers
+ end
+
+ def put_finalize(lfs_tmp = lfs_tmp_file)
+ put "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}/#{sample_size}", nil,
+ headers.merge('X-Gitlab-Lfs-Tmp' => lfs_tmp).compact
+ end
+
+ def lfs_tmp_file
+ "#{sample_oid}012345678"
+ end
+ end
+
+ def enable_lfs
+ allow(Gitlab.config.lfs).to receive(:enabled).and_return(true)
+ end
+
+ def authorize_ci_project
+ ActionController::HttpAuthentication::Basic.encode_credentials('gitlab-ci-token', project.runners_token)
+ end
+
+ def authorize_user
+ ActionController::HttpAuthentication::Basic.encode_credentials(user.username, user.password)
+ end
+
+ def fork_project(project, user, object = nil)
+ allow(RepositoryForkWorker).to receive(:perform_async).and_return(true)
+ Projects::ForkService.new(project, user, {}).execute
+ end
+
+ def post_lfs_json(url, body = nil, headers = nil)
+ post(url, body.try(:to_json), (headers || {}).merge('Content-Type' => 'application/vnd.git-lfs+json'))
+ end
+
+ def json_response
+ @json_response ||= JSON.parse(response.body)
+ end
+end
diff --git a/spec/routing/admin_routing_spec.rb b/spec/routing/admin_routing_spec.rb
index 8b19936ae6d..69eeb45ed71 100644
--- a/spec/routing/admin_routing_spec.rb
+++ b/spec/routing/admin_routing_spec.rb
@@ -1,6 +1,5 @@
require 'spec_helper'
-# team_update_admin_user PUT /admin/users/:id/team_update(.:format) admin/users#team_update
# block_admin_user PUT /admin/users/:id/block(.:format) admin/users#block
# unblock_admin_user PUT /admin/users/:id/unblock(.:format) admin/users#unblock
# admin_users GET /admin/users(.:format) admin/users#index
@@ -11,10 +10,6 @@ require 'spec_helper'
# PUT /admin/users/:id(.:format) admin/users#update
# DELETE /admin/users/:id(.:format) admin/users#destroy
describe Admin::UsersController, "routing" do
- it "to #team_update" do
- expect(put("/admin/users/1/team_update")).to route_to('admin/users#team_update', id: '1')
- end
-
it "to #block" do
expect(put("/admin/users/1/block")).to route_to('admin/users#block', id: '1')
end
diff --git a/spec/routing/project_routing_spec.rb b/spec/routing/project_routing_spec.rb
index 620f328a114..77842057a10 100644
--- a/spec/routing/project_routing_spec.rb
+++ b/spec/routing/project_routing_spec.rb
@@ -60,7 +60,7 @@ end
# project GET /:id(.:format) projects#show
# PUT /:id(.:format) projects#update
# DELETE /:id(.:format) projects#destroy
-# markdown_preview_project POST /:id/markdown_preview(.:format) projects#markdown_preview
+# preview_markdown_project POST /:id/preview_markdown(.:format) projects#preview_markdown
describe ProjectsController, 'routing' do
it 'to #create' do
expect(post('/projects')).to route_to('projects#create')
@@ -91,9 +91,9 @@ describe ProjectsController, 'routing' do
expect(delete('/gitlab/gitlabhq')).to route_to('projects#destroy', namespace_id: 'gitlab', id: 'gitlabhq')
end
- it 'to #markdown_preview' do
- expect(post('/gitlab/gitlabhq/markdown_preview')).to(
- route_to('projects#markdown_preview', namespace_id: 'gitlab', id: 'gitlabhq')
+ it 'to #preview_markdown' do
+ expect(post('/gitlab/gitlabhq/preview_markdown')).to(
+ route_to('projects#preview_markdown', namespace_id: 'gitlab', id: 'gitlabhq')
)
end
end
@@ -135,10 +135,6 @@ describe Projects::RepositoriesController, 'routing' do
it 'to #archive format:tar.bz2' do
expect(get('/gitlab/gitlabhq/repository/archive.tar.bz2')).to route_to('projects/repositories#archive', namespace_id: 'gitlab', project_id: 'gitlabhq', format: 'tar.bz2')
end
-
- it 'to #show' do
- expect(get('/gitlab/gitlabhq/repository')).to route_to('projects/repositories#show', namespace_id: 'gitlab', project_id: 'gitlabhq')
- end
end
describe Projects::BranchesController, 'routing' do
@@ -483,13 +479,16 @@ end
describe Projects::NetworkController, 'routing' do
it 'to #show' do
expect(get('/gitlab/gitlabhq/network/master')).to route_to('projects/network#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master')
- expect(get('/gitlab/gitlabhq/network/master.json')).to route_to('projects/network#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master', format: 'json')
+ expect(get('/gitlab/gitlabhq/network/ends-with.json')).to route_to('projects/network#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'ends-with.json')
+ expect(get('/gitlab/gitlabhq/network/master?format=json')).to route_to('projects/network#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master', format: 'json')
end
end
describe Projects::GraphsController, 'routing' do
it 'to #show' do
expect(get('/gitlab/gitlabhq/graphs/master')).to route_to('projects/graphs#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master')
+ expect(get('/gitlab/gitlabhq/graphs/ends-with.json')).to route_to('projects/graphs#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'ends-with.json')
+ expect(get('/gitlab/gitlabhq/graphs/master?format=json')).to route_to('projects/graphs#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master', format: 'json')
end
end
diff --git a/spec/routing/routing_spec.rb b/spec/routing/routing_spec.rb
index 8a8e131c57b..4bc3cddd9c2 100644
--- a/spec/routing/routing_spec.rb
+++ b/spec/routing/routing_spec.rb
@@ -98,7 +98,7 @@ describe SnippetsController, "routing" do
end
# help GET /help(.:format) help#index
-# help_page GET /help/:category/:file(.:format) help#show {:category=>/.*/, :file=>/[^\/\.]+/}
+# help_page GET /help/*path(.:format) help#show
# help_shortcuts GET /help/shortcuts(.:format) help#shortcuts
# help_ui GET /help/ui(.:format) help#ui
describe HelpController, "routing" do
@@ -107,25 +107,25 @@ describe HelpController, "routing" do
end
it 'to #show' do
- path = '/help/markdown/markdown.md'
+ path = '/help/user/markdown.md'
expect(get(path)).to route_to('help#show',
- category: 'markdown',
- file: 'markdown',
+ path: 'user/markdown',
format: 'md')
path = '/help/workflow/protected_branches/protected_branches1.png'
expect(get(path)).to route_to('help#show',
- category: 'workflow/protected_branches',
- file: 'protected_branches1',
+ path: 'workflow/protected_branches/protected_branches1',
format: 'png')
- end
- it 'to #shortcuts' do
- expect(get('/help/shortcuts')).to route_to('help#shortcuts')
+ path = '/help/ui'
+ expect(get(path)).to route_to('help#ui')
end
+end
- it 'to #ui' do
- expect(get('/help/ui')).to route_to('help#ui')
+# koding GET /koding(.:format) koding#index
+describe KodingController, "routing" do
+ it "to #index" do
+ expect(get("/koding")).to route_to('koding#index')
end
end
@@ -183,18 +183,10 @@ describe Profiles::KeysController, "routing" do
expect(post("/profile/keys")).to route_to('profiles/keys#create')
end
- it "to #edit" do
- expect(get("/profile/keys/1/edit")).to route_to('profiles/keys#edit', id: '1')
- end
-
it "to #show" do
expect(get("/profile/keys/1")).to route_to('profiles/keys#show', id: '1')
end
- it "to #update" do
- expect(put("/profile/keys/1")).to route_to('profiles/keys#update', id: '1')
- end
-
it "to #destroy" do
expect(delete("/profile/keys/1")).to route_to('profiles/keys#destroy', id: '1')
end
diff --git a/spec/services/auth/container_registry_authentication_service_spec.rb b/spec/services/auth/container_registry_authentication_service_spec.rb
index 67777ad48bc..7cc71f706ce 100644
--- a/spec/services/auth/container_registry_authentication_service_spec.rb
+++ b/spec/services/auth/container_registry_authentication_service_spec.rb
@@ -87,51 +87,105 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do
end
context 'user authorization' do
- let(:project) { create(:project) }
let(:current_user) { create(:user) }
- context 'allow to use scope-less authentication' do
- it_behaves_like 'a valid token'
- end
+ context 'for private project' do
+ let(:project) { create(:empty_project) }
- context 'allow developer to push images' do
- before { project.team << [current_user, :developer] }
+ context 'allow to use scope-less authentication' do
+ it_behaves_like 'a valid token'
+ end
- let(:current_params) do
- { scope: "repository:#{project.path_with_namespace}:push" }
+ context 'allow developer to push images' do
+ before { project.team << [current_user, :developer] }
+
+ let(:current_params) do
+ { scope: "repository:#{project.path_with_namespace}:push" }
+ end
+
+ it_behaves_like 'a pushable'
end
- it_behaves_like 'a pushable'
- end
+ context 'allow reporter to pull images' do
+ before { project.team << [current_user, :reporter] }
+
+ let(:current_params) do
+ { scope: "repository:#{project.path_with_namespace}:pull" }
+ end
- context 'allow reporter to pull images' do
- before { project.team << [current_user, :reporter] }
+ it_behaves_like 'a pullable'
+ end
- let(:current_params) do
- { scope: "repository:#{project.path_with_namespace}:pull" }
+ context 'return a least of privileges' do
+ before { project.team << [current_user, :reporter] }
+
+ let(:current_params) do
+ { scope: "repository:#{project.path_with_namespace}:push,pull" }
+ end
+
+ it_behaves_like 'a pullable'
end
- it_behaves_like 'a pullable'
+ context 'disallow guest to pull or push images' do
+ before { project.team << [current_user, :guest] }
+
+ let(:current_params) do
+ { scope: "repository:#{project.path_with_namespace}:pull,push" }
+ end
+
+ it_behaves_like 'an inaccessible'
+ end
end
- context 'return a least of privileges' do
- before { project.team << [current_user, :reporter] }
+ context 'for public project' do
+ let(:project) { create(:empty_project, :public) }
- let(:current_params) do
- { scope: "repository:#{project.path_with_namespace}:push,pull" }
+ context 'allow anyone to pull images' do
+ let(:current_params) do
+ { scope: "repository:#{project.path_with_namespace}:pull" }
+ end
+
+ it_behaves_like 'a pullable'
end
- it_behaves_like 'a pullable'
+ context 'disallow anyone to push images' do
+ let(:current_params) do
+ { scope: "repository:#{project.path_with_namespace}:push" }
+ end
+
+ it_behaves_like 'an inaccessible'
+ end
end
- context 'disallow guest to pull or push images' do
- before { project.team << [current_user, :guest] }
+ context 'for internal project' do
+ let(:project) { create(:empty_project, :internal) }
- let(:current_params) do
- { scope: "repository:#{project.path_with_namespace}:pull,push" }
+ context 'for internal user' do
+ context 'allow anyone to pull images' do
+ let(:current_params) do
+ { scope: "repository:#{project.path_with_namespace}:pull" }
+ end
+
+ it_behaves_like 'a pullable'
+ end
+
+ context 'disallow anyone to push images' do
+ let(:current_params) do
+ { scope: "repository:#{project.path_with_namespace}:push" }
+ end
+
+ it_behaves_like 'an inaccessible'
+ end
end
- it_behaves_like 'an inaccessible'
+ context 'for external user' do
+ let(:current_user) { create(:user, external: true) }
+ let(:current_params) do
+ { scope: "repository:#{project.path_with_namespace}:pull,push" }
+ end
+
+ it_behaves_like 'an inaccessible'
+ end
end
end
diff --git a/spec/services/boards/create_service_spec.rb b/spec/services/boards/create_service_spec.rb
new file mode 100644
index 00000000000..a1a4dd4c57c
--- /dev/null
+++ b/spec/services/boards/create_service_spec.rb
@@ -0,0 +1,35 @@
+require 'spec_helper'
+
+describe Boards::CreateService, services: true do
+ describe '#execute' do
+ subject(:service) { described_class.new(project, double) }
+
+ context 'when project does not have a board' do
+ let(:project) { create(:empty_project, board: nil) }
+
+ it 'creates a new board' do
+ expect { service.execute }.to change(Board, :count).by(1)
+ end
+
+ it 'creates default lists' do
+ service.execute
+
+ expect(project.board.lists.size).to eq 2
+ expect(project.board.lists.first).to be_backlog
+ expect(project.board.lists.last).to be_done
+ end
+ end
+
+ context 'when project has a board' do
+ let!(:project) { create(:project_with_board) }
+
+ it 'does not create a new board' do
+ expect { service.execute }.not_to change(Board, :count)
+ end
+
+ it 'does not create board lists' do
+ expect { service.execute }.not_to change(project.board.lists, :count)
+ end
+ end
+ end
+end
diff --git a/spec/services/boards/issues/list_service_spec.rb b/spec/services/boards/issues/list_service_spec.rb
new file mode 100644
index 00000000000..cf4c5f13635
--- /dev/null
+++ b/spec/services/boards/issues/list_service_spec.rb
@@ -0,0 +1,73 @@
+require 'spec_helper'
+
+describe Boards::Issues::ListService, services: true do
+ describe '#execute' do
+ let(:user) { create(:user) }
+ let(:project) { create(:project_with_board) }
+ let(:board) { project.board }
+
+ let(:bug) { create(:label, project: project, name: 'Bug') }
+ let(:development) { create(:label, project: project, name: 'Development') }
+ let(:testing) { create(:label, project: project, name: 'Testing') }
+ let(:p1) { create(:label, title: 'P1', project: project, priority: 1) }
+ let(:p2) { create(:label, title: 'P2', project: project, priority: 2) }
+ let(:p3) { create(:label, title: 'P3', project: project, priority: 3) }
+
+ let!(:backlog) { create(:backlog_list, board: board) }
+ let!(:list1) { create(:list, board: board, label: development, position: 0) }
+ let!(:list2) { create(:list, board: board, label: testing, position: 1) }
+ let!(:done) { create(:done_list, board: board) }
+
+ let!(:opened_issue1) { create(:labeled_issue, project: project, labels: [bug]) }
+ let!(:opened_issue2) { create(:labeled_issue, project: project, labels: [p2]) }
+ let!(:reopened_issue1) { create(:issue, :reopened, project: project) }
+
+ let!(:list1_issue1) { create(:labeled_issue, project: project, labels: [p2, development]) }
+ let!(:list1_issue2) { create(:labeled_issue, project: project, labels: [development]) }
+ let!(:list1_issue3) { create(:labeled_issue, project: project, labels: [development, p1]) }
+ let!(:list2_issue1) { create(:labeled_issue, project: project, labels: [testing]) }
+
+ let!(:closed_issue1) { create(:labeled_issue, :closed, project: project, labels: [bug]) }
+ let!(:closed_issue2) { create(:labeled_issue, :closed, project: project, labels: [p3]) }
+ let!(:closed_issue3) { create(:issue, :closed, project: project) }
+ let!(:closed_issue4) { create(:labeled_issue, :closed, project: project, labels: [p1, development]) }
+
+ before do
+ project.team << [user, :developer]
+ end
+
+ it 'delegates search to IssuesFinder' do
+ params = { id: list1.id }
+
+ expect_any_instance_of(IssuesFinder).to receive(:execute).once.and_call_original
+
+ described_class.new(project, user, params).execute
+ end
+
+ context 'sets default order to priority' do
+ it 'returns opened issues when listing issues from Backlog' do
+ params = { id: backlog.id }
+
+ issues = described_class.new(project, user, params).execute
+
+ expect(issues).to eq [opened_issue2, reopened_issue1, opened_issue1]
+ end
+
+ it 'returns closed issues when listing issues from Done' do
+ params = { id: done.id }
+
+ issues = described_class.new(project, user, params).execute
+
+ expect(issues).to eq [closed_issue2, closed_issue3, closed_issue1]
+ end
+
+ it 'returns opened/closed issues that have label list applied when listing issues from a label list' do
+ params = { id: list1.id }
+
+ issues = described_class.new(project, user, params).execute
+
+ expect(issues).to eq [closed_issue4, list1_issue3, list1_issue1, list1_issue2]
+ end
+ end
+ end
+end
diff --git a/spec/services/boards/issues/move_service_spec.rb b/spec/services/boards/issues/move_service_spec.rb
new file mode 100644
index 00000000000..0122159cab8
--- /dev/null
+++ b/spec/services/boards/issues/move_service_spec.rb
@@ -0,0 +1,140 @@
+require 'spec_helper'
+
+describe Boards::Issues::MoveService, services: true do
+ describe '#execute' do
+ let(:user) { create(:user) }
+ let(:project) { create(:project_with_board) }
+ let(:board) { project.board }
+
+ let(:bug) { create(:label, project: project, name: 'Bug') }
+ let(:development) { create(:label, project: project, name: 'Development') }
+ let(:testing) { create(:label, project: project, name: 'Testing') }
+
+ let!(:backlog) { create(:backlog_list, board: board) }
+ let!(:list1) { create(:list, board: board, label: development, position: 0) }
+ let!(:list2) { create(:list, board: board, label: testing, position: 1) }
+ let!(:done) { create(:done_list, board: board) }
+
+ before do
+ project.team << [user, :developer]
+ end
+
+ context 'when moving from backlog' do
+ it 'adds the label of the list it goes to' do
+ issue = create(:labeled_issue, project: project, labels: [bug])
+ params = { from_list_id: backlog.id, to_list_id: list1.id }
+
+ described_class.new(project, user, params).execute(issue)
+
+ expect(issue.reload.labels).to contain_exactly(bug, development)
+ end
+ end
+
+ context 'when moving to backlog' do
+ it 'removes all list-labels' do
+ issue = create(:labeled_issue, project: project, labels: [bug, development, testing])
+ params = { from_list_id: list1.id, to_list_id: backlog.id }
+
+ described_class.new(project, user, params).execute(issue)
+
+ expect(issue.reload.labels).to contain_exactly(bug)
+ end
+ end
+
+ context 'when moving from backlog to done' do
+ it 'closes the issue' do
+ issue = create(:labeled_issue, project: project, labels: [bug])
+ params = { from_list_id: backlog.id, to_list_id: done.id }
+
+ described_class.new(project, user, params).execute(issue)
+ issue.reload
+
+ expect(issue.labels).to contain_exactly(bug)
+ expect(issue).to be_closed
+ end
+ end
+
+ context 'when moving an issue between lists' do
+ let(:issue) { create(:labeled_issue, project: project, labels: [bug, development]) }
+ let(:params) { { from_list_id: list1.id, to_list_id: list2.id } }
+
+ it 'delegates the label changes to Issues::UpdateService' do
+ expect_any_instance_of(Issues::UpdateService).to receive(:execute).with(issue).once
+
+ described_class.new(project, user, params).execute(issue)
+ end
+
+ it 'removes the label from the list it came from and adds the label of the list it goes to' do
+ described_class.new(project, user, params).execute(issue)
+
+ expect(issue.reload.labels).to contain_exactly(bug, testing)
+ end
+ end
+
+ context 'when moving to done' do
+ let(:issue) { create(:labeled_issue, project: project, labels: [bug, development, testing]) }
+ let(:params) { { from_list_id: list2.id, to_list_id: done.id } }
+
+ it 'delegates the close proceedings to Issues::CloseService' do
+ expect_any_instance_of(Issues::CloseService).to receive(:execute).with(issue).once
+
+ described_class.new(project, user, params).execute(issue)
+ end
+
+ it 'removes all list-labels and close the issue' do
+ described_class.new(project, user, params).execute(issue)
+ issue.reload
+
+ expect(issue.labels).to contain_exactly(bug)
+ expect(issue).to be_closed
+ end
+ end
+
+ context 'when moving from done' do
+ let(:issue) { create(:labeled_issue, :closed, project: project, labels: [bug]) }
+ let(:params) { { from_list_id: done.id, to_list_id: list2.id } }
+
+ it 'delegates the re-open proceedings to Issues::ReopenService' do
+ expect_any_instance_of(Issues::ReopenService).to receive(:execute).with(issue).once
+
+ described_class.new(project, user, params).execute(issue)
+ end
+
+ it 'adds the label of the list it goes to and reopen the issue' do
+ described_class.new(project, user, params).execute(issue)
+ issue.reload
+
+ expect(issue.labels).to contain_exactly(bug, testing)
+ expect(issue).to be_reopened
+ end
+ end
+
+ context 'when moving from done to backlog' do
+ it 'reopens the issue' do
+ issue = create(:labeled_issue, :closed, project: project, labels: [bug])
+ params = { from_list_id: done.id, to_list_id: backlog.id }
+
+ described_class.new(project, user, params).execute(issue)
+ issue.reload
+
+ expect(issue.labels).to contain_exactly(bug)
+ expect(issue).to be_reopened
+ end
+ end
+
+ context 'when moving to same list' do
+ let(:issue) { create(:labeled_issue, project: project, labels: [bug, development]) }
+ let(:params) { { from_list_id: list1.id, to_list_id: list1.id } }
+
+ it 'returns false' do
+ expect(described_class.new(project, user, params).execute(issue)).to eq false
+ end
+
+ it 'keeps issues labels' do
+ described_class.new(project, user, params).execute(issue)
+
+ expect(issue.reload.labels).to contain_exactly(bug, development)
+ end
+ end
+ end
+end
diff --git a/spec/services/boards/lists/create_service_spec.rb b/spec/services/boards/lists/create_service_spec.rb
new file mode 100644
index 00000000000..5e7e145065e
--- /dev/null
+++ b/spec/services/boards/lists/create_service_spec.rb
@@ -0,0 +1,54 @@
+require 'spec_helper'
+
+describe Boards::Lists::CreateService, services: true do
+ describe '#execute' do
+ let(:project) { create(:project_with_board) }
+ let(:board) { project.board }
+ let(:user) { create(:user) }
+ let(:label) { create(:label, name: 'in-progress') }
+
+ subject(:service) { described_class.new(project, user, label_id: label.id) }
+
+ context 'when board lists is empty' do
+ it 'creates a new list at beginning of the list' do
+ list = service.execute
+
+ expect(list.position).to eq 0
+ end
+ end
+
+ context 'when board lists has only a backlog list' do
+ it 'creates a new list at beginning of the list' do
+ create(:backlog_list, board: board)
+
+ list = service.execute
+
+ expect(list.position).to eq 0
+ end
+ end
+
+ context 'when board lists has only labels lists' do
+ it 'creates a new list at end of the lists' do
+ create(:list, board: board, position: 0)
+ create(:list, board: board, position: 1)
+
+ list = service.execute
+
+ expect(list.position).to eq 2
+ end
+ end
+
+ context 'when board lists has backlog, label and done lists' do
+ it 'creates a new list at end of the label lists' do
+ create(:backlog_list, board: board)
+ create(:done_list, board: board)
+ list1 = create(:list, board: board, position: 0)
+
+ list2 = service.execute
+
+ expect(list1.reload.position).to eq 0
+ expect(list2.reload.position).to eq 1
+ end
+ end
+ end
+end
diff --git a/spec/services/boards/lists/destroy_service_spec.rb b/spec/services/boards/lists/destroy_service_spec.rb
new file mode 100644
index 00000000000..6eff445feee
--- /dev/null
+++ b/spec/services/boards/lists/destroy_service_spec.rb
@@ -0,0 +1,47 @@
+require 'spec_helper'
+
+describe Boards::Lists::DestroyService, services: true do
+ describe '#execute' do
+ let(:project) { create(:project_with_board) }
+ let(:board) { project.board }
+ let(:user) { create(:user) }
+
+ context 'when list type is label' do
+ it 'removes list from board' do
+ list = create(:list, board: board)
+ service = described_class.new(project, user)
+
+ expect { service.execute(list) }.to change(board.lists, :count).by(-1)
+ end
+
+ it 'decrements position of higher lists' do
+ backlog = create(:backlog_list, board: board)
+ development = create(:list, board: board, position: 0)
+ review = create(:list, board: board, position: 1)
+ staging = create(:list, board: board, position: 2)
+ done = create(:done_list, board: board)
+
+ described_class.new(project, user).execute(development)
+
+ expect(backlog.reload.position).to be_nil
+ expect(review.reload.position).to eq 0
+ expect(staging.reload.position).to eq 1
+ expect(done.reload.position).to be_nil
+ end
+ end
+
+ it 'does not remove list from board when list type is backlog' do
+ list = create(:backlog_list, board: board)
+ service = described_class.new(project, user)
+
+ expect { service.execute(list) }.not_to change(board.lists, :count)
+ end
+
+ it 'does not remove list from board when list type is done' do
+ list = create(:done_list, board: board)
+ service = described_class.new(project, user)
+
+ expect { service.execute(list) }.not_to change(board.lists, :count)
+ end
+ end
+end
diff --git a/spec/services/boards/lists/generate_service_spec.rb b/spec/services/boards/lists/generate_service_spec.rb
new file mode 100644
index 00000000000..9fd39122737
--- /dev/null
+++ b/spec/services/boards/lists/generate_service_spec.rb
@@ -0,0 +1,40 @@
+require 'spec_helper'
+
+describe Boards::Lists::GenerateService, services: true do
+ describe '#execute' do
+ let(:project) { create(:project_with_board) }
+ let(:board) { project.board }
+ let(:user) { create(:user) }
+
+ subject(:service) { described_class.new(project, user) }
+
+ context 'when board lists is empty' do
+ it 'creates the default lists' do
+ expect { service.execute }.to change(board.lists, :count).by(4)
+ end
+ end
+
+ context 'when board lists is not empty' do
+ it 'does not creates the default lists' do
+ create(:list, board: board)
+
+ expect { service.execute }.not_to change(board.lists, :count)
+ end
+ end
+
+ context 'when project labels does not contains any list label' do
+ it 'creates labels' do
+ expect { service.execute }.to change(project.labels, :count).by(4)
+ end
+ end
+
+ context 'when project labels contains some of list label' do
+ it 'creates the missing labels' do
+ create(:label, project: project, name: 'Development')
+ create(:label, project: project, name: 'Ready')
+
+ expect { service.execute }.to change(project.labels, :count).by(2)
+ end
+ end
+ end
+end
diff --git a/spec/services/boards/lists/move_service_spec.rb b/spec/services/boards/lists/move_service_spec.rb
new file mode 100644
index 00000000000..3e9b7d07fc6
--- /dev/null
+++ b/spec/services/boards/lists/move_service_spec.rb
@@ -0,0 +1,110 @@
+require 'spec_helper'
+
+describe Boards::Lists::MoveService, services: true do
+ describe '#execute' do
+ let(:project) { create(:project_with_board) }
+ let(:board) { project.board }
+ let(:user) { create(:user) }
+
+ let!(:backlog) { create(:backlog_list, board: board) }
+ let!(:planning) { create(:list, board: board, position: 0) }
+ let!(:development) { create(:list, board: board, position: 1) }
+ let!(:review) { create(:list, board: board, position: 2) }
+ let!(:staging) { create(:list, board: board, position: 3) }
+ let!(:done) { create(:done_list, board: board) }
+
+ context 'when list type is set to label' do
+ it 'keeps position of lists when new position is nil' do
+ service = described_class.new(project, user, position: nil)
+
+ service.execute(planning)
+
+ expect(current_list_positions).to eq [0, 1, 2, 3]
+ end
+
+ it 'keeps position of lists when new positon is equal to old position' do
+ service = described_class.new(project, user, position: planning.position)
+
+ service.execute(planning)
+
+ expect(current_list_positions).to eq [0, 1, 2, 3]
+ end
+
+ it 'keeps position of lists when new positon is negative' do
+ service = described_class.new(project, user, position: -1)
+
+ service.execute(planning)
+
+ expect(current_list_positions).to eq [0, 1, 2, 3]
+ end
+
+ it 'keeps position of lists when new positon is equal to number of labels lists' do
+ service = described_class.new(project, user, position: board.lists.label.size)
+
+ service.execute(planning)
+
+ expect(current_list_positions).to eq [0, 1, 2, 3]
+ end
+
+ it 'keeps position of lists when new positon is greater than number of labels lists' do
+ service = described_class.new(project, user, position: board.lists.label.size + 1)
+
+ service.execute(planning)
+
+ expect(current_list_positions).to eq [0, 1, 2, 3]
+ end
+
+ it 'increments position of intermediate lists when new positon is equal to first position' do
+ service = described_class.new(project, user, position: 0)
+
+ service.execute(staging)
+
+ expect(current_list_positions).to eq [1, 2, 3, 0]
+ end
+
+ it 'decrements position of intermediate lists when new positon is equal to last position' do
+ service = described_class.new(project, user, position: board.lists.label.last.position)
+
+ service.execute(planning)
+
+ expect(current_list_positions).to eq [3, 0, 1, 2]
+ end
+
+ it 'decrements position of intermediate lists when new position is greater than old position' do
+ service = described_class.new(project, user, position: 2)
+
+ service.execute(planning)
+
+ expect(current_list_positions).to eq [2, 0, 1, 3]
+ end
+
+ it 'increments position of intermediate lists when new position is lower than old position' do
+ service = described_class.new(project, user, position: 1)
+
+ service.execute(staging)
+
+ expect(current_list_positions).to eq [0, 2, 3, 1]
+ end
+ end
+
+ it 'keeps position of lists when list type is backlog' do
+ service = described_class.new(project, user, position: 2)
+
+ service.execute(backlog)
+
+ expect(current_list_positions).to eq [0, 1, 2, 3]
+ end
+
+ it 'keeps position of lists when list type is done' do
+ service = described_class.new(project, user, position: 2)
+
+ service.execute(done)
+
+ expect(current_list_positions).to eq [0, 1, 2, 3]
+ end
+ end
+
+ def current_list_positions
+ [planning, development, review, staging].map { |list| list.reload.position }
+ end
+end
diff --git a/spec/services/ci/create_builds_service_spec.rb b/spec/services/ci/create_builds_service_spec.rb
deleted file mode 100644
index 8b0becd83d3..00000000000
--- a/spec/services/ci/create_builds_service_spec.rb
+++ /dev/null
@@ -1,32 +0,0 @@
-require 'spec_helper'
-
-describe Ci::CreateBuildsService, services: true do
- let(:pipeline) { create(:ci_pipeline, ref: 'master') }
- let(:user) { create(:user) }
-
- describe '#execute' do
- # Using stubbed .gitlab-ci.yml created in commit factory
- #
-
- subject do
- described_class.new(pipeline).execute('test', user, status, nil)
- end
-
- context 'next builds available' do
- let(:status) { 'success' }
-
- it { is_expected.to be_an_instance_of Array }
- it { is_expected.to all(be_an_instance_of Ci::Build) }
-
- it 'does not persist created builds' do
- expect(subject.first).not_to be_persisted
- end
- end
-
- context 'builds skipped' do
- let(:status) { 'skipped' }
-
- it { is_expected.to be_empty }
- end
- end
-end
diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb
new file mode 100644
index 00000000000..4aadd009f3e
--- /dev/null
+++ b/spec/services/ci/create_pipeline_service_spec.rb
@@ -0,0 +1,214 @@
+require 'spec_helper'
+
+describe Ci::CreatePipelineService, services: true do
+ let(:project) { FactoryGirl.create(:project) }
+ let(:user) { create(:admin) }
+
+ before do
+ stub_ci_pipeline_to_return_yaml_file
+ end
+
+ describe '#execute' do
+ def execute(params)
+ described_class.new(project, user, params).execute
+ end
+
+ context 'valid params' do
+ let(:pipeline) do
+ execute(ref: 'refs/heads/master',
+ before: '00000000',
+ after: project.commit.id,
+ commits: [{ message: "Message" }])
+ end
+
+ 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).to have_attributes(user: user) }
+ it { expect(pipeline.builds.first).to be_kind_of(Ci::Build) }
+ end
+
+ context "skip tag if there is no build for it" do
+ it "creates commit if there is appropriate job" do
+ result = execute(ref: 'refs/heads/master',
+ before: '00000000',
+ after: project.commit.id,
+ commits: [{ message: "Message" }])
+ expect(result).to be_persisted
+ end
+
+ it "creates commit if there is no appropriate job but deploy job has right ref setting" do
+ config = YAML.dump({ deploy: { script: "ls", only: ["master"] } })
+ stub_ci_pipeline_yaml_file(config)
+ result = execute(ref: 'refs/heads/master',
+ before: '00000000',
+ after: project.commit.id,
+ commits: [{ message: "Message" }])
+
+ expect(result).to be_persisted
+ end
+ end
+
+ it 'skips creating pipeline for refs without .gitlab-ci.yml' do
+ stub_ci_pipeline_yaml_file(nil)
+ result = execute(ref: 'refs/heads/master',
+ before: '00000000',
+ after: project.commit.id,
+ commits: [{ message: 'Message' }])
+
+ expect(result).not_to be_persisted
+ expect(Ci::Pipeline.count).to eq(0)
+ end
+
+ it 'fails commits if yaml is invalid' do
+ message = 'message'
+ allow_any_instance_of(Ci::Pipeline).to receive(:git_commit_message) { message }
+ stub_ci_pipeline_yaml_file('invalid: file: file')
+ commits = [{ message: message }]
+ pipeline = execute(ref: 'refs/heads/master',
+ before: '00000000',
+ after: project.commit.id,
+ 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
+
+ context 'when commit contains a [ci skip] directive' do
+ let(:message) { "some message[ci skip]" }
+ let(:messageFlip) { "some message[skip ci]" }
+ let(:capMessage) { "some message[CI SKIP]" }
+ let(:capMessageFlip) { "some message[SKIP CI]" }
+
+ before do
+ 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 }]
+ pipeline = execute(ref: 'refs/heads/master',
+ before: '00000000',
+ after: project.commit.id,
+ commits: commits)
+
+ expect(pipeline).to be_persisted
+ expect(pipeline.builds.any?).to be false
+ expect(pipeline.status).to eq("skipped")
+ end
+
+ it "skips builds creation if there is [skip ci] tag in commit message" do
+ commits = [{ message: messageFlip }]
+ pipeline = execute(ref: 'refs/heads/master',
+ before: '00000000',
+ after: project.commit.id,
+ commits: commits)
+
+ expect(pipeline).to be_persisted
+ expect(pipeline.builds.any?).to be false
+ expect(pipeline.status).to eq("skipped")
+ end
+
+ it "skips builds creation if there is [CI SKIP] tag in commit message" do
+ commits = [{ message: capMessage }]
+ pipeline = execute(ref: 'refs/heads/master',
+ before: '00000000',
+ after: project.commit.id,
+ commits: commits)
+
+ expect(pipeline).to be_persisted
+ expect(pipeline.builds.any?).to be false
+ expect(pipeline.status).to eq("skipped")
+ end
+
+ it "skips builds creation if there is [SKIP CI] tag in commit message" do
+ commits = [{ message: capMessageFlip }]
+ pipeline = execute(ref: 'refs/heads/master',
+ before: '00000000',
+ after: project.commit.id,
+ 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] or [skip ci] tag in commit message" do
+ allow_any_instance_of(Ci::Pipeline).to receive(:git_commit_message) { "some message" }
+
+ commits = [{ message: "some message" }]
+ pipeline = execute(ref: 'refs/heads/master',
+ before: '00000000',
+ after: project.commit.id,
+ commits: commits)
+
+ expect(pipeline).to be_persisted
+ expect(pipeline.builds.first.name).to eq("rspec")
+ end
+
+ it "fails builds creation if there is [ci skip] tag in commit message and yaml is invalid" do
+ stub_ci_pipeline_yaml_file('invalid: file: fiile')
+ commits = [{ message: message }]
+ pipeline = execute(ref: 'refs/heads/master',
+ before: '00000000',
+ after: project.commit.id,
+ 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
+ end
+
+ it "creates commit with failed status if yaml is invalid" do
+ stub_ci_pipeline_yaml_file('invalid: file')
+ commits = [{ message: "some message" }]
+ pipeline = execute(ref: 'refs/heads/master',
+ before: '00000000',
+ after: project.commit.id,
+ commits: commits)
+
+ expect(pipeline).to be_persisted
+ expect(pipeline.status).to eq("failed")
+ expect(pipeline.builds.any?).to be false
+ end
+
+ context 'when there are no jobs for this pipeline' do
+ before do
+ config = YAML.dump({ test: { script: 'ls', only: ['feature'] } })
+ stub_ci_pipeline_yaml_file(config)
+ end
+
+ it 'does not create a new pipeline' do
+ result = execute(ref: 'refs/heads/master',
+ before: '00000000',
+ after: project.commit.id,
+ commits: [{ message: 'some msg' }])
+
+ expect(result).not_to be_persisted
+ expect(Ci::Build.all).to be_empty
+ expect(Ci::Pipeline.count).to eq(0)
+ end
+ end
+
+ context 'with manual actions' do
+ before do
+ config = YAML.dump({ deploy: { script: 'ls', when: 'manual' } })
+ stub_ci_pipeline_yaml_file(config)
+ end
+
+ it 'does not create a new pipeline' do
+ result = execute(ref: 'refs/heads/master',
+ before: '00000000',
+ after: project.commit.id,
+ commits: [{ message: 'some msg' }])
+
+ expect(result).to be_persisted
+ expect(result.manual_actions).not_to be_empty
+ end
+ end
+ end
+end
diff --git a/spec/services/ci/create_trigger_request_service_spec.rb b/spec/services/ci/create_trigger_request_service_spec.rb
index ae4b7aca820..d8c443d29d5 100644
--- a/spec/services/ci/create_trigger_request_service_spec.rb
+++ b/spec/services/ci/create_trigger_request_service_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe Ci::CreateTriggerRequestService, services: true do
- let(:service) { Ci::CreateTriggerRequestService.new }
+ let(:service) { described_class.new }
let(:project) { create(:project) }
let(:trigger) { create(:ci_trigger, project: project) }
@@ -9,7 +9,7 @@ describe Ci::CreateTriggerRequestService, services: true do
stub_ci_pipeline_to_return_yaml_file
end
- describe :execute do
+ describe '#execute' do
context 'valid params' do
subject { service.execute(project, trigger, 'master') }
@@ -27,8 +27,7 @@ describe Ci::CreateTriggerRequestService, services: true do
subject { service.execute(project, trigger, 'master') }
before do
- stub_ci_pipeline_yaml_file('{}')
- FactoryGirl.create :ci_pipeline, project: project
+ stub_ci_pipeline_yaml_file('script: { only: [develop], script: hello World }')
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 476a888e394..c931c3e4829 100644
--- a/spec/services/ci/image_for_build_service_spec.rb
+++ b/spec/services/ci/image_for_build_service_spec.rb
@@ -5,10 +5,10 @@ module Ci
let(:service) { ImageForBuildService.new }
let(:project) { FactoryGirl.create(:empty_project) }
let(:commit_sha) { '01234567890123456789' }
- let(:commit) { project.ensure_pipeline(commit_sha, 'master') }
- let(:build) { FactoryGirl.create(:ci_build, pipeline: commit) }
+ let(:pipeline) { project.ensure_pipeline(commit_sha, 'master') }
+ let(:build) { FactoryGirl.create(:ci_build, pipeline: pipeline) }
- describe :execute do
+ describe '#execute' do
before { build }
context 'branch name' do
diff --git a/spec/services/ci/process_pipeline_service_spec.rb b/spec/services/ci/process_pipeline_service_spec.rb
new file mode 100644
index 00000000000..8326e5cd313
--- /dev/null
+++ b/spec/services/ci/process_pipeline_service_spec.rb
@@ -0,0 +1,328 @@
+require 'spec_helper'
+
+describe Ci::ProcessPipelineService, services: true do
+ let(:pipeline) { create(:ci_pipeline, ref: 'master') }
+ let(:user) { create(:user) }
+ let(:config) { nil }
+
+ before do
+ allow(pipeline).to receive(:ci_yaml_file).and_return(config)
+ end
+
+ describe '#execute' do
+ def all_builds
+ pipeline.builds
+ end
+
+ def builds
+ all_builds.where.not(status: [:created, :skipped])
+ end
+
+ def create_builds
+ described_class.new(pipeline.project, user).execute(pipeline)
+ end
+
+ def succeed_pending
+ builds.pending.update_all(status: 'success')
+ end
+
+ context 'start queuing next builds' do
+ before do
+ create(:ci_build, :created, pipeline: pipeline, name: 'linux', stage_idx: 0)
+ create(:ci_build, :created, pipeline: pipeline, name: 'mac', stage_idx: 0)
+ create(:ci_build, :created, pipeline: pipeline, name: 'rspec', stage_idx: 1)
+ create(:ci_build, :created, pipeline: pipeline, name: 'rubocop', stage_idx: 1)
+ create(:ci_build, :created, pipeline: pipeline, name: 'deploy', stage_idx: 2)
+ end
+
+ it 'processes a pipeline' do
+ expect(create_builds).to be_truthy
+ succeed_pending
+ expect(builds.success.count).to eq(2)
+
+ expect(create_builds).to be_truthy
+ succeed_pending
+ expect(builds.success.count).to eq(4)
+
+ expect(create_builds).to be_truthy
+ succeed_pending
+ expect(builds.success.count).to eq(5)
+
+ expect(create_builds).to be_falsey
+ end
+
+ it 'does not process pipeline if existing stage is running' do
+ expect(create_builds).to be_truthy
+ expect(builds.pending.count).to eq(2)
+
+ expect(create_builds).to be_falsey
+ expect(builds.pending.count).to eq(2)
+ end
+ end
+
+ context 'custom stage with first job allowed to fail' do
+ before do
+ create(:ci_build, :created, pipeline: pipeline, name: 'clean_job', stage_idx: 0, allow_failure: true)
+ create(:ci_build, :created, pipeline: pipeline, name: 'test_job', stage_idx: 1, allow_failure: true)
+ end
+
+ it 'automatically triggers a next stage when build finishes' do
+ expect(create_builds).to be_truthy
+ expect(builds.pluck(:status)).to contain_exactly('pending')
+
+ pipeline.builds.running_or_pending.each(&:drop)
+ expect(builds.pluck(:status)).to contain_exactly('failed', 'pending')
+ end
+ end
+
+ context 'properly creates builds when "when" is defined' do
+ before do
+ create(:ci_build, :created, pipeline: pipeline, name: 'build', stage_idx: 0)
+ create(:ci_build, :created, pipeline: pipeline, name: 'test', stage_idx: 1)
+ create(:ci_build, :created, pipeline: pipeline, name: 'test_failure', stage_idx: 2, when: 'on_failure')
+ create(:ci_build, :created, pipeline: pipeline, name: 'deploy', stage_idx: 3)
+ create(:ci_build, :created, pipeline: pipeline, name: 'production', stage_idx: 3, when: 'manual')
+ create(:ci_build, :created, pipeline: pipeline, name: 'cleanup', stage_idx: 4, when: 'always')
+ create(:ci_build, :created, pipeline: pipeline, name: 'clear cache', stage_idx: 4, when: 'manual')
+ end
+
+ context 'when builds are successful' do
+ it 'properly creates builds' do
+ expect(create_builds).to be_truthy
+ expect(builds.pluck(:name)).to contain_exactly('build')
+ expect(builds.pluck(:status)).to contain_exactly('pending')
+ pipeline.builds.running_or_pending.each(&:success)
+
+ expect(builds.pluck(:name)).to contain_exactly('build', 'test')
+ expect(builds.pluck(:status)).to contain_exactly('success', 'pending')
+ pipeline.builds.running_or_pending.each(&:success)
+
+ expect(builds.pluck(:name)).to contain_exactly('build', 'test', 'deploy')
+ expect(builds.pluck(:status)).to contain_exactly('success', 'success', 'pending')
+ pipeline.builds.running_or_pending.each(&:success)
+
+ expect(builds.pluck(:name)).to contain_exactly('build', 'test', 'deploy', 'cleanup')
+ expect(builds.pluck(:status)).to contain_exactly('success', 'success', 'success', 'pending')
+ pipeline.builds.running_or_pending.each(&:success)
+
+ expect(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(builds.pluck(:name)).to contain_exactly('build')
+ expect(builds.pluck(:status)).to contain_exactly('pending')
+ pipeline.builds.running_or_pending.each(&:success)
+
+ expect(builds.pluck(:name)).to contain_exactly('build', 'test')
+ expect(builds.pluck(:status)).to contain_exactly('success', 'pending')
+ pipeline.builds.running_or_pending.each(&:drop)
+
+ expect(builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure')
+ expect(builds.pluck(:status)).to contain_exactly('success', 'failed', 'pending')
+ pipeline.builds.running_or_pending.each(&:success)
+
+ expect(builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure', 'cleanup')
+ expect(builds.pluck(:status)).to contain_exactly('success', 'failed', 'success', 'pending')
+ pipeline.builds.running_or_pending.each(&:success)
+
+ expect(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(builds.pluck(:name)).to contain_exactly('build')
+ expect(builds.pluck(:status)).to contain_exactly('pending')
+ pipeline.builds.running_or_pending.each(&:success)
+
+ expect(builds.pluck(:name)).to contain_exactly('build', 'test')
+ expect(builds.pluck(:status)).to contain_exactly('success', 'pending')
+ pipeline.builds.running_or_pending.each(&:drop)
+
+ expect(builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure')
+ expect(builds.pluck(:status)).to contain_exactly('success', 'failed', 'pending')
+ pipeline.builds.running_or_pending.each(&:drop)
+
+ expect(builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure', 'cleanup')
+ expect(builds.pluck(:status)).to contain_exactly('success', 'failed', 'failed', 'pending')
+ pipeline.builds.running_or_pending.each(&:success)
+
+ expect(builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure', 'cleanup')
+ expect(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(builds.pluck(:name)).to contain_exactly('build')
+ expect(builds.pluck(:status)).to contain_exactly('pending')
+ pipeline.builds.running_or_pending.each(&:success)
+
+ expect(builds.pluck(:name)).to contain_exactly('build', 'test')
+ expect(builds.pluck(:status)).to contain_exactly('success', 'pending')
+ pipeline.builds.running_or_pending.each(&:success)
+
+ expect(builds.pluck(:name)).to contain_exactly('build', 'test', 'deploy')
+ expect(builds.pluck(:status)).to contain_exactly('success', 'success', 'pending')
+ pipeline.builds.running_or_pending.each(&:drop)
+
+ expect(builds.pluck(:name)).to contain_exactly('build', 'test', 'deploy', 'cleanup')
+ expect(builds.pluck(:status)).to contain_exactly('success', 'success', 'failed', 'pending')
+ pipeline.builds.running_or_pending.each(&:success)
+
+ expect(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(builds.pluck(:name)).to contain_exactly('build')
+ expect(builds.pluck(:status)).to contain_exactly('pending')
+ pipeline.builds.running_or_pending.each(&:success)
+
+ expect(builds.running_or_pending).not_to be_empty
+
+ expect(builds.pluck(:name)).to contain_exactly('build', 'test')
+ expect(builds.pluck(:status)).to contain_exactly('success', 'pending')
+ pipeline.builds.running_or_pending.each(&:cancel)
+
+ expect(builds.running_or_pending).to be_empty
+ expect(pipeline.reload.status).to eq('canceled')
+ end
+ end
+
+ context 'when listing manual actions' do
+ it 'returns only for skipped builds' do
+ # currently all builds are created
+ expect(create_builds).to be_truthy
+ expect(manual_actions).to be_empty
+
+ # succeed stage build
+ pipeline.builds.running_or_pending.each(&:success)
+ expect(manual_actions).to be_empty
+
+ # succeed stage test
+ pipeline.builds.running_or_pending.each(&:success)
+ expect(manual_actions).to be_one # production
+
+ # succeed stage deploy
+ pipeline.builds.running_or_pending.each(&:success)
+ expect(manual_actions).to be_many # production and clear cache
+ end
+
+ def manual_actions
+ pipeline.manual_actions
+ end
+ end
+ end
+
+ context 'when failed build in the middle stage is retried' do
+ context 'when failed build is the only unsuccessful build in the stage' do
+ before do
+ create(:ci_build, :created, pipeline: pipeline, name: 'build:1', stage_idx: 0)
+ create(:ci_build, :created, pipeline: pipeline, name: 'build:2', stage_idx: 0)
+ create(:ci_build, :created, pipeline: pipeline, name: 'test:1', stage_idx: 1)
+ create(:ci_build, :created, pipeline: pipeline, name: 'test:2', stage_idx: 1)
+ create(:ci_build, :created, pipeline: pipeline, name: 'deploy:1', stage_idx: 2)
+ create(:ci_build, :created, pipeline: pipeline, name: 'deploy:2', stage_idx: 2)
+ end
+
+ it 'does trigger builds in the next stage' do
+ expect(create_builds).to be_truthy
+ expect(builds.pluck(:name)).to contain_exactly('build:1', 'build:2')
+
+ pipeline.builds.running_or_pending.each(&:success)
+
+ expect(builds.pluck(:name))
+ .to contain_exactly('build:1', 'build:2', 'test:1', 'test:2')
+
+ pipeline.builds.find_by(name: 'test:1').success
+ pipeline.builds.find_by(name: 'test:2').drop
+
+ expect(builds.pluck(:name))
+ .to contain_exactly('build:1', 'build:2', 'test:1', 'test:2')
+
+ Ci::Build.retry(pipeline.builds.find_by(name: 'test:2')).success
+
+ expect(builds.pluck(:name)).to contain_exactly(
+ 'build:1', 'build:2', 'test:1', 'test:2', 'test:2', 'deploy:1', 'deploy:2')
+ end
+ end
+ end
+
+ context 'creates a builds from .gitlab-ci.yml' do
+ let(:config) do
+ YAML.dump({
+ rspec: {
+ stage: 'test',
+ script: 'rspec'
+ },
+ rubocop: {
+ stage: 'test',
+ script: 'rubocop'
+ },
+ deploy: {
+ stage: 'deploy',
+ script: 'deploy'
+ }
+ })
+ end
+
+ # Using stubbed .gitlab-ci.yml created in commit factory
+ #
+
+ before do
+ stub_ci_pipeline_yaml_file(config)
+ create(:ci_build, :created, pipeline: pipeline, name: 'linux', stage: 'build', stage_idx: 0)
+ create(:ci_build, :created, pipeline: pipeline, name: 'mac', stage: 'build', stage_idx: 0)
+ end
+
+ it 'when processing a pipeline' do
+ # Currently we have two builds with state created
+ expect(builds.count).to eq(0)
+ expect(all_builds.count).to eq(2)
+
+ # Create builds will mark the created as pending
+ expect(create_builds).to be_truthy
+ expect(builds.count).to eq(2)
+ expect(all_builds.count).to eq(2)
+
+ # When we builds succeed we will create a rest of pipeline from .gitlab-ci.yml
+ # We will have 2 succeeded, 2 pending (from stage test), total 5 (one more build from deploy)
+ succeed_pending
+ expect(create_builds).to be_truthy
+ expect(builds.success.count).to eq(2)
+ expect(builds.pending.count).to eq(2)
+ expect(all_builds.count).to eq(5)
+
+ # When we succeed the 2 pending from stage test,
+ # We will queue a deploy stage, no new builds will be created
+ succeed_pending
+ expect(create_builds).to be_truthy
+ expect(builds.pending.count).to eq(1)
+ expect(builds.success.count).to eq(4)
+ expect(all_builds.count).to eq(5)
+
+ # When we succeed last pending build, we will have a total of 5 succeeded builds, no new builds will be created
+ succeed_pending
+ expect(create_builds).to be_falsey
+ expect(builds.success.count).to eq(5)
+ expect(all_builds.count).to eq(5)
+ end
+ end
+ end
+end
diff --git a/spec/services/ci/register_build_service_spec.rb b/spec/services/ci/register_build_service_spec.rb
index f28f2f1438d..026d0ca6534 100644
--- a/spec/services/ci/register_build_service_spec.rb
+++ b/spec/services/ci/register_build_service_spec.rb
@@ -13,7 +13,7 @@ module Ci
specific_runner.assign_to(project)
end
- describe :execute do
+ describe '#execute' do
context 'runner follow tag list' do
it "picks build with the same tag" do
pending_build.tag_list = ["linux"]
diff --git a/spec/services/create_commit_builds_service_spec.rb b/spec/services/create_commit_builds_service_spec.rb
deleted file mode 100644
index 309213bd44c..00000000000
--- a/spec/services/create_commit_builds_service_spec.rb
+++ /dev/null
@@ -1,240 +0,0 @@
-require 'spec_helper'
-
-describe CreateCommitBuildsService, services: true do
- let(:service) { CreateCommitBuildsService.new }
- let(:project) { FactoryGirl.create(:empty_project) }
- let(:user) { nil }
-
- before do
- stub_ci_pipeline_to_return_yaml_file
- end
-
- describe :execute do
- context 'valid params' do
- let(:pipeline) do
- service.execute(project, user,
- ref: 'refs/heads/master',
- before: '00000000',
- after: '31das312',
- commits: [{ message: "Message" }]
- )
- end
-
- 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
- it "creates commit if there is appropriate job" do
- result = service.execute(project, user,
- ref: 'refs/tags/0_1',
- before: '00000000',
- after: '31das312',
- commits: [{ message: "Message" }]
- )
- expect(result).to be_persisted
- end
-
- it "creates commit if there is no appropriate job but deploy job has right ref setting" do
- config = YAML.dump({ deploy: { script: "ls", only: ["0_1"] } })
- stub_ci_pipeline_yaml_file(config)
-
- result = service.execute(project, user,
- ref: 'refs/heads/0_1',
- before: '00000000',
- after: '31das312',
- commits: [{ message: "Message" }]
- )
- expect(result).to be_persisted
- end
- end
-
- 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',
- after: '31das312',
- commits: [{ message: 'Message' }]
- )
- expect(result).to be_falsey
- expect(Ci::Pipeline.count).to eq(0)
- end
-
- it 'fails commits if yaml is invalid' do
- message = 'message'
- allow_any_instance_of(Ci::Pipeline).to receive(:git_commit_message) { message }
- stub_ci_pipeline_yaml_file('invalid: file: file')
- commits = [{ message: message }]
- 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
-
- context 'when commit contains a [ci skip] directive' do
- let(:message) { "some message[ci skip]" }
- let(:messageFlip) { "some message[skip ci]" }
- let(:capMessage) { "some message[CI SKIP]" }
- let(:capMessageFlip) { "some message[SKIP CI]" }
-
- before do
- 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 }]
- 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 "skips builds creation if there is [skip ci] tag in commit message" do
- commits = [{ message: messageFlip }]
- 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 "skips builds creation if there is [CI SKIP] tag in commit message" do
- commits = [{ message: capMessage }]
- 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 "skips builds creation if there is [SKIP CI] tag in commit message" do
- commits = [{ message: capMessageFlip }]
- 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] or [skip ci] tag in commit message" do
- allow_any_instance_of(Ci::Pipeline).to receive(:git_commit_message) { "some message" }
-
- commits = [{ message: "some message" }]
- 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_pipeline_yaml_file('invalid: file: fiile')
- commits = [{ message: message }]
- 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::Pipeline).to receive(:ci_yaml_file) { gitlab_ci_yaml }
-
- commits = [{ message: "message" }]
- 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)
-
- 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_pipeline_yaml_file('invalid: file')
-
- commits = [{ message: "some message" }]
-
- pipeline = service.execute(project, user,
- ref: 'refs/tags/0_1',
- before: '00000000',
- after: '31das312',
- commits: commits
- )
-
- expect(pipeline).to be_persisted
- expect(pipeline.status).to eq("failed")
- expect(pipeline.builds.any?).to be false
- end
-
- context 'when there are no jobs for this pipeline' do
- before do
- config = YAML.dump({ test: { script: 'ls', only: ['feature'] } })
- stub_ci_pipeline_yaml_file(config)
- end
-
- it 'does not create a new pipeline' do
- result = service.execute(project, user,
- ref: 'refs/heads/master',
- before: '00000000',
- after: '31das312',
- commits: [{ message: 'some msg' }])
-
- expect(result).to be_falsey
- expect(Ci::Build.all).to be_empty
- expect(Ci::Pipeline.count).to eq(0)
- end
- end
- end
-end
diff --git a/spec/services/create_deployment_service_spec.rb b/spec/services/create_deployment_service_spec.rb
index 654e441f3cd..8da2a2b3c1b 100644
--- a/spec/services/create_deployment_service_spec.rb
+++ b/spec/services/create_deployment_service_spec.rb
@@ -89,6 +89,12 @@ describe CreateDeploymentService, services: true do
expect_any_instance_of(described_class).to receive(:execute)
subject
end
+
+ it 'is set as deployable' do
+ subject
+
+ expect(Deployment.last.deployable).to eq(deployable)
+ end
end
context 'without environment specified' do
@@ -105,6 +111,8 @@ describe CreateDeploymentService, services: true do
context 'when build succeeds' do
it_behaves_like 'does create environment and deployment' do
+ let(:deployable) { build }
+
subject { build.success }
end
end
@@ -114,6 +122,14 @@ describe CreateDeploymentService, services: true do
subject { build.drop }
end
end
+
+ context 'when build is retried' do
+ it_behaves_like 'does create environment and deployment' do
+ let(:deployable) { Ci::Build.retry(build) }
+
+ subject { deployable.success }
+ end
+ end
end
end
end
diff --git a/spec/services/create_snippet_service_spec.rb b/spec/services/create_snippet_service_spec.rb
index 7a850066bf8..d81d0fd76c9 100644
--- a/spec/services/create_snippet_service_spec.rb
+++ b/spec/services/create_snippet_service_spec.rb
@@ -19,7 +19,7 @@ describe CreateSnippetService, services: true do
@opts.merge!(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
end
- it 'non-admins should not be able to create a public snippet' do
+ it 'non-admins are not able to create a public snippet' do
snippet = create_snippet(nil, @user, @opts)
expect(snippet.errors.messages).to have_key(:visibility_level)
expect(snippet.errors.messages[:visibility_level].first).to(
@@ -27,7 +27,7 @@ describe CreateSnippetService, services: true do
)
end
- it 'admins should be able to create a public snippet' do
+ it 'admins are able to create a public snippet' do
snippet = create_snippet(nil, @admin, @opts)
expect(snippet.errors.any?).to be_falsey
expect(snippet.visibility_level).to eq(Gitlab::VisibilityLevel::PUBLIC)
diff --git a/spec/services/delete_user_service_spec.rb b/spec/services/delete_user_service_spec.rb
index a65938fa03b..418a12a83a9 100644
--- a/spec/services/delete_user_service_spec.rb
+++ b/spec/services/delete_user_service_spec.rb
@@ -9,13 +9,15 @@ describe DeleteUserService, services: true do
context 'no options are given' do
it 'deletes the user' do
- DeleteUserService.new(current_user).execute(user)
+ user_data = DeleteUserService.new(current_user).execute(user)
- expect { User.find(user.id) }.to raise_error(ActiveRecord::RecordNotFound)
+ expect { user_data['email'].to eq(user.email) }
+ expect { User.find(user.id) }.to raise_error(ActiveRecord::RecordNotFound)
+ expect { Namespace.with_deleted.find(user.namespace.id) }.to raise_error(ActiveRecord::RecordNotFound)
end
it 'will delete the project in the near future' do
- expect_any_instance_of(Projects::DestroyService).to receive(:pending_delete!).once
+ expect_any_instance_of(Projects::DestroyService).to receive(:async_execute).once
DeleteUserService.new(current_user).execute(user)
end
diff --git a/spec/services/destroy_group_service_spec.rb b/spec/services/destroy_group_service_spec.rb
index eca8ddd8ea4..da724643604 100644
--- a/spec/services/destroy_group_service_spec.rb
+++ b/spec/services/destroy_group_service_spec.rb
@@ -7,38 +7,52 @@ describe DestroyGroupService, services: true do
let!(:gitlab_shell) { Gitlab::Shell.new }
let!(:remove_path) { group.path + "+#{group.id}+deleted" }
- context 'database records' do
- before do
- destroy_group(group, user)
+ shared_examples 'group destruction' do |async|
+ context 'database records' do
+ before do
+ destroy_group(group, user, async)
+ end
+
+ it { expect(Group.all).not_to include(group) }
+ it { expect(Project.all).not_to include(project) }
end
- it { expect(Group.all).not_to include(group) }
- it { expect(Project.all).not_to include(project) }
- end
+ context 'file system' do
+ context 'Sidekiq inline' do
+ before do
+ # Run sidekiq immediatly to check that renamed dir will be removed
+ Sidekiq::Testing.inline! { destroy_group(group, user, async) }
+ end
- context 'file system' do
- context 'Sidekiq inline' do
- before do
- # Run sidekiq immediatly to check that renamed dir will be removed
- Sidekiq::Testing.inline! { destroy_group(group, user) }
+ it { expect(gitlab_shell.exists?(project.repository_storage_path, group.path)).to be_falsey }
+ it { expect(gitlab_shell.exists?(project.repository_storage_path, remove_path)).to be_falsey }
end
- it { expect(gitlab_shell.exists?(project.repository_storage_path, group.path)).to be_falsey }
- it { expect(gitlab_shell.exists?(project.repository_storage_path, remove_path)).to be_falsey }
- end
+ context 'Sidekiq fake' do
+ before do
+ # Dont run sidekiq to check if renamed repository exists
+ Sidekiq::Testing.fake! { destroy_group(group, user, async) }
+ end
- context 'Sidekiq fake' do
- before do
- # Dont run sidekiq to check if renamed repository exists
- Sidekiq::Testing.fake! { destroy_group(group, user) }
+ it { expect(gitlab_shell.exists?(project.repository_storage_path, group.path)).to be_falsey }
+ it { expect(gitlab_shell.exists?(project.repository_storage_path, remove_path)).to be_truthy }
end
+ end
- it { expect(gitlab_shell.exists?(project.repository_storage_path, group.path)).to be_falsey }
- it { expect(gitlab_shell.exists?(project.repository_storage_path, remove_path)).to be_truthy }
+ def destroy_group(group, user, async)
+ if async
+ DestroyGroupService.new(group, user).async_execute
+ else
+ DestroyGroupService.new(group, user).execute
+ end
end
end
- def destroy_group(group, user)
- DestroyGroupService.new(group, user).execute
+ describe 'asynchronous delete' do
+ it_behaves_like 'group destruction', true
+ end
+
+ describe 'synchronous delete' do
+ it_behaves_like 'group destruction', false
end
end
diff --git a/spec/services/event_create_service_spec.rb b/spec/services/event_create_service_spec.rb
index f6dc9d4008f..16a9956fe7f 100644
--- a/spec/services/event_create_service_spec.rb
+++ b/spec/services/event_create_service_spec.rb
@@ -4,7 +4,7 @@ describe EventCreateService, services: true do
let(:service) { EventCreateService.new }
describe 'Issues' do
- describe :open_issue do
+ describe '#open_issue' do
let(:issue) { create(:issue) }
it { expect(service.open_issue(issue, issue.author)).to be_truthy }
@@ -14,7 +14,7 @@ describe EventCreateService, services: true do
end
end
- describe :close_issue do
+ describe '#close_issue' do
let(:issue) { create(:issue) }
it { expect(service.close_issue(issue, issue.author)).to be_truthy }
@@ -24,7 +24,7 @@ describe EventCreateService, services: true do
end
end
- describe :reopen_issue do
+ describe '#reopen_issue' do
let(:issue) { create(:issue) }
it { expect(service.reopen_issue(issue, issue.author)).to be_truthy }
@@ -36,42 +36,42 @@ describe EventCreateService, services: true do
end
describe 'Merge Requests' do
- describe :open_mr do
+ describe '#open_mr' do
let(:merge_request) { create(:merge_request) }
it { expect(service.open_mr(merge_request, merge_request.author)).to be_truthy }
- it "should create new event" do
+ it "creates new event" do
expect { service.open_mr(merge_request, merge_request.author) }.to change { Event.count }
end
end
- describe :close_mr do
+ describe '#close_mr' do
let(:merge_request) { create(:merge_request) }
it { expect(service.close_mr(merge_request, merge_request.author)).to be_truthy }
- it "should create new event" do
+ it "creates new event" do
expect { service.close_mr(merge_request, merge_request.author) }.to change { Event.count }
end
end
- describe :merge_mr do
+ describe '#merge_mr' do
let(:merge_request) { create(:merge_request) }
it { expect(service.merge_mr(merge_request, merge_request.author)).to be_truthy }
- it "should create new event" do
+ it "creates new event" do
expect { service.merge_mr(merge_request, merge_request.author) }.to change { Event.count }
end
end
- describe :reopen_mr do
+ describe '#reopen_mr' do
let(:merge_request) { create(:merge_request) }
it { expect(service.reopen_mr(merge_request, merge_request.author)).to be_truthy }
- it "should create new event" do
+ it "creates new event" do
expect { service.reopen_mr(merge_request, merge_request.author) }.to change { Event.count }
end
end
@@ -80,32 +80,32 @@ describe EventCreateService, services: true do
describe 'Milestone' do
let(:user) { create :user }
- describe :open_milestone do
+ describe '#open_milestone' do
let(:milestone) { create(:milestone) }
it { expect(service.open_milestone(milestone, user)).to be_truthy }
- it "should create new event" do
+ it "creates new event" do
expect { service.open_milestone(milestone, user) }.to change { Event.count }
end
end
- describe :close_mr do
+ describe '#close_mr' do
let(:milestone) { create(:milestone) }
it { expect(service.close_milestone(milestone, user)).to be_truthy }
- it "should create new event" do
+ it "creates new event" do
expect { service.close_milestone(milestone, user) }.to change { Event.count }
end
end
- describe :destroy_mr do
+ describe '#destroy_mr' do
let(:milestone) { create(:milestone) }
it { expect(service.destroy_milestone(milestone, user)).to be_truthy }
- it "should create new event" do
+ it "creates new event" do
expect { service.destroy_milestone(milestone, user) }.to change { Event.count }
end
end
diff --git a/spec/services/files/update_service_spec.rb b/spec/services/files/update_service_spec.rb
new file mode 100644
index 00000000000..d019e50649f
--- /dev/null
+++ b/spec/services/files/update_service_spec.rb
@@ -0,0 +1,84 @@
+require "spec_helper"
+
+describe Files::UpdateService do
+ subject { described_class.new(project, user, commit_params) }
+
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+ let(:file_path) { 'files/ruby/popen.rb' }
+ let(:new_contents) { "New Content" }
+ let(:commit_params) do
+ {
+ file_path: file_path,
+ commit_message: "Update File",
+ file_content: new_contents,
+ file_content_encoding: "text",
+ last_commit_sha: last_commit_sha,
+ source_project: project,
+ source_branch: project.default_branch,
+ target_branch: project.default_branch,
+ }
+ end
+
+ before do
+ project.team << [user, :master]
+ end
+
+ describe "#execute" do
+ context "when the file's last commit sha does not match the supplied last_commit_sha" do
+ let(:last_commit_sha) { "foo" }
+
+ it "returns a hash with the correct error message and a :error status " do
+ expect { subject.execute }.
+ to raise_error(Files::UpdateService::FileChangedError,
+ "You are attempting to update a file that has changed since you started editing it.")
+ end
+ end
+
+ context "when the file's last commit sha does match the supplied last_commit_sha" do
+ let(:last_commit_sha) { Gitlab::Git::Commit.last_for_path(project.repository, project.default_branch, file_path).sha }
+
+ it "returns a hash with the :success status " do
+ results = subject.execute
+
+ expect(results).to match({ status: :success })
+ end
+
+ it "updates the file with the new contents" do
+ subject.execute
+
+ results = project.repository.blob_at_branch(project.default_branch, file_path)
+
+ expect(results.data).to eq(new_contents)
+ end
+ end
+
+ context "when the last_commit_sha is not supplied" do
+ let(:commit_params) do
+ {
+ file_path: file_path,
+ commit_message: "Update File",
+ file_content: new_contents,
+ file_content_encoding: "text",
+ source_project: project,
+ source_branch: project.default_branch,
+ target_branch: project.default_branch,
+ }
+ end
+
+ it "returns a hash with the :success status " do
+ results = subject.execute
+
+ expect(results).to match({ status: :success })
+ end
+
+ it "updates the file with the new contents" do
+ subject.execute
+
+ results = project.repository.blob_at_branch(project.default_branch, file_path)
+
+ expect(results.data).to eq(new_contents)
+ end
+ end
+ end
+end
diff --git a/spec/services/git_hooks_service_spec.rb b/spec/services/git_hooks_service_spec.rb
index 3fc37a315c0..41b0968b8b4 100644
--- a/spec/services/git_hooks_service_spec.rb
+++ b/spec/services/git_hooks_service_spec.rb
@@ -17,7 +17,7 @@ describe GitHooksService, services: true do
describe '#execute' do
context 'when receive hooks were successful' do
- it 'should call post-receive hook' do
+ it 'calls post-receive hook' do
hook = double(trigger: [true, nil])
expect(Gitlab::Git::Hook).to receive(:new).exactly(3).times.and_return(hook)
@@ -26,7 +26,7 @@ describe GitHooksService, services: true do
end
context 'when pre-receive hook failed' do
- it 'should not call post-receive hook' do
+ it 'does not call post-receive hook' do
expect(service).to receive(:run_hook).with('pre-receive').and_return([false, ''])
expect(service).not_to receive(:run_hook).with('post-receive')
@@ -37,7 +37,7 @@ describe GitHooksService, services: true do
end
context 'when update hook failed' do
- it 'should not call post-receive hook' do
+ it 'does not call post-receive hook' do
expect(service).to receive(:run_hook).with('pre-receive').and_return([true, nil])
expect(service).to receive(:run_hook).with('update').and_return([false, ''])
expect(service).not_to receive(:run_hook).with('post-receive')
diff --git a/spec/services/git_push_service_spec.rb b/spec/services/git_push_service_spec.rb
index afabeed4a80..6ac1fa8f182 100644
--- a/spec/services/git_push_service_spec.rb
+++ b/spec/services/git_push_service_spec.rb
@@ -7,6 +7,7 @@ describe GitPushService, services: true do
let(:project) { create :project }
before do
+ project.team << [user, :master]
@blankrev = Gitlab::Git::BLANK_SHA
@oldrev = sample_commit.parent_id
@newrev = sample_commit.id
@@ -172,7 +173,7 @@ describe GitPushService, services: true do
describe "Push Event" do
before do
service = execute_service(project, user, @oldrev, @newrev, @ref )
- @event = Event.last
+ @event = Event.find_by_action(Event::PUSHED)
@push_data = service.push_data
end
@@ -224,8 +225,10 @@ describe GitPushService, services: true do
it "when pushing a branch for the first time" do
expect(project).to receive(:execute_hooks)
expect(project.default_branch).to eq("master")
- expect(project.protected_branches).to receive(:create).with({ name: "master", developers_can_push: false })
execute_service(project, user, @blankrev, 'newrev', 'refs/heads/master' )
+ expect(project.protected_branches).not_to be_empty
+ expect(project.protected_branches.first.push_access_levels.map(&:access_level)).to eq([Gitlab::Access::MASTER])
+ expect(project.protected_branches.first.merge_access_levels.map(&:access_level)).to eq([Gitlab::Access::MASTER])
end
it "when pushing a branch for the first time with default branch protection disabled" do
@@ -233,8 +236,8 @@ describe GitPushService, services: true do
expect(project).to receive(:execute_hooks)
expect(project.default_branch).to eq("master")
- expect(project.protected_branches).not_to receive(:create)
execute_service(project, user, @blankrev, 'newrev', 'refs/heads/master' )
+ expect(project.protected_branches).to be_empty
end
it "when pushing a branch for the first time with default branch protection set to 'developers can push'" do
@@ -242,8 +245,23 @@ describe GitPushService, services: true do
expect(project).to receive(:execute_hooks)
expect(project.default_branch).to eq("master")
- expect(project.protected_branches).to receive(:create).with({ name: "master", developers_can_push: true })
+
+ execute_service(project, user, @blankrev, 'newrev', 'refs/heads/master' )
+
+ expect(project.protected_branches).not_to be_empty
+ expect(project.protected_branches.last.push_access_levels.map(&:access_level)).to eq([Gitlab::Access::DEVELOPER])
+ expect(project.protected_branches.last.merge_access_levels.map(&:access_level)).to eq([Gitlab::Access::MASTER])
+ end
+
+ it "when pushing a branch for the first time with default branch protection set to 'developers can merge'" do
+ stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_DEV_CAN_MERGE)
+
+ expect(project).to receive(:execute_hooks)
+ expect(project.default_branch).to eq("master")
execute_service(project, user, @blankrev, 'newrev', 'refs/heads/master' )
+ expect(project.protected_branches).not_to be_empty
+ expect(project.protected_branches.first.push_access_levels.map(&:access_level)).to eq([Gitlab::Access::MASTER])
+ expect(project.protected_branches.first.merge_access_levels.map(&:access_level)).to eq([Gitlab::Access::DEVELOPER])
end
it "when pushing new commits to existing branch" do
@@ -402,7 +420,7 @@ describe GitPushService, services: true do
context "mentioning an issue" do
let(:message) { "this is some work.\n\nrelated to JIRA-1" }
- it "should initiate one api call to jira server to mention the issue" do
+ it "initiates one api call to jira server to mention the issue" do
execute_service(project, user, @oldrev, @newrev, @ref )
expect(WebMock).to have_requested(:post, jira_api_comment_url).with(
@@ -414,7 +432,7 @@ describe GitPushService, services: true do
context "closing an issue" do
let(:message) { "this is some work.\n\ncloses JIRA-1" }
- it "should initiate one api call to jira server to close the issue" do
+ it "initiates one api call to jira server to close the issue" do
transition_body = {
transition: {
id: '2'
@@ -427,7 +445,7 @@ describe GitPushService, services: true do
).once
end
- it "should initiate one api call to jira server to comment on the issue" do
+ it "initiates one api call to jira server to comment on the issue" do
comment_body = {
body: "Issue solved with [#{closing_commit.id}|http://localhost/#{project.path_with_namespace}/commit/#{closing_commit.id}]."
}.to_json
diff --git a/spec/services/import_export_clean_up_service_spec.rb b/spec/services/import_export_clean_up_service_spec.rb
new file mode 100644
index 00000000000..81b1d327696
--- /dev/null
+++ b/spec/services/import_export_clean_up_service_spec.rb
@@ -0,0 +1,64 @@
+require 'spec_helper'
+
+describe ImportExportCleanUpService, services: true do
+ describe '#execute' do
+ let(:service) { described_class.new }
+
+ let(:tmp_import_export_folder) { 'tmp/project_exports' }
+
+ context 'when the import/export directory does not exist' do
+ it 'does not remove any archives' do
+ path = '/invalid/path/'
+ stub_repository_downloads_path(path)
+
+ expect(File).to receive(:directory?).with(path + tmp_import_export_folder).and_return(false).at_least(:once)
+ expect(service).not_to receive(:clean_up_export_files)
+
+ service.execute
+ end
+ end
+
+ context 'when the import/export directory exists' do
+ it 'removes old files' do
+ in_directory_with_files(mtime: 2.days.ago) do |dir, files|
+ service.execute
+
+ files.each { |file| expect(File.exist?(file)).to eq false }
+ expect(File.directory?(dir)).to eq false
+ end
+ end
+
+ it 'does not remove new files' do
+ in_directory_with_files(mtime: 2.hours.ago) do |dir, files|
+ service.execute
+
+ files.each { |file| expect(File.exist?(file)).to eq true }
+ expect(File.directory?(dir)).to eq true
+ end
+ end
+ end
+
+ def in_directory_with_files(mtime:)
+ Dir.mktmpdir do |tmpdir|
+ stub_repository_downloads_path(tmpdir)
+ dir = File.join(tmpdir, tmp_import_export_folder, 'subfolder')
+ FileUtils.mkdir_p(dir)
+
+ files = FileUtils.touch(file_list(dir) + [dir], mtime: mtime.to_time)
+
+ yield(dir, files)
+ end
+ end
+
+ def stub_repository_downloads_path(path)
+ new_shared_settings = Settings.shared.merge('path' => path)
+ allow(Settings).to receive(:shared).and_return(new_shared_settings)
+ end
+
+ def file_list(dir)
+ Array.new(5) do |num|
+ File.join(dir, "random-#{num}.tar.gz")
+ end
+ end
+ end
+end
diff --git a/spec/services/issues/bulk_update_service_spec.rb b/spec/services/issues/bulk_update_service_spec.rb
index 4a689e64dc5..ac08aa53b0b 100644
--- a/spec/services/issues/bulk_update_service_spec.rb
+++ b/spec/services/issues/bulk_update_service_spec.rb
@@ -1,118 +1,106 @@
require 'spec_helper'
describe Issues::BulkUpdateService, services: true do
- let(:user) { create(:user) }
- let(:project) { Projects::CreateService.new(user, namespace: user.namespace, name: 'test').execute }
+ let(:user) { create(:user) }
+ let(:project) { create(:empty_project, namespace: user.namespace) }
- let!(:result) { Issues::BulkUpdateService.new(project, user, params).execute }
+ def bulk_update(issues, extra_params = {})
+ bulk_update_params = extra_params
+ .reverse_merge(issues_ids: Array(issues).map(&:id).join(','))
- describe :close_issue do
- let(:issues) { create_list(:issue, 5, project: project) }
- let(:params) do
- {
- state_event: 'close',
- issues_ids: issues.map(&:id).join(',')
- }
- end
+ Issues::BulkUpdateService.new(project, user, bulk_update_params).execute
+ end
+
+ describe 'close issues' do
+ let(:issues) { create_list(:issue, 2, project: project) }
it 'succeeds and returns the correct number of issues updated' do
+ result = bulk_update(issues, state_event: 'close')
+
expect(result[:success]).to be_truthy
expect(result[:count]).to eq(issues.count)
end
it 'closes all the issues passed' do
+ bulk_update(issues, state_event: 'close')
+
expect(project.issues.opened).to be_empty
expect(project.issues.closed).not_to be_empty
end
end
- describe :reopen_issues do
- let(:issues) { create_list(:closed_issue, 5, project: project) }
- let(:params) do
- {
- state_event: 'reopen',
- issues_ids: issues.map(&:id).join(',')
- }
- end
+ describe 'reopen issues' do
+ let(:issues) { create_list(:closed_issue, 2, project: project) }
it 'succeeds and returns the correct number of issues updated' do
+ result = bulk_update(issues, state_event: 'reopen')
+
expect(result[:success]).to be_truthy
expect(result[:count]).to eq(issues.count)
end
it 'reopens all the issues passed' do
+ bulk_update(issues, state_event: 'reopen')
+
expect(project.issues.closed).to be_empty
expect(project.issues.opened).not_to be_empty
end
end
describe 'updating assignee' do
- let(:issue) do
- create(:issue, project: project) { |issue| issue.update_attributes(assignee: user) }
- end
-
- let(:params) do
- {
- assignee_id: assignee_id,
- issues_ids: issue.id.to_s
- }
- end
+ let(:issue) { create(:issue, project: project, assignee: user) }
context 'when the new assignee ID is a valid user' do
- let(:new_assignee) { create(:user) }
- let(:assignee_id) { new_assignee.id }
-
it 'succeeds' do
+ result = bulk_update(issue, assignee_id: create(:user).id)
+
expect(result[:success]).to be_truthy
expect(result[:count]).to eq(1)
end
it 'updates the assignee to the use ID passed' do
- expect(issue.reload.assignee).to eq(new_assignee)
+ assignee = create(:user)
+
+ expect { bulk_update(issue, assignee_id: assignee.id) }
+ .to change { issue.reload.assignee }.from(user).to(assignee)
end
end
context 'when the new assignee ID is -1' do
- let(:assignee_id) { -1 }
-
it 'unassigns the issues' do
- expect(issue.reload.assignee).to be_nil
+ expect { bulk_update(issue, assignee_id: -1) }
+ .to change { issue.reload.assignee }.to(nil)
end
end
context 'when the new assignee ID is not present' do
- let(:assignee_id) { nil }
-
it 'does not unassign' do
- expect(issue.reload.assignee).to eq(user)
+ expect { bulk_update(issue, assignee_id: nil) }
+ .not_to change { issue.reload.assignee }
end
end
end
describe 'updating milestones' do
- let(:issue) { create(:issue, project: project) }
+ let(:issue) { create(:issue, project: project) }
let(:milestone) { create(:milestone, project: project) }
- let(:params) do
- {
- issues_ids: issue.id.to_s,
- milestone_id: milestone.id
- }
- end
-
it 'succeeds' do
+ result = bulk_update(issue, milestone_id: milestone.id)
+
expect(result[:success]).to be_truthy
expect(result[:count]).to eq(1)
end
it 'updates the issue milestone' do
- expect(project.issues.first.milestone).to eq(milestone)
+ expect { bulk_update(issue, milestone_id: milestone.id) }
+ .to change { issue.reload.milestone }.from(nil).to(milestone)
end
end
describe 'updating labels' do
def create_issue_with_labels(labels)
- create(:issue, project: project) { |issue| issue.update_attributes(labels: labels) }
+ create(:labeled_issue, project: project, labels: labels)
end
let(:bug) { create(:label, project: project) }
@@ -129,15 +117,18 @@ describe Issues::BulkUpdateService, services: true do
let(:add_labels) { [] }
let(:remove_labels) { [] }
- let(:params) do
+ let(:bulk_update_params) do
{
- label_ids: labels.map(&:id),
- add_label_ids: add_labels.map(&:id),
+ 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
+ before do
+ bulk_update(issues, bulk_update_params)
+ end
+
context 'when label_ids are passed' do
let(:issues) { [issue_all_labels, issue_no_labels] }
let(:labels) { [bug, regression] }
@@ -226,7 +217,7 @@ describe Issues::BulkUpdateService, services: true do
let(:labels) { [merge_requests] }
let(:remove_labels) { [regression] }
- it 'remove the label IDs from all issues passed' do
+ it 'removes the label IDs from all issues passed' do
expect(issues.map(&:reload).map(&:label_ids).flatten).not_to include(regression.id)
end
@@ -262,4 +253,30 @@ describe Issues::BulkUpdateService, services: true do
end
end
end
+
+ describe 'subscribe to issues' do
+ let(:issues) { create_list(:issue, 2, project: project) }
+
+ it 'subscribes the given user' do
+ bulk_update(issues, subscription_event: 'subscribe')
+
+ expect(issues).to all(be_subscribed(user))
+ end
+ end
+
+ describe 'unsubscribe from issues' do
+ let(:issues) do
+ create_list(:closed_issue, 2, project: project) do |issue|
+ issue.subscriptions.create(user: user, subscribed: true)
+ end
+ end
+
+ it 'unsubscribes the given user' do
+ bulk_update(issues, subscription_event: 'unsubscribe')
+
+ issues.each do |issue|
+ expect(issue).not_to be_subscribed(user)
+ end
+ end
+ end
end
diff --git a/spec/services/issues/close_service_spec.rb b/spec/services/issues/close_service_spec.rb
index 62b25709a5d..aff022a573e 100644
--- a/spec/services/issues/close_service_spec.rb
+++ b/spec/services/issues/close_service_spec.rb
@@ -3,6 +3,7 @@ require 'spec_helper'
describe Issues::CloseService, services: true do
let(:user) { create(:user) }
let(:user2) { create(:user) }
+ let(:guest) { create(:user) }
let(:issue) { create(:issue, assignee: user2) }
let(:project) { issue.project }
let!(:todo) { create(:todo, :assigned, user: user, project: project, target: issue, author: user2) }
@@ -10,26 +11,27 @@ describe Issues::CloseService, services: true do
before do
project.team << [user, :master]
project.team << [user2, :developer]
+ project.team << [guest, :guest]
end
- describe :execute do
+ describe '#execute' do
context "valid params" do
before do
perform_enqueued_jobs do
- @issue = Issues::CloseService.new(project, user, {}).execute(issue)
+ @issue = described_class.new(project, user, {}).execute(issue)
end
end
it { expect(@issue).to be_valid }
it { expect(@issue).to be_closed }
- it 'should send email to user2 about assign of new issue' do
+ it 'sends email to user2 about assign of new issue' do
email = ActionMailer::Base.deliveries.last
expect(email.to.first).to eq(user2.email)
expect(email.subject).to include(issue.title)
end
- it 'should create system note about issue reassign' do
+ it 'creates system note about issue reassign' do
note = @issue.notes.last
expect(note.note).to include "Status changed to closed"
end
@@ -39,10 +41,22 @@ describe Issues::CloseService, services: true do
end
end
+ context 'current user is not authorized to close issue' do
+ before do
+ perform_enqueued_jobs do
+ @issue = described_class.new(project, guest).execute(issue)
+ end
+ end
+
+ it 'does not close the issue' do
+ expect(@issue).to be_open
+ end
+ end
+
context "external issue tracker" do
before do
allow(project).to receive(:default_issues_tracker?).and_return(false)
- @issue = Issues::CloseService.new(project, user, {}).execute(issue)
+ @issue = described_class.new(project, user, {}).execute(issue)
end
it { expect(@issue).to be_valid }
diff --git a/spec/services/issues/create_service_spec.rb b/spec/services/issues/create_service_spec.rb
index 1ee9f3aae4d..fcc3c0a00bd 100644
--- a/spec/services/issues/create_service_spec.rb
+++ b/spec/services/issues/create_service_spec.rb
@@ -73,5 +73,7 @@ describe Issues::CreateService, services: true do
end
end
end
+
+ it_behaves_like 'new issuable record that supports slash commands'
end
end
diff --git a/spec/services/issues/reopen_service_spec.rb b/spec/services/issues/reopen_service_spec.rb
new file mode 100644
index 00000000000..34a89fcd4e1
--- /dev/null
+++ b/spec/services/issues/reopen_service_spec.rb
@@ -0,0 +1,25 @@
+require 'spec_helper'
+
+describe Issues::ReopenService, services: true do
+ let(:guest) { create(:user) }
+ let(:issue) { create(:issue, :closed) }
+ let(:project) { issue.project }
+
+ before do
+ project.team << [guest, :guest]
+ end
+
+ describe '#execute' do
+ context 'current user is not authorized to reopen issue' do
+ before do
+ perform_enqueued_jobs do
+ @issue = described_class.new(project, guest).execute(issue)
+ end
+ end
+
+ it 'does not reopen the issue' do
+ expect(@issue).to be_closed
+ end
+ end
+ end
+end
diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb
index dacbcd8fb46..0313f424463 100644
--- a/spec/services/issues/update_service_spec.rb
+++ b/spec/services/issues/update_service_spec.rb
@@ -53,7 +53,7 @@ describe Issues::UpdateService, services: true do
it { expect(@issue.labels.count).to eq(1) }
it { expect(@issue.labels.first.title).to eq(label.name) }
- it 'should send email to user2 about assign of new issue and email to user3 about issue unassignment' do
+ it 'sends email to user2 about assign of new issue and email to user3 about issue unassignment' do
deliveries = ActionMailer::Base.deliveries
email = deliveries.last
recipients = deliveries.last(2).map(&:to).flatten
@@ -61,14 +61,14 @@ describe Issues::UpdateService, services: true do
expect(email.subject).to include(issue.title)
end
- it 'should create system note about issue reassign' do
+ it 'creates system note about issue reassign' do
note = find_note('Reassigned to')
expect(note).not_to be_nil
expect(note.note).to include "Reassigned to \@#{user2.username}"
end
- it 'should create system note about issue label edit' do
+ it 'creates system note about issue label edit' do
note = find_note('Added ~')
expect(note).not_to be_nil
@@ -267,7 +267,7 @@ describe Issues::UpdateService, services: true do
expect(note).to be_nil
end
- it 'should not generate a new note at all' do
+ it 'does not generate a new note at all' do
expect do
update_issue({ description: "- [ ] One\n- [ ] Two\n- [ ] Three" })
end.not_to change { Note.count }
@@ -319,5 +319,10 @@ describe Issues::UpdateService, services: true do
end
end
end
+
+ context 'updating mentions' do
+ let(:mentionable) { issue }
+ include_examples 'updating mentions', Issues::UpdateService
+ end
end
end
diff --git a/spec/services/merge_requests/build_service_spec.rb b/spec/services/merge_requests/build_service_spec.rb
index 782d74ec5ec..232508cda23 100644
--- a/spec/services/merge_requests/build_service_spec.rb
+++ b/spec/services/merge_requests/build_service_spec.rb
@@ -61,7 +61,7 @@ describe MergeRequests::BuildService, services: true do
end
context 'one commit in the diff' do
- let(:commits) { [commit_1] }
+ let(:commits) { Commit.decorate([commit_1], project) }
it 'allows the merge request to be created' do
expect(merge_request.can_be_created).to eq(true)
@@ -84,7 +84,7 @@ describe MergeRequests::BuildService, services: true do
end
context 'commit has no description' do
- let(:commits) { [commit_2] }
+ let(:commits) { Commit.decorate([commit_2], project) }
it 'uses the title of the commit as the title of the merge request' do
expect(merge_request.title).to eq(commit_2.safe_message)
@@ -111,7 +111,7 @@ describe MergeRequests::BuildService, services: true do
end
context 'commit has no description' do
- let(:commits) { [commit_2] }
+ let(:commits) { Commit.decorate([commit_2], project) }
it 'sets the description to "Closes #$issue-iid"' do
expect(merge_request.description).to eq("Closes ##{issue.iid}")
@@ -121,7 +121,7 @@ describe MergeRequests::BuildService, services: true do
end
context 'more than one commit in the diff' do
- let(:commits) { [commit_1, commit_2] }
+ let(:commits) { Commit.decorate([commit_1, commit_2], project) }
it 'allows the merge request to be created' do
expect(merge_request.can_be_created).to eq(true)
diff --git a/spec/services/merge_requests/close_service_spec.rb b/spec/services/merge_requests/close_service_spec.rb
index 8443a00e70c..24c25e4350f 100644
--- a/spec/services/merge_requests/close_service_spec.rb
+++ b/spec/services/merge_requests/close_service_spec.rb
@@ -3,6 +3,7 @@ require 'spec_helper'
describe MergeRequests::CloseService, services: true do
let(:user) { create(:user) }
let(:user2) { create(:user) }
+ let(:guest) { create(:user) }
let(:merge_request) { create(:merge_request, assignee: user2) }
let(:project) { merge_request.project }
let!(:todo) { create(:todo, :assigned, user: user, project: project, target: merge_request, author: user2) }
@@ -10,11 +11,12 @@ describe MergeRequests::CloseService, services: true do
before do
project.team << [user, :master]
project.team << [user2, :developer]
+ project.team << [guest, :guest]
end
- describe :execute do
+ describe '#execute' do
context 'valid params' do
- let(:service) { MergeRequests::CloseService.new(project, user, {}) }
+ let(:service) { described_class.new(project, user, {}) }
before do
allow(service).to receive(:execute_hooks)
@@ -32,13 +34,13 @@ describe MergeRequests::CloseService, services: true do
with(@merge_request, 'close')
end
- it 'should send email to user2 about assign of new merge_request' do
+ it 'sends email to user2 about assign of new merge_request' do
email = ActionMailer::Base.deliveries.last
expect(email.to.first).to eq(user2.email)
expect(email.subject).to include(merge_request.title)
end
- it 'should create system note about merge_request reassign' do
+ it 'creates system note about merge_request reassign' do
note = @merge_request.notes.last
expect(note.note).to include 'Status changed to closed'
end
@@ -47,5 +49,17 @@ describe MergeRequests::CloseService, services: true do
expect(todo.reload).to be_done
end
end
+
+ context 'current user is not authorized to close merge request' do
+ before do
+ perform_enqueued_jobs do
+ @merge_request = described_class.new(project, guest).execute(merge_request)
+ end
+ end
+
+ it 'does not close the merge request' do
+ expect(@merge_request).to be_open
+ 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 e433f49872d..c1e4f8bd96b 100644
--- a/spec/services/merge_requests/create_service_spec.rb
+++ b/spec/services/merge_requests/create_service_spec.rb
@@ -5,7 +5,7 @@ describe MergeRequests::CreateService, services: true do
let(:user) { create(:user) }
let(:assignee) { create(:user) }
- describe :execute do
+ describe '#execute' do
context 'valid params' do
let(:opts) do
{
@@ -17,7 +17,7 @@ describe MergeRequests::CreateService, services: true do
}
end
- let(:service) { MergeRequests::CreateService.new(project, user, opts) }
+ let(:service) { described_class.new(project, user, opts) }
before do
project.team << [user, :master]
@@ -32,7 +32,7 @@ describe MergeRequests::CreateService, services: true do
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
+ it 'executes hooks with default action' do
expect(service).to have_received(:execute_hooks).with(@merge_request)
end
@@ -74,5 +74,14 @@ describe MergeRequests::CreateService, services: true do
end
end
end
+
+ it_behaves_like 'new issuable record that supports slash commands' do
+ let(:default_params) do
+ {
+ source_branch: 'feature',
+ target_branch: 'master'
+ }
+ end
+ end
end
end
diff --git a/spec/services/merge_requests/get_urls_service_spec.rb b/spec/services/merge_requests/get_urls_service_spec.rb
new file mode 100644
index 00000000000..8a4b76367e3
--- /dev/null
+++ b/spec/services/merge_requests/get_urls_service_spec.rb
@@ -0,0 +1,134 @@
+require "spec_helper"
+
+describe MergeRequests::GetUrlsService do
+ let(:project) { create(:project, :public) }
+ let(:service) { MergeRequests::GetUrlsService.new(project) }
+ let(:source_branch) { "my_branch" }
+ let(:new_merge_request_url) { "http://localhost/#{project.namespace.name}/#{project.path}/merge_requests/new?merge_request%5Bsource_branch%5D=#{source_branch}" }
+ let(:show_merge_request_url) { "http://localhost/#{project.namespace.name}/#{project.path}/merge_requests/#{merge_request.iid}" }
+ let(:new_branch_changes) { "#{Gitlab::Git::BLANK_SHA} 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/#{source_branch}" }
+ let(:deleted_branch_changes) { "d14d6c0abdd253381df51a723d58691b2ee1ab08 #{Gitlab::Git::BLANK_SHA} refs/heads/#{source_branch}" }
+ let(:existing_branch_changes) { "d14d6c0abdd253381df51a723d58691b2ee1ab08 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/#{source_branch}" }
+ let(:default_branch_changes) { "d14d6c0abdd253381df51a723d58691b2ee1ab08 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/master" }
+
+ describe "#execute" do
+ shared_examples 'new_merge_request_link' do
+ it 'returns url to create new merge request' do
+ result = service.execute(changes)
+ expect(result).to match([{
+ branch_name: source_branch,
+ url: new_merge_request_url,
+ new_merge_request: true
+ }])
+ end
+ end
+
+ shared_examples 'show_merge_request_url' do
+ it 'returns url to view merge request' do
+ result = service.execute(changes)
+ expect(result).to match([{
+ branch_name: source_branch,
+ url: show_merge_request_url,
+ new_merge_request: false
+ }])
+ end
+ end
+
+ shared_examples 'no_merge_request_url' do
+ it 'returns no URL' do
+ result = service.execute(changes)
+ expect(result).to be_empty
+ end
+ end
+
+ context 'pushing to default branch' do
+ let(:changes) { default_branch_changes }
+ it_behaves_like 'no_merge_request_url'
+ end
+
+ context 'pushing to project with MRs disabled' do
+ let(:changes) { new_branch_changes }
+
+ before do
+ project.merge_requests_enabled = false
+ end
+
+ it_behaves_like 'no_merge_request_url'
+ end
+
+ context 'pushing one completely new branch' do
+ let(:changes) { new_branch_changes }
+ it_behaves_like 'new_merge_request_link'
+ end
+
+ context 'pushing to existing branch but no merge request' do
+ let(:changes) { existing_branch_changes }
+ it_behaves_like 'new_merge_request_link'
+ end
+
+ context 'pushing to deleted branch' do
+ let(:changes) { deleted_branch_changes }
+ it_behaves_like 'no_merge_request_url'
+ end
+
+ context 'pushing to existing branch and merge request opened' do
+ let!(:merge_request) { create(:merge_request, source_project: project, source_branch: source_branch) }
+ let(:changes) { existing_branch_changes }
+ it_behaves_like 'show_merge_request_url'
+ end
+
+ context 'pushing to existing branch and merge request is reopened' do
+ let!(:merge_request) { create(:merge_request, :reopened, source_project: project, source_branch: source_branch) }
+ let(:changes) { existing_branch_changes }
+ it_behaves_like 'show_merge_request_url'
+ end
+
+ context 'pushing to existing branch from forked project' do
+ let(:user) { create(:user) }
+ let!(:forked_project) { Projects::ForkService.new(project, user).execute }
+ let!(:merge_request) { create(:merge_request, source_project: forked_project, target_project: project, source_branch: source_branch) }
+ let(:changes) { existing_branch_changes }
+ # Source project is now the forked one
+ let(:service) { MergeRequests::GetUrlsService.new(forked_project) }
+
+ before do
+ allow(forked_project).to receive(:empty_repo?).and_return(false)
+ end
+
+ it_behaves_like 'show_merge_request_url'
+ end
+
+ context 'pushing to existing branch and merge request is closed' do
+ let!(:merge_request) { create(:merge_request, :closed, source_project: project, source_branch: source_branch) }
+ let(:changes) { existing_branch_changes }
+ it_behaves_like 'new_merge_request_link'
+ end
+
+ context 'pushing to existing branch and merge request is merged' do
+ let!(:merge_request) { create(:merge_request, :merged, source_project: project, source_branch: source_branch) }
+ let(:changes) { existing_branch_changes }
+ it_behaves_like 'new_merge_request_link'
+ end
+
+ context 'pushing new branch and existing branch (with merge request created) at once' do
+ let!(:merge_request) { create(:merge_request, source_project: project, source_branch: "existing_branch") }
+ let(:new_branch_changes) { "#{Gitlab::Git::BLANK_SHA} 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/new_branch" }
+ let(:existing_branch_changes) { "d14d6c0abdd253381df51a723d58691b2ee1ab08 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/existing_branch" }
+ let(:changes) { "#{new_branch_changes}\n#{existing_branch_changes}" }
+ let(:new_merge_request_url) { "http://localhost/#{project.namespace.name}/#{project.path}/merge_requests/new?merge_request%5Bsource_branch%5D=new_branch" }
+
+ it 'returns 2 urls for both creating new and showing merge request' do
+ result = service.execute(changes)
+ expect(result).to match([{
+ branch_name: "new_branch",
+ url: new_merge_request_url,
+ new_merge_request: true
+ }, {
+ branch_name: "existing_branch",
+ url: show_merge_request_url,
+ new_merge_request: false
+ }])
+ end
+ end
+ end
+end
diff --git a/spec/services/merge_requests/merge_request_diff_cache_service_spec.rb b/spec/services/merge_requests/merge_request_diff_cache_service_spec.rb
new file mode 100644
index 00000000000..807f89e80b7
--- /dev/null
+++ b/spec/services/merge_requests/merge_request_diff_cache_service_spec.rb
@@ -0,0 +1,17 @@
+require 'spec_helper'
+
+describe MergeRequests::MergeRequestDiffCacheService do
+ let(:subject) { MergeRequests::MergeRequestDiffCacheService.new }
+
+ describe '#execute' do
+ it 'retrieves the diff files to cache the highlighted result' do
+ merge_request = create(:merge_request)
+ cache_key = [merge_request.merge_request_diff, 'highlighted-diff-files', Gitlab::Diff::FileCollection::MergeRequestDiff.default_options]
+
+ expect(Rails.cache).to receive(:read).with(cache_key).and_return({})
+ expect(Rails.cache).to receive(:write).with(cache_key, anything)
+
+ subject.execute(merge_request)
+ end
+ end
+end
diff --git a/spec/services/merge_requests/merge_service_spec.rb b/spec/services/merge_requests/merge_service_spec.rb
index 2f72cd60071..159f6817e8d 100644
--- a/spec/services/merge_requests/merge_service_spec.rb
+++ b/spec/services/merge_requests/merge_service_spec.rb
@@ -11,7 +11,7 @@ describe MergeRequests::MergeService, services: true do
project.team << [user2, :developer]
end
- describe :execute do
+ describe '#execute' do
context 'valid params' do
let(:service) { MergeRequests::MergeService.new(project, user, commit_message: 'Awesome message') }
@@ -26,13 +26,13 @@ describe MergeRequests::MergeService, services: true do
it { expect(merge_request).to be_valid }
it { expect(merge_request).to be_merged }
- it 'should send email to user2 about merge of new merge_request' do
+ it 'sends email to user2 about merge of new merge_request' do
email = ActionMailer::Base.deliveries.last
expect(email.to.first).to eq(user2.email)
expect(email.subject).to include(merge_request.title)
end
- it 'should create system note about merge_request merge' do
+ it 'creates system note about merge_request merge' do
note = merge_request.notes.last
expect(note.note).to include 'Status changed to merged'
end
@@ -75,6 +75,17 @@ describe MergeRequests::MergeService, services: true do
expect(merge_request.merge_error).to eq("error")
end
+
+ it 'aborts if there is a merge conflict' do
+ allow_any_instance_of(Repository).to receive(:merge).and_return(false)
+ allow(service).to receive(:execute_hooks)
+
+ service.execute(merge_request)
+
+ expect(merge_request.open?).to be_truthy
+ expect(merge_request.merge_commit_sha).to be_nil
+ expect(merge_request.merge_error).to eq("Conflicts detected during merge")
+ end
end
end
end
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 4da8146e3d6..520e906b21f 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
@@ -110,19 +110,15 @@ describe MergeRequests::MergeWhenBuildSucceedsService do
context 'properly handles multiple stages' do
let(:ref) { mr_merge_if_green_enabled.source_branch }
- 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') }
+ let!(:build) { create(:ci_build, :created, pipeline: pipeline, ref: ref, name: 'build', stage: 'build') }
+ let!(:test) { create(:ci_build, :created, pipeline: pipeline, ref: ref, name: 'test', stage: 'test') }
+ let(:pipeline) { create(:ci_empty_pipeline, ref: mr_merge_if_green_enabled.source_branch, project: project) }
before do
# This behavior of MergeRequest: we instantiate a new object
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(pipeline).to receive(:create_next_builds).and_wrap_original do
- test
- end
end
it "doesn't merge if some stages failed" do
diff --git a/spec/services/merge_requests/refresh_service_spec.rb b/spec/services/merge_requests/refresh_service_spec.rb
index 7d5cb876063..fff86480c6d 100644
--- a/spec/services/merge_requests/refresh_service_spec.rb
+++ b/spec/services/merge_requests/refresh_service_spec.rb
@@ -5,7 +5,7 @@ describe MergeRequests::RefreshService, services: true do
let(:user) { create(:user) }
let(:service) { MergeRequests::RefreshService }
- describe :execute do
+ describe '#execute' do
before do
@user = create(:user)
group = create(:group)
@@ -55,9 +55,9 @@ describe MergeRequests::RefreshService, services: true do
reload_mrs
end
- it 'should execute hooks with update action' do
+ it 'executes hooks with update action' do
expect(refresh_service).to have_received(:execute_hooks).
- with(@merge_request, 'update')
+ with(@merge_request, 'update', @oldrev)
end
it { expect(@merge_request.notes).not_to be_empty }
@@ -88,8 +88,7 @@ describe MergeRequests::RefreshService, services: true do
# Merge master -> feature branch
author = { email: 'test@gitlab.com', time: Time.now, name: "Me" }
commit_options = { message: 'Test message', committer: author, author: author }
- master_commit = @project.repository.commit('master')
- @project.repository.merge(@user, master_commit.id, 'feature', commit_options)
+ @project.repository.merge(@user, @merge_request, commit_options)
commit = @project.repository.commit('feature')
service.new(@project, @user).execute(@oldrev, commit.id, 'refs/heads/feature')
reload_mrs
@@ -112,9 +111,9 @@ describe MergeRequests::RefreshService, services: true do
reload_mrs
end
- it 'should execute hooks with update action' do
+ it 'executes hooks with update action' do
expect(refresh_service).to have_received(:execute_hooks).
- with(@fork_merge_request, 'update')
+ with(@fork_merge_request, 'update', @oldrev)
end
it { expect(@merge_request.notes).to be_empty }
@@ -159,7 +158,7 @@ describe MergeRequests::RefreshService, services: true do
it 'refreshes the merge request' do
expect(refresh_service).to receive(:execute_hooks).
- with(@fork_merge_request, 'update')
+ with(@fork_merge_request, 'update', Gitlab::Git::BLANK_SHA)
allow_any_instance_of(Repository).to receive(:merge_base).and_return(@oldrev)
refresh_service.execute(Gitlab::Git::BLANK_SHA, @newrev, 'refs/heads/master')
diff --git a/spec/services/merge_requests/reopen_service_spec.rb b/spec/services/merge_requests/reopen_service_spec.rb
index ac0221998f5..af7424a76a9 100644
--- a/spec/services/merge_requests/reopen_service_spec.rb
+++ b/spec/services/merge_requests/reopen_service_spec.rb
@@ -3,22 +3,23 @@ require 'spec_helper'
describe MergeRequests::ReopenService, services: true do
let(:user) { create(:user) }
let(:user2) { create(:user) }
- let(:merge_request) { create(:merge_request, assignee: user2) }
+ let(:guest) { create(:user) }
+ let(:merge_request) { create(:merge_request, :closed, assignee: user2) }
let(:project) { merge_request.project }
before do
project.team << [user, :master]
project.team << [user2, :developer]
+ project.team << [guest, :guest]
end
- describe :execute do
+ describe '#execute' do
context 'valid params' do
- let(:service) { MergeRequests::ReopenService.new(project, user, {}) }
+ let(:service) { described_class.new(project, user, {}) }
before do
allow(service).to receive(:execute_hooks)
- merge_request.state = :closed
perform_enqueued_jobs do
service.execute(merge_request)
end
@@ -27,21 +28,33 @@ describe MergeRequests::ReopenService, services: true do
it { expect(merge_request).to be_valid }
it { expect(merge_request).to be_reopened }
- it 'should execute hooks with reopen action' do
+ it 'executes hooks with reopen action' do
expect(service).to have_received(:execute_hooks).
with(merge_request, 'reopen')
end
- it 'should send email to user2 about reopen of merge_request' do
+ it 'sends email to user2 about reopen of merge_request' do
email = ActionMailer::Base.deliveries.last
expect(email.to.first).to eq(user2.email)
expect(email.subject).to include(merge_request.title)
end
- it 'should create system note about merge_request reopen' do
+ it 'creates system note about merge_request reopen' do
note = merge_request.notes.last
expect(note.note).to include 'Status changed to reopened'
end
end
+
+ context 'current user is not authorized to reopen merge request' do
+ before do
+ perform_enqueued_jobs do
+ @merge_request = described_class.new(project, guest).execute(merge_request)
+ end
+ end
+
+ it 'does not reopen the merge request' do
+ expect(@merge_request).to be_closed
+ end
+ end
end
end
diff --git a/spec/services/merge_requests/resolved_discussion_notification_service.rb b/spec/services/merge_requests/resolved_discussion_notification_service.rb
new file mode 100644
index 00000000000..7ddd812e513
--- /dev/null
+++ b/spec/services/merge_requests/resolved_discussion_notification_service.rb
@@ -0,0 +1,46 @@
+require 'spec_helper'
+
+describe MergeRequests::ResolvedDiscussionNotificationService, services: true do
+ let(:merge_request) { create(:merge_request) }
+ let(:user) { create(:user) }
+ let(:project) { merge_request.project }
+ subject { described_class.new(project, user) }
+
+ describe "#execute" do
+ context "when not all discussions are resolved" do
+ before do
+ allow(merge_request).to receive(:discussions_resolved?).and_return(false)
+ end
+
+ it "doesn't add a system note" do
+ expect(SystemNoteService).not_to receive(:resolve_all_discussions)
+
+ subject.execute(merge_request)
+ end
+
+ it "doesn't send a notification email" do
+ expect_any_instance_of(NotificationService).not_to receive(:resolve_all_discussions)
+
+ subject.execute(merge_request)
+ end
+ end
+
+ context "when all discussions are resolved" do
+ before do
+ allow(merge_request).to receive(:discussions_resolved?).and_return(true)
+ end
+
+ it "adds a system note" do
+ expect(SystemNoteService).to receive(:resolve_all_discussions).with(merge_request, project, user)
+
+ subject.execute(merge_request)
+ end
+
+ it "sends a notification email" do
+ expect_any_instance_of(NotificationService).to receive(:resolve_all_discussions).with(merge_request, user)
+
+ subject.execute(merge_request)
+ end
+ end
+ end
+end
diff --git a/spec/services/merge_requests/update_service_spec.rb b/spec/services/merge_requests/update_service_spec.rb
index d4ebe28c276..6dfeb581975 100644
--- a/spec/services/merge_requests/update_service_spec.rb
+++ b/spec/services/merge_requests/update_service_spec.rb
@@ -64,12 +64,12 @@ describe MergeRequests::UpdateService, services: true do
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
+ it 'executes hooks with update action' do
expect(service).to have_received(:execute_hooks).
with(@merge_request, 'update')
end
- it 'should send email to user2 about assign of new merge request and email to user3 about merge request unassignment' do
+ it 'sends email to user2 about assign of new merge request and email to user3 about merge request unassignment' do
deliveries = ActionMailer::Base.deliveries
email = deliveries.last
recipients = deliveries.last(2).map(&:to).flatten
@@ -77,14 +77,14 @@ describe MergeRequests::UpdateService, services: true do
expect(email.subject).to include(merge_request.title)
end
- it 'should create system note about merge_request reassign' do
+ it 'creates system note about merge_request reassign' do
note = find_note('Reassigned to')
expect(note).not_to be_nil
expect(note.note).to include "Reassigned to \@#{user2.username}"
end
- it 'should create system note about merge_request label edit' do
+ it 'creates system note about merge_request label edit' do
note = find_note('Added ~')
expect(note).not_to be_nil
@@ -226,6 +226,11 @@ describe MergeRequests::UpdateService, services: true do
end
end
+ context 'updating mentions' do
+ let(:mentionable) { merge_request }
+ include_examples 'updating mentions', MergeRequests::UpdateService
+ end
+
context 'when MergeRequest has tasks' do
before { update_merge_request({ description: "- [ ] Task 1\n- [ ] Task 2" }) }
diff --git a/spec/services/milestones/close_service_spec.rb b/spec/services/milestones/close_service_spec.rb
index 1cd6eb2ab38..5d400299be0 100644
--- a/spec/services/milestones/close_service_spec.rb
+++ b/spec/services/milestones/close_service_spec.rb
@@ -9,7 +9,7 @@ describe Milestones::CloseService, services: true do
project.team << [user, :master]
end
- describe :execute do
+ describe '#execute' do
before do
Milestones::CloseService.new(project, user, {}).execute(milestone)
end
diff --git a/spec/services/milestones/create_service_spec.rb b/spec/services/milestones/create_service_spec.rb
index c793026e300..6d29edb449a 100644
--- a/spec/services/milestones/create_service_spec.rb
+++ b/spec/services/milestones/create_service_spec.rb
@@ -4,7 +4,7 @@ describe Milestones::CreateService, services: true do
let(:project) { create(:empty_project) }
let(:user) { create(:user) }
- describe :execute do
+ describe '#execute' do
context "valid params" do
before do
project.team << [user, :master]
diff --git a/spec/services/notes/create_service_spec.rb b/spec/services/notes/create_service_spec.rb
index 35f576874b8..93885c84dc3 100644
--- a/spec/services/notes/create_service_spec.rb
+++ b/spec/services/notes/create_service_spec.rb
@@ -4,22 +4,36 @@ describe Notes::CreateService, services: true do
let(:project) { create(:empty_project) }
let(:issue) { create(:issue, project: project) }
let(:user) { create(:user) }
+ let(:opts) do
+ { note: 'Awesome comment', noteable_type: 'Issue', noteable_id: issue.id }
+ end
+
+ describe '#execute' do
+ before do
+ project.team << [user, :master]
+ end
- describe :execute do
context "valid params" do
before do
- project.team << [user, :master]
- opts = {
- note: 'Awesome comment',
- noteable_type: 'Issue',
- noteable_id: issue.id
- }
-
@note = Notes::CreateService.new(project, user, opts).execute
end
it { expect(@note).to be_valid }
- it { expect(@note.note).to eq('Awesome comment') }
+ it { expect(@note.note).to eq(opts[:note]) }
+ end
+
+ describe 'note with commands' do
+ describe '/close, /label, /assign & /milestone' do
+ let(:note_text) { %(HELLO\n/close\n/assign @#{user.username}\nWORLD) }
+
+ it 'saves the note and does not alter the note text' do
+ expect_any_instance_of(Issues::UpdateService).to receive(:execute).and_call_original
+
+ note = described_class.new(project, user, opts.merge(note: note_text)).execute
+
+ expect(note.note).to eq "HELLO\nWORLD"
+ end
+ end
end
end
@@ -42,7 +56,7 @@ describe Notes::CreateService, services: true do
it "creates regular note if emoji name is invalid" do
opts = {
- note: ':smile: moretext: ',
+ note: ':smile: moretext:',
noteable_type: 'Issue',
noteable_id: issue.id
}
diff --git a/spec/services/notes/post_process_service_spec.rb b/spec/services/notes/post_process_service_spec.rb
index d4c50f824c1..e33a611929b 100644
--- a/spec/services/notes/post_process_service_spec.rb
+++ b/spec/services/notes/post_process_service_spec.rb
@@ -5,7 +5,7 @@ describe Notes::PostProcessService, services: true do
let(:issue) { create(:issue, project: project) }
let(:user) { create(:user) }
- describe :execute do
+ describe '#execute' do
before do
project.team << [user, :master]
note_opts = {
diff --git a/spec/services/notes/slash_commands_service_spec.rb b/spec/services/notes/slash_commands_service_spec.rb
new file mode 100644
index 00000000000..4f231aab161
--- /dev/null
+++ b/spec/services/notes/slash_commands_service_spec.rb
@@ -0,0 +1,140 @@
+require 'spec_helper'
+
+describe Notes::SlashCommandsService, services: true do
+ shared_context 'note on noteable' do
+ let(:project) { create(:empty_project) }
+ let(:master) { create(:user).tap { |u| project.team << [u, :master] } }
+ let(:assignee) { create(:user) }
+ end
+
+ shared_examples 'note on noteable that does not support slash commands' do
+ include_context 'note on noteable'
+
+ before do
+ note.note = note_text
+ end
+
+ describe 'note with only command' do
+ describe '/close, /label, /assign & /milestone' do
+ let(:note_text) { %(/close\n/assign @#{assignee.username}") }
+
+ it 'saves the note and does not alter the note text' do
+ content, command_params = service.extract_commands(note)
+
+ expect(content).to eq note_text
+ expect(command_params).to be_empty
+ end
+ end
+ end
+
+ describe 'note with command & text' do
+ describe '/close, /label, /assign & /milestone' do
+ let(:note_text) { %(HELLO\n/close\n/assign @#{assignee.username}\nWORLD) }
+
+ it 'saves the note and does not alter the note text' do
+ content, command_params = service.extract_commands(note)
+
+ expect(content).to eq note_text
+ expect(command_params).to be_empty
+ end
+ end
+ end
+ end
+
+ shared_examples 'note on noteable that supports slash commands' do
+ include_context 'note on noteable'
+
+ before do
+ note.note = note_text
+ end
+
+ let!(:milestone) { create(:milestone, project: project) }
+ let!(:labels) { create_pair(:label, project: project) }
+
+ describe 'note with only command' do
+ describe '/close, /label, /assign & /milestone' do
+ let(:note_text) do
+ %(/close\n/label ~#{labels.first.name} ~#{labels.last.name}\n/assign @#{assignee.username}\n/milestone %"#{milestone.name}")
+ end
+
+ it 'closes noteable, sets labels, assigns, and sets milestone to noteable, and leave no note' do
+ content, command_params = service.extract_commands(note)
+ service.execute(command_params, note)
+
+ expect(content).to eq ''
+ expect(note.noteable).to be_closed
+ expect(note.noteable.labels).to match_array(labels)
+ expect(note.noteable.assignee).to eq(assignee)
+ expect(note.noteable.milestone).to eq(milestone)
+ end
+ end
+
+ describe '/reopen' do
+ before do
+ note.noteable.close!
+ expect(note.noteable).to be_closed
+ end
+ let(:note_text) { '/reopen' }
+
+ it 'opens the noteable, and leave no note' do
+ content, command_params = service.extract_commands(note)
+ service.execute(command_params, note)
+
+ expect(content).to eq ''
+ expect(note.noteable).to be_open
+ end
+ end
+ end
+
+ describe 'note with command & text' do
+ describe '/close, /label, /assign & /milestone' do
+ let(:note_text) do
+ %(HELLO\n/close\n/label ~#{labels.first.name} ~#{labels.last.name}\n/assign @#{assignee.username}\n/milestone %"#{milestone.name}"\nWORLD)
+ end
+
+ it 'closes noteable, sets labels, assigns, and sets milestone to noteable' do
+ content, command_params = service.extract_commands(note)
+ service.execute(command_params, note)
+
+ expect(content).to eq "HELLO\nWORLD"
+ expect(note.noteable).to be_closed
+ expect(note.noteable.labels).to match_array(labels)
+ expect(note.noteable.assignee).to eq(assignee)
+ expect(note.noteable.milestone).to eq(milestone)
+ end
+ end
+
+ describe '/reopen' do
+ before do
+ note.noteable.close
+ expect(note.noteable).to be_closed
+ end
+ let(:note_text) { "HELLO\n/reopen\nWORLD" }
+
+ it 'opens the noteable' do
+ content, command_params = service.extract_commands(note)
+ service.execute(command_params, note)
+
+ expect(content).to eq "HELLO\nWORLD"
+ expect(note.noteable).to be_open
+ end
+ end
+ end
+ end
+
+ describe '#execute' do
+ let(:service) { described_class.new(project, master) }
+
+ it_behaves_like 'note on noteable that supports slash commands' do
+ let(:note) { build(:note_on_issue, project: project) }
+ end
+
+ it_behaves_like 'note on noteable that supports slash commands' do
+ let(:note) { build(:note_on_merge_request, project: project) }
+ end
+
+ it_behaves_like 'note on noteable that does not support slash commands' do
+ let(:note) { build(:note_on_commit, project: project) }
+ end
+ end
+end
diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb
index 54719cbb8d8..f81a58899fd 100644
--- a/spec/services/notification_service_spec.rb
+++ b/spec/services/notification_service_spec.rb
@@ -9,13 +9,35 @@ describe NotificationService, services: true do
end
end
+ shared_examples 'notifications for new mentions' do
+ def send_notifications(*new_mentions)
+ reset_delivered_emails!
+ notification.send(notification_method, mentionable, new_mentions, @u_disabled)
+ end
+
+ it 'sends no emails when no new mentions are present' do
+ send_notifications
+ expect(ActionMailer::Base.deliveries).to be_empty
+ end
+
+ it 'emails new mentions with a watch level higher than participant' do
+ send_notifications(@u_watcher, @u_participant_mentioned, @u_custom_global)
+ should_only_email(@u_watcher, @u_participant_mentioned, @u_custom_global)
+ end
+
+ it 'does not email new mentions with a watch level equal to or less than participant' do
+ send_notifications(@u_participating, @u_mentioned)
+ expect(ActionMailer::Base.deliveries).to be_empty
+ end
+ end
+
describe 'Keys' do
describe '#new_key' do
let!(:key) { create(:personal_key) }
it { expect(notification.new_key(key)).to be_truthy }
- it 'should sent email to key owner' do
+ it 'sends email to key owner' do
expect{ notification.new_key(key) }.to change{ ActionMailer::Base.deliveries.size }.by(1)
end
end
@@ -27,7 +49,7 @@ describe NotificationService, services: true do
it { expect(notification.new_email(email)).to be_truthy }
- it 'should send email to email owner' do
+ it 'sends email to email owner' do
expect{ notification.new_email(email) }.to change{ ActionMailer::Base.deliveries.size }.by(1)
end
end
@@ -50,7 +72,7 @@ describe NotificationService, services: true do
update_custom_notification(:new_note, @u_custom_global)
end
- describe :new_note do
+ describe '#new_note' do
it do
add_users_with_subscription(note.project, issue)
@@ -293,6 +315,30 @@ describe NotificationService, services: true do
end
end
end
+
+ context "merge request diff note" do
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+ let(:merge_request) { create(:merge_request, source_project: project, assignee: user) }
+ let(:note) { create(:diff_note_on_merge_request, project: project, noteable: merge_request) }
+
+ before do
+ build_team(note.project)
+ project.team << [merge_request.author, :master]
+ project.team << [merge_request.assignee, :master]
+ end
+
+ describe '#new_note' do
+ it "records sent notifications" do
+ # Ensure create SentNotification by noteable = merge_request 6 times, not noteable = note
+ expect(SentNotification).to receive(:record_note).with(note, any_args).exactly(4).times.and_call_original
+
+ notification.new_note(note)
+
+ expect(SentNotification.last.position).to eq(note.position)
+ end
+ end
+ end
end
describe 'Issues' do
@@ -375,6 +421,13 @@ describe NotificationService, services: true do
end
end
+ describe '#new_mentions_in_issue' do
+ let(:notification_method) { :new_mentions_in_issue }
+ let(:mentionable) { issue }
+
+ include_examples 'notifications for new mentions'
+ end
+
describe '#reassigned_issue' do
before do
update_custom_notification(:reassign_issue, @u_guest_custom, project)
@@ -569,7 +622,7 @@ describe NotificationService, services: true do
update_custom_notification(:close_issue, @u_custom_global)
end
- it 'should sent email to issue assignee and issue author' do
+ it 'sends email to issue assignee and issue author' do
notification.close_issue(issue, @u_disabled)
should_email(issue.assignee)
@@ -622,7 +675,7 @@ describe NotificationService, services: true do
update_custom_notification(:reopen_issue, @u_custom_global)
end
- it 'should send email to issue assignee and issue author' do
+ it 'sends email to issue assignee and issue author' do
notification.reopen_issue(issue, @u_disabled)
should_email(issue.assignee)
@@ -676,6 +729,8 @@ describe NotificationService, services: true do
before do
build_team(merge_request.target_project)
add_users_with_subscription(merge_request.target_project, merge_request)
+ update_custom_notification(:new_merge_request, @u_guest_custom, project)
+ update_custom_notification(:new_merge_request, @u_custom_global)
ActionMailer::Base.deliveries.clear
end
@@ -739,6 +794,13 @@ describe NotificationService, services: true do
end
end
+ describe '#new_mentions_in_merge_request' do
+ let(:notification_method) { :new_mentions_in_merge_request }
+ let(:mentionable) { merge_request }
+
+ include_examples 'notifications for new mentions'
+ end
+
describe '#reassigned_merge_request' do
before do
update_custom_notification(:reassign_merge_request, @u_guest_custom, project)
@@ -980,6 +1042,52 @@ describe NotificationService, services: true do
end
end
end
+
+ describe "#resolve_all_discussions" do
+ it do
+ notification.resolve_all_discussions(merge_request, @u_disabled)
+
+ should_email(merge_request.assignee)
+ should_email(@u_watcher)
+ 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.resolve_all_discussions(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.resolve_all_discussions(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.resolve_all_discussions(merge_request, @u_disabled)
+ end
+
+ it { should_email(@u_lazy_participant) }
+ end
+ end
+ end
end
describe 'Projects' do
@@ -1005,6 +1113,46 @@ describe NotificationService, services: true do
end
end
+ describe 'GroupMember' do
+ describe '#decline_group_invite' do
+ let(:creator) { create(:user) }
+ let(:group) { create(:group) }
+ let(:member) { create(:user) }
+
+ before(:each) do
+ group.add_owner(creator)
+ group.add_developer(member, creator)
+ end
+
+ it do
+ group_member = group.members.first
+
+ expect do
+ notification.decline_group_invite(group_member)
+ end.to change { ActionMailer::Base.deliveries.size }.by(1)
+ end
+ end
+ end
+
+ describe 'ProjectMember' do
+ describe '#decline_group_invite' do
+ let(:project) { create(:project) }
+ let(:member) { create(:user) }
+
+ before(:each) do
+ project.team << [member, :developer, project.owner]
+ end
+
+ it do
+ project_member = project.members.first
+
+ expect do
+ notification.decline_project_invite(project_member)
+ end.to change { ActionMailer::Base.deliveries.size }.by(1)
+ end
+ end
+ end
+
def build_team(project)
@u_watcher = create_global_setting_for(create(:user), :watch)
@u_participating = create_global_setting_for(create(:user), :participating)
diff --git a/spec/services/projects/autocomplete_service_spec.rb b/spec/services/projects/autocomplete_service_spec.rb
index 0971fec2e9f..7916c2d957c 100644
--- a/spec/services/projects/autocomplete_service_spec.rb
+++ b/spec/services/projects/autocomplete_service_spec.rb
@@ -13,7 +13,7 @@ describe Projects::AutocompleteService, services: true do
let!(:security_issue_1) { create(:issue, :confidential, project: project, title: 'Security issue 1', author: author) }
let!(:security_issue_2) { create(:issue, :confidential, title: 'Security issue 2', project: project, assignee: assignee) }
- it 'should not list project confidential issues for guests' do
+ it 'does not list project confidential issues for guests' do
autocomplete = described_class.new(project, nil)
issues = autocomplete.issues.map(&:iid)
@@ -23,7 +23,7 @@ describe Projects::AutocompleteService, services: true do
expect(issues.count).to eq 1
end
- it 'should not list project confidential issues for non project members' do
+ it 'does not list project confidential issues for non project members' do
autocomplete = described_class.new(project, non_member)
issues = autocomplete.issues.map(&:iid)
@@ -33,7 +33,7 @@ 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
+ it 'does not list project confidential issues for project members with guest role' do
project.team << [member, :guest]
autocomplete = described_class.new(project, non_member)
@@ -45,7 +45,7 @@ describe Projects::AutocompleteService, services: true do
expect(issues.count).to eq 1
end
- it 'should list project confidential issues for author' do
+ it 'lists project confidential issues for author' do
autocomplete = described_class.new(project, author)
issues = autocomplete.issues.map(&:iid)
@@ -55,7 +55,7 @@ describe Projects::AutocompleteService, services: true do
expect(issues.count).to eq 2
end
- it 'should list project confidential issues for assignee' do
+ it 'lists project confidential issues for assignee' do
autocomplete = described_class.new(project, assignee)
issues = autocomplete.issues.map(&:iid)
@@ -65,7 +65,7 @@ describe Projects::AutocompleteService, services: true do
expect(issues.count).to eq 2
end
- it 'should list project confidential issues for project members' do
+ it 'lists project confidential issues for project members' do
project.team << [member, :developer]
autocomplete = described_class.new(project, member)
@@ -77,7 +77,7 @@ describe Projects::AutocompleteService, services: true do
expect(issues.count).to eq 3
end
- it 'should list all project issues for admin' do
+ it 'lists all project issues for admin' do
autocomplete = described_class.new(project, admin)
issues = autocomplete.issues.map(&:iid)
diff --git a/spec/services/projects/create_service_spec.rb b/spec/services/projects/create_service_spec.rb
index fd114359467..bbced59ff02 100644
--- a/spec/services/projects/create_service_spec.rb
+++ b/spec/services/projects/create_service_spec.rb
@@ -109,7 +109,7 @@ describe Projects::CreateService, services: true do
)
end
- it 'should not allow a restricted visibility level for non-admins' do
+ it 'does not allow a restricted visibility level for non-admins' do
project = create_project(@user, @opts)
expect(project).to respond_to(:errors)
expect(project.errors.messages).to have_key(:visibility_level)
@@ -118,7 +118,7 @@ describe Projects::CreateService, services: true do
)
end
- it 'should allow a restricted visibility level for admins' do
+ it 'allows a restricted visibility level for admins' do
admin = create(:admin)
project = create_project(admin, @opts)
@@ -128,7 +128,7 @@ describe Projects::CreateService, services: true do
end
context 'repository creation' do
- it 'should synchronously create the repository' do
+ it 'synchronously creates the repository' do
expect_any_instance_of(Project).to receive(:create_repository)
project = create_project(@user, @opts)
diff --git a/spec/services/projects/download_service_spec.rb b/spec/services/projects/download_service_spec.rb
index f252e2c5902..122a7cea2a1 100644
--- a/spec/services/projects/download_service_spec.rb
+++ b/spec/services/projects/download_service_spec.rb
@@ -35,8 +35,6 @@ describe Projects::DownloadService, services: true do
it { expect(@link_to_file).to have_key(:alt) }
it { expect(@link_to_file).to have_key(:url) }
- it { expect(@link_to_file).to have_key(:is_image) }
- it { expect(@link_to_file[:is_image]).to be true }
it { expect(@link_to_file[:url]).to match('rails_sample.jpg') }
it { expect(@link_to_file[:alt]).to eq('rails_sample') }
end
@@ -49,8 +47,6 @@ describe Projects::DownloadService, services: true do
it { expect(@link_to_file).to have_key(:alt) }
it { expect(@link_to_file).to have_key(:url) }
- it { expect(@link_to_file).to have_key(:is_image) }
- it { expect(@link_to_file[:is_image]).to be false }
it { expect(@link_to_file[:url]).to match('doc_sample.txt') }
it { expect(@link_to_file[:alt]).to eq('doc_sample.txt') }
end
diff --git a/spec/services/projects/enable_deploy_key_service_spec.rb b/spec/services/projects/enable_deploy_key_service_spec.rb
new file mode 100644
index 00000000000..a37510cf159
--- /dev/null
+++ b/spec/services/projects/enable_deploy_key_service_spec.rb
@@ -0,0 +1,27 @@
+require 'spec_helper'
+
+describe Projects::EnableDeployKeyService, services: true do
+ let(:deploy_key) { create(:deploy_key, public: true) }
+ let(:project) { create(:empty_project) }
+ let(:user) { project.creator}
+ let!(:params) { { key_id: deploy_key.id } }
+
+ it 'enables the key' do
+ expect do
+ service.execute
+ end.to change { project.deploy_keys.count }.from(0).to(1)
+ end
+
+ context 'trying to add an unaccessable key' do
+ let(:another_key) { create(:another_key) }
+ let!(:params) { { key_id: another_key.id } }
+
+ it 'returns nil if the key cannot be added' do
+ expect(service.execute).to be nil
+ end
+ end
+
+ def service
+ Projects::EnableDeployKeyService.new(project, user, params)
+ end
+end
diff --git a/spec/services/projects/fork_service_spec.rb b/spec/services/projects/fork_service_spec.rb
index 31bb7120d84..ef2036c78b1 100644
--- a/spec/services/projects/fork_service_spec.rb
+++ b/spec/services/projects/fork_service_spec.rb
@@ -26,7 +26,7 @@ describe Projects::ForkService, services: true do
end
context 'project already exists' do
- it "should fail due to validation, not transaction failure" do
+ it "fails due to validation, not transaction failure" do
@existing_project = create(:project, creator_id: @to_user.id, name: @from_project.name, namespace: @to_namespace)
@to_project = fork_project(@from_project, @to_user)
expect(@existing_project.persisted?).to be_truthy
@@ -36,7 +36,7 @@ describe Projects::ForkService, services: true do
end
context 'GitLab CI is enabled' do
- it "fork and enable CI for fork" do
+ it "forks and enables CI for fork" do
@from_project.enable_ci
@to_project = fork_project(@from_project, @to_user)
expect(@to_project.builds_enabled?).to be_truthy
@@ -97,14 +97,14 @@ describe Projects::ForkService, services: true do
end
context 'fork project for group when user not owner' do
- it 'group developer should fail to fork project into the group' do
+ it 'group developer fails to fork project into the group' do
to_project = fork_project(@project, @developer, @opts)
expect(to_project.errors[:namespace]).to eq(['is not valid'])
end
end
context 'project already exists in group' do
- it 'should fail due to validation, not transaction failure' do
+ it 'fails due to validation, not transaction failure' do
existing_project = create(:project, name: @project.name,
namespace: @group)
to_project = fork_project(@project, @group_owner, @opts)
diff --git a/spec/services/projects/housekeeping_service_spec.rb b/spec/services/projects/housekeeping_service_spec.rb
index bd4dc6a0f79..ad0d58672b3 100644
--- a/spec/services/projects/housekeeping_service_spec.rb
+++ b/spec/services/projects/housekeeping_service_spec.rb
@@ -12,18 +12,28 @@ describe Projects::HousekeepingService do
it 'enqueues a sidekiq job' do
expect(subject).to receive(:try_obtain_lease).and_return(true)
- expect(GitlabShellOneShotWorker).to receive(:perform_async).with(:gc, project.repository_storage_path, project.path_with_namespace)
+ expect(GitGarbageCollectWorker).to receive(:perform_async).with(project.id)
subject.execute
- expect(project.pushes_since_gc).to eq(0)
+ expect(project.reload.pushes_since_gc).to eq(0)
end
- it 'does not enqueue a job when no lease can be obtained' do
- expect(subject).to receive(:try_obtain_lease).and_return(false)
- expect(GitlabShellOneShotWorker).not_to receive(:perform_async)
+ context 'when no lease can be obtained' do
+ before(:each) do
+ expect(subject).to receive(:try_obtain_lease).and_return(false)
+ end
- expect { subject.execute }.to raise_error(Projects::HousekeepingService::LeaseTaken)
- expect(project.pushes_since_gc).to eq(0)
+ it 'does not enqueue a job' do
+ expect(GitGarbageCollectWorker).not_to receive(:perform_async)
+
+ expect { subject.execute }.to raise_error(Projects::HousekeepingService::LeaseTaken)
+ end
+
+ it 'does not reset pushes_since_gc' do
+ expect do
+ expect { subject.execute }.to raise_error(Projects::HousekeepingService::LeaseTaken)
+ end.not_to change { project.pushes_since_gc }.from(3)
+ end
end
end
@@ -39,10 +49,24 @@ describe Projects::HousekeepingService do
end
describe 'increment!' do
+ let(:lease_key) { "project_housekeeping:increment!:#{project.id}" }
+
it 'increments the pushes_since_gc counter' do
- expect(project.pushes_since_gc).to eq(0)
- subject.increment!
- expect(project.pushes_since_gc).to eq(1)
+ lease = double(:lease, try_obtain: true)
+ expect(Gitlab::ExclusiveLease).to receive(:new).with(lease_key, anything).and_return(lease)
+
+ expect do
+ subject.increment!
+ end.to change { project.pushes_since_gc }.from(0).to(1)
+ end
+
+ it 'does not increment when no lease can be obtained' do
+ lease = double(:lease, try_obtain: false)
+ expect(Gitlab::ExclusiveLease).to receive(:new).with(lease_key, anything).and_return(lease)
+
+ expect do
+ subject.increment!
+ end.not_to change { project.pushes_since_gc }
end
end
end
diff --git a/spec/services/projects/update_service_spec.rb b/spec/services/projects/update_service_spec.rb
index e8b9e6b9238..e139be19140 100644
--- a/spec/services/projects/update_service_spec.rb
+++ b/spec/services/projects/update_service_spec.rb
@@ -9,7 +9,7 @@ describe Projects::UpdateService, services: true do
@opts = {}
end
- context 'should be private when updated to private' do
+ context 'is private when updated to private' do
before do
@created_private = @project.private?
@@ -21,7 +21,7 @@ describe Projects::UpdateService, services: true do
it { expect(@project.private?).to be_truthy }
end
- context 'should be internal when updated to internal' do
+ context 'is internal when updated to internal' do
before do
@created_private = @project.private?
@@ -33,7 +33,7 @@ describe Projects::UpdateService, services: true do
it { expect(@project.internal?).to be_truthy }
end
- context 'should be public when updated to public' do
+ context 'is public when updated to public' do
before do
@created_private = @project.private?
@@ -50,7 +50,7 @@ describe Projects::UpdateService, services: true do
stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC])
end
- context 'should be private when updated to private' do
+ context 'is private when updated to private' do
before do
@created_private = @project.private?
@@ -62,7 +62,7 @@ describe Projects::UpdateService, services: true do
it { expect(@project.private?).to be_truthy }
end
- context 'should be internal when updated to internal' do
+ context 'is internal when updated to internal' do
before do
@created_private = @project.private?
@@ -74,7 +74,7 @@ describe Projects::UpdateService, services: true do
it { expect(@project.internal?).to be_truthy }
end
- context 'should be private when updated to public' do
+ context 'is private when updated to public' do
before do
@created_private = @project.private?
@@ -86,7 +86,7 @@ describe Projects::UpdateService, services: true do
it { expect(@project.private?).to be_truthy }
end
- context 'should be public when updated to public by admin' do
+ context 'is public when updated to public by admin' do
before do
@created_private = @project.private?
@@ -114,7 +114,7 @@ describe Projects::UpdateService, services: true do
@fork_created_internal = forked_project.internal?
end
- context 'should update forks visibility level when parent set to more restrictive' do
+ context 'updates forks visibility level when parent set to more restrictive' do
before do
opts.merge!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
update_project(project, user, opts).inspect
@@ -126,7 +126,7 @@ describe Projects::UpdateService, services: true do
it { expect(project.forks.first.private?).to be_truthy }
end
- context 'should not update forks visibility level when parent set to less restrictive' do
+ context 'does not update forks visibility level when parent set to less restrictive' do
before do
opts.merge!(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
update_project(project, user, opts).inspect
diff --git a/spec/services/projects/upload_service_spec.rb b/spec/services/projects/upload_service_spec.rb
index 9268a9fb1a2..c42eeba4b9c 100644
--- a/spec/services/projects/upload_service_spec.rb
+++ b/spec/services/projects/upload_service_spec.rb
@@ -15,9 +15,7 @@ describe Projects::UploadService, services: true do
it { expect(@link_to_file).to have_key(:alt) }
it { expect(@link_to_file).to have_key(:url) }
- it { expect(@link_to_file).to have_key(:is_image) }
it { expect(@link_to_file).to have_value('banana_sample') }
- it { expect(@link_to_file[:is_image]).to equal(true) }
it { expect(@link_to_file[:url]).to match('banana_sample.gif') }
end
@@ -31,8 +29,6 @@ describe Projects::UploadService, services: true do
it { expect(@link_to_file).to have_key(:alt) }
it { expect(@link_to_file).to have_key(:url) }
it { expect(@link_to_file).to have_value('dk') }
- it { expect(@link_to_file).to have_key(:is_image) }
- it { expect(@link_to_file[:is_image]).to equal(true) }
it { expect(@link_to_file[:url]).to match('dk.png') }
end
@@ -44,9 +40,7 @@ describe Projects::UploadService, services: true do
it { expect(@link_to_file).to have_key(:alt) }
it { expect(@link_to_file).to have_key(:url) }
- it { expect(@link_to_file).to have_key(:is_image) }
it { expect(@link_to_file).to have_value('rails_sample') }
- it { expect(@link_to_file[:is_image]).to equal(true) }
it { expect(@link_to_file[:url]).to match('rails_sample.jpg') }
end
@@ -58,9 +52,7 @@ describe Projects::UploadService, services: true do
it { expect(@link_to_file).to have_key(:alt) }
it { expect(@link_to_file).to have_key(:url) }
- it { expect(@link_to_file).to have_key(:is_image) }
it { expect(@link_to_file).to have_value('doc_sample.txt') }
- it { expect(@link_to_file[:is_image]).to equal(false) }
it { expect(@link_to_file[:url]).to match('doc_sample.txt') }
end
diff --git a/spec/services/repair_ldap_blocked_user_service_spec.rb b/spec/services/repair_ldap_blocked_user_service_spec.rb
index ce7d1455975..87192457298 100644
--- a/spec/services/repair_ldap_blocked_user_service_spec.rb
+++ b/spec/services/repair_ldap_blocked_user_service_spec.rb
@@ -6,14 +6,14 @@ describe RepairLdapBlockedUserService, services: true do
subject(:service) { RepairLdapBlockedUserService.new(user) }
describe '#execute' do
- it 'change to normal block after destroying last ldap identity' do
+ it 'changes to normal block after destroying last ldap identity' do
identity.destroy
service.execute
expect(user.reload).not_to be_ldap_blocked
end
- it 'change to normal block after changing last ldap identity to another provider' do
+ it 'changes to normal block after changing last ldap identity to another provider' do
identity.update_attribute(:provider, 'twitter')
service.execute
diff --git a/spec/services/repository_archive_clean_up_service_spec.rb b/spec/services/repository_archive_clean_up_service_spec.rb
new file mode 100644
index 00000000000..842585f9e54
--- /dev/null
+++ b/spec/services/repository_archive_clean_up_service_spec.rb
@@ -0,0 +1,81 @@
+require 'spec_helper'
+
+describe RepositoryArchiveCleanUpService, services: true do
+ describe '#execute' do
+ subject(:service) { described_class.new }
+
+ context 'when the downloads directory does not exist' do
+ it 'does not remove any archives' do
+ path = '/invalid/path/'
+ stub_repository_downloads_path(path)
+
+ expect(File).to receive(:directory?).with(path).and_return(false)
+ expect(service).not_to receive(:clean_up_old_archives)
+ expect(service).not_to receive(:clean_up_empty_directories)
+
+ service.execute
+ end
+ end
+
+ context 'when the downloads directory exists' do
+ shared_examples 'invalid archive files' do |dirname, extensions, mtime|
+ it 'does not remove files and directoy' do
+ in_directory_with_files(dirname, extensions, mtime) do |dir, files|
+ service.execute
+
+ files.each { |file| expect(File.exist?(file)).to eq true }
+ expect(File.directory?(dir)).to eq true
+ end
+ end
+ end
+
+ it 'removes files older than 2 hours that matches valid archive extensions' do
+ in_directory_with_files('sample.git', %w[tar tar.bz2 tar.gz zip], 2.hours) do |dir, files|
+ service.execute
+
+ files.each { |file| expect(File.exist?(file)).to eq false }
+ expect(File.directory?(dir)).to eq false
+ end
+ end
+
+ context 'with files older than 2 hours that does not matches valid archive extensions' do
+ it_behaves_like 'invalid archive files', 'sample.git', %w[conf rb], 2.hours
+ end
+
+ context 'with files older than 2 hours inside invalid directories' do
+ it_behaves_like 'invalid archive files', 'john_doe/sample.git', %w[conf rb tar tar.gz], 2.hours
+ end
+
+ context 'with files newer than 2 hours that matches valid archive extensions' do
+ it_behaves_like 'invalid archive files', 'sample.git', %w[tar tar.bz2 tar.gz zip], 1.hour
+ end
+
+ context 'with files newer than 2 hours that does not matches valid archive extensions' do
+ it_behaves_like 'invalid archive files', 'sample.git', %w[conf rb], 1.hour
+ end
+
+ context 'with files newer than 2 hours inside invalid directories' do
+ it_behaves_like 'invalid archive files', 'sample.git', %w[conf rb tar tar.gz], 1.hour
+ end
+ end
+
+ def in_directory_with_files(dirname, extensions, mtime)
+ Dir.mktmpdir do |tmpdir|
+ stub_repository_downloads_path(tmpdir)
+ dir = File.join(tmpdir, dirname)
+ files = create_temporary_files(dir, extensions, mtime)
+
+ yield(dir, files)
+ end
+ end
+
+ def stub_repository_downloads_path(path)
+ allow(Gitlab.config.gitlab).to receive(:repository_downloads_path).and_return(path)
+ end
+
+ def create_temporary_files(dir, extensions, mtime)
+ FileUtils.mkdir_p(dir)
+ FileUtils.touch(extensions.map { |ext| File.join(dir, "sample.#{ext}") }, mtime: Time.now - mtime)
+ end
+ end
+end
diff --git a/spec/services/search_service_spec.rb b/spec/services/search_service_spec.rb
index 7b3a9a75d7c..bd89c4a7c11 100644
--- a/spec/services/search_service_spec.rb
+++ b/spec/services/search_service_spec.rb
@@ -16,7 +16,7 @@ describe 'Search::GlobalService', services: true do
describe '#execute' do
context 'unauthenticated' do
- it 'should return public projects only' do
+ it 'returns public projects only' do
context = Search::GlobalService.new(nil, search: "searchable")
results = context.execute
expect(results.objects('projects')).to match_array [public_project]
@@ -24,19 +24,19 @@ describe 'Search::GlobalService', services: true do
end
context 'authenticated' do
- it 'should return public, internal and private projects' do
+ it 'returns public, internal and private projects' do
context = Search::GlobalService.new(user, search: "searchable")
results = context.execute
expect(results.objects('projects')).to match_array [public_project, found_project, internal_project]
end
- it 'should return only public & internal projects' do
+ it 'returns only public & internal projects' do
context = Search::GlobalService.new(internal_user, search: "searchable")
results = context.execute
expect(results.objects('projects')).to match_array [internal_project, public_project]
end
- it 'namespace name should be searchable' do
+ it 'namespace name is searchable' do
context = Search::GlobalService.new(user, search: found_project.namespace.path)
results = context.execute
expect(results.objects('projects')).to match_array [found_project]
diff --git a/spec/services/slash_commands/interpret_service_spec.rb b/spec/services/slash_commands/interpret_service_spec.rb
new file mode 100644
index 00000000000..a616275e883
--- /dev/null
+++ b/spec/services/slash_commands/interpret_service_spec.rb
@@ -0,0 +1,384 @@
+require 'spec_helper'
+
+describe SlashCommands::InterpretService, services: true do
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+ let(:issue) { create(:issue, project: project) }
+ let(:milestone) { create(:milestone, project: project, title: '9.10') }
+ let(:inprogress) { create(:label, project: project, title: 'In Progress') }
+ let(:bug) { create(:label, project: project, title: 'Bug') }
+
+ before do
+ project.team << [user, :developer]
+ end
+
+ describe '#execute' do
+ let(:service) { described_class.new(project, user) }
+ let(:merge_request) { create(:merge_request, source_project: project) }
+
+ shared_examples 'reopen command' do
+ it 'returns state_event: "reopen" if content contains /reopen' do
+ issuable.close!
+ _, updates = service.execute(content, issuable)
+
+ expect(updates).to eq(state_event: 'reopen')
+ end
+ end
+
+ shared_examples 'close command' do
+ it 'returns state_event: "close" if content contains /close' do
+ _, updates = service.execute(content, issuable)
+
+ expect(updates).to eq(state_event: 'close')
+ end
+ end
+
+ shared_examples 'title command' do
+ it 'populates title: "A brand new title" if content contains /title A brand new title' do
+ _, updates = service.execute(content, issuable)
+
+ expect(updates).to eq(title: 'A brand new title')
+ end
+ end
+
+ shared_examples 'assign command' do
+ it 'fetches assignee and populates assignee_id if content contains /assign' do
+ _, updates = service.execute(content, issuable)
+
+ expect(updates).to eq(assignee_id: user.id)
+ end
+ end
+
+ shared_examples 'unassign command' do
+ it 'populates assignee_id: nil if content contains /unassign' do
+ issuable.update(assignee_id: user.id)
+ _, updates = service.execute(content, issuable)
+
+ expect(updates).to eq(assignee_id: nil)
+ end
+ end
+
+ shared_examples 'milestone command' do
+ it 'fetches milestone and populates milestone_id if content contains /milestone' do
+ milestone # populate the milestone
+ _, updates = service.execute(content, issuable)
+
+ expect(updates).to eq(milestone_id: milestone.id)
+ end
+ end
+
+ shared_examples 'remove_milestone command' do
+ it 'populates milestone_id: nil if content contains /remove_milestone' do
+ issuable.update(milestone_id: milestone.id)
+ _, updates = service.execute(content, issuable)
+
+ expect(updates).to eq(milestone_id: nil)
+ end
+ end
+
+ shared_examples 'label command' do
+ it 'fetches label ids and populates add_label_ids if content contains /label' do
+ bug # populate the label
+ inprogress # populate the label
+ _, updates = service.execute(content, issuable)
+
+ expect(updates).to eq(add_label_ids: [bug.id, inprogress.id])
+ end
+ end
+
+ shared_examples 'unlabel command' do
+ it 'fetches label ids and populates remove_label_ids if content contains /unlabel' do
+ issuable.update(label_ids: [inprogress.id]) # populate the label
+ _, updates = service.execute(content, issuable)
+
+ expect(updates).to eq(remove_label_ids: [inprogress.id])
+ end
+ end
+
+ shared_examples 'unlabel command with no argument' do
+ it 'populates label_ids: [] if content contains /unlabel with no arguments' do
+ issuable.update(label_ids: [inprogress.id]) # populate the label
+ _, updates = service.execute(content, issuable)
+
+ expect(updates).to eq(label_ids: [])
+ end
+ end
+
+ shared_examples 'relabel command' do
+ it 'populates label_ids: [] if content contains /relabel' do
+ issuable.update(label_ids: [bug.id]) # populate the label
+ inprogress # populate the label
+ _, updates = service.execute(content, issuable)
+
+ expect(updates).to eq(label_ids: [inprogress.id])
+ end
+ end
+
+ shared_examples 'todo command' do
+ it 'populates todo_event: "add" if content contains /todo' do
+ _, updates = service.execute(content, issuable)
+
+ expect(updates).to eq(todo_event: 'add')
+ end
+ end
+
+ shared_examples 'done command' do
+ it 'populates todo_event: "done" if content contains /done' do
+ TodoService.new.mark_todo(issuable, user)
+ _, updates = service.execute(content, issuable)
+
+ expect(updates).to eq(todo_event: 'done')
+ end
+ end
+
+ shared_examples 'subscribe command' do
+ it 'populates subscription_event: "subscribe" if content contains /subscribe' do
+ _, updates = service.execute(content, issuable)
+
+ expect(updates).to eq(subscription_event: 'subscribe')
+ end
+ end
+
+ shared_examples 'unsubscribe command' do
+ it 'populates subscription_event: "unsubscribe" if content contains /unsubscribe' do
+ issuable.subscribe(user)
+ _, updates = service.execute(content, issuable)
+
+ expect(updates).to eq(subscription_event: 'unsubscribe')
+ end
+ end
+
+ shared_examples 'due command' do
+ it 'populates due_date: Date.new(2016, 8, 28) if content contains /due 2016-08-28' do
+ _, updates = service.execute(content, issuable)
+
+ expect(updates).to eq(due_date: defined?(expected_date) ? expected_date : Date.new(2016, 8, 28))
+ end
+ end
+
+ shared_examples 'remove_due_date command' do
+ it 'populates due_date: nil if content contains /remove_due_date' do
+ issuable.update(due_date: Date.today)
+ _, updates = service.execute(content, issuable)
+
+ expect(updates).to eq(due_date: nil)
+ end
+ end
+
+ shared_examples 'empty command' do
+ it 'populates {} if content contains an unsupported command' do
+ _, updates = service.execute(content, issuable)
+
+ expect(updates).to be_empty
+ end
+ end
+
+ it_behaves_like 'reopen command' do
+ let(:content) { '/reopen' }
+ let(:issuable) { issue }
+ end
+
+ it_behaves_like 'reopen command' do
+ let(:content) { '/reopen' }
+ let(:issuable) { merge_request }
+ end
+
+ it_behaves_like 'close command' do
+ let(:content) { '/close' }
+ let(:issuable) { issue }
+ end
+
+ it_behaves_like 'close command' do
+ let(:content) { '/close' }
+ let(:issuable) { merge_request }
+ end
+
+ it_behaves_like 'title command' do
+ let(:content) { '/title A brand new title' }
+ let(:issuable) { issue }
+ end
+
+ it_behaves_like 'title command' do
+ let(:content) { '/title A brand new title' }
+ let(:issuable) { merge_request }
+ end
+
+ it_behaves_like 'empty command' do
+ let(:content) { '/title' }
+ let(:issuable) { issue }
+ end
+
+ it_behaves_like 'assign command' do
+ let(:content) { "/assign @#{user.username}" }
+ let(:issuable) { issue }
+ end
+
+ it_behaves_like 'assign command' do
+ let(:content) { "/assign @#{user.username}" }
+ let(:issuable) { merge_request }
+ end
+
+ it_behaves_like 'empty command' do
+ let(:content) { '/assign @abcd1234' }
+ let(:issuable) { issue }
+ end
+
+ it_behaves_like 'empty command' do
+ let(:content) { '/assign' }
+ let(:issuable) { issue }
+ end
+
+ it_behaves_like 'unassign command' do
+ let(:content) { '/unassign' }
+ let(:issuable) { issue }
+ end
+
+ it_behaves_like 'unassign command' do
+ let(:content) { '/unassign' }
+ let(:issuable) { merge_request }
+ end
+
+ it_behaves_like 'milestone command' do
+ let(:content) { "/milestone %#{milestone.title}" }
+ let(:issuable) { issue }
+ end
+
+ it_behaves_like 'milestone command' do
+ let(:content) { "/milestone %#{milestone.title}" }
+ let(:issuable) { merge_request }
+ end
+
+ it_behaves_like 'remove_milestone command' do
+ let(:content) { '/remove_milestone' }
+ let(:issuable) { issue }
+ end
+
+ it_behaves_like 'remove_milestone command' do
+ let(:content) { '/remove_milestone' }
+ let(:issuable) { merge_request }
+ end
+
+ it_behaves_like 'label command' do
+ let(:content) { %(/label ~"#{inprogress.title}" ~#{bug.title} ~unknown) }
+ let(:issuable) { issue }
+ end
+
+ it_behaves_like 'label command' do
+ let(:content) { %(/label ~"#{inprogress.title}" ~#{bug.title} ~unknown) }
+ let(:issuable) { merge_request }
+ end
+
+ it_behaves_like 'unlabel command' do
+ let(:content) { %(/unlabel ~"#{inprogress.title}") }
+ let(:issuable) { issue }
+ end
+
+ it_behaves_like 'unlabel command' do
+ let(:content) { %(/unlabel ~"#{inprogress.title}") }
+ let(:issuable) { merge_request }
+ end
+
+ it_behaves_like 'unlabel command with no argument' do
+ let(:content) { %(/unlabel) }
+ let(:issuable) { issue }
+ end
+
+ it_behaves_like 'unlabel command with no argument' do
+ let(:content) { %(/unlabel) }
+ let(:issuable) { merge_request }
+ end
+
+ it_behaves_like 'relabel command' do
+ let(:content) { %(/relabel ~"#{inprogress.title}") }
+ let(:issuable) { issue }
+ end
+
+ it_behaves_like 'relabel command' do
+ let(:content) { %(/relabel ~"#{inprogress.title}") }
+ let(:issuable) { merge_request }
+ end
+
+ it_behaves_like 'todo command' do
+ let(:content) { '/todo' }
+ let(:issuable) { issue }
+ end
+
+ it_behaves_like 'todo command' do
+ let(:content) { '/todo' }
+ let(:issuable) { merge_request }
+ end
+
+ it_behaves_like 'done command' do
+ let(:content) { '/done' }
+ let(:issuable) { issue }
+ end
+
+ it_behaves_like 'done command' do
+ let(:content) { '/done' }
+ let(:issuable) { merge_request }
+ end
+
+ it_behaves_like 'subscribe command' do
+ let(:content) { '/subscribe' }
+ let(:issuable) { issue }
+ end
+
+ it_behaves_like 'subscribe command' do
+ let(:content) { '/subscribe' }
+ let(:issuable) { merge_request }
+ end
+
+ it_behaves_like 'unsubscribe command' do
+ let(:content) { '/unsubscribe' }
+ let(:issuable) { issue }
+ end
+
+ it_behaves_like 'unsubscribe command' do
+ let(:content) { '/unsubscribe' }
+ let(:issuable) { merge_request }
+ end
+
+ it_behaves_like 'due command' do
+ let(:content) { '/due 2016-08-28' }
+ let(:issuable) { issue }
+ end
+
+ it_behaves_like 'due command' do
+ let(:content) { '/due tomorrow' }
+ let(:issuable) { issue }
+ let(:expected_date) { Date.tomorrow }
+ end
+
+ it_behaves_like 'due command' do
+ let(:content) { '/due 5 days from now' }
+ let(:issuable) { issue }
+ let(:expected_date) { 5.days.from_now.to_date }
+ end
+
+ it_behaves_like 'due command' do
+ let(:content) { '/due in 2 days' }
+ let(:issuable) { issue }
+ let(:expected_date) { 2.days.from_now.to_date }
+ end
+
+ it_behaves_like 'empty command' do
+ let(:content) { '/due foo bar' }
+ let(:issuable) { issue }
+ end
+
+ it_behaves_like 'empty command' do
+ let(:content) { '/due 2016-08-28' }
+ let(:issuable) { merge_request }
+ end
+
+ it_behaves_like 'remove_due_date command' do
+ let(:content) { '/remove_due_date' }
+ let(:issuable) { issue }
+ end
+
+ it_behaves_like 'empty command' do
+ let(:content) { '/remove_due_date' }
+ let(:issuable) { merge_request }
+ end
+ end
+end
diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb
index 43693441450..3d854a959f3 100644
--- a/spec/services/system_note_service_spec.rb
+++ b/spec/services/system_note_service_spec.rb
@@ -330,13 +330,13 @@ describe SystemNoteService, services: true do
let(:mentioner) { project2.repository.commit }
it 'references the mentioning commit' do
- expect(subject.note).to eq "mentioned in commit #{mentioner.to_reference(project)}"
+ expect(subject.note).to eq "Mentioned in commit #{mentioner.to_reference(project)}"
end
end
context 'from non-Commit' do
it 'references the mentioning object' do
- expect(subject.note).to eq "mentioned in issue #{mentioner.to_reference(project)}"
+ expect(subject.note).to eq "Mentioned in issue #{mentioner.to_reference(project)}"
end
end
end
@@ -346,13 +346,13 @@ describe SystemNoteService, services: true do
let(:mentioner) { project.repository.commit }
it 'references the mentioning commit' do
- expect(subject.note).to eq "mentioned in commit #{mentioner.to_reference}"
+ expect(subject.note).to eq "Mentioned in commit #{mentioner.to_reference}"
end
end
context 'from non-Commit' do
it 'references the mentioning object' do
- expect(subject.note).to eq "mentioned in issue #{mentioner.to_reference}"
+ expect(subject.note).to eq "Mentioned in issue #{mentioner.to_reference}"
end
end
end
@@ -362,7 +362,7 @@ describe SystemNoteService, services: true do
describe '.cross_reference?' do
it 'is truthy when text begins with expected text' do
- expect(described_class.cross_reference?('mentioned in something')).to be_truthy
+ expect(described_class.cross_reference?('Mentioned in something')).to be_truthy
end
it 'is falsey when text does not begin with expected text' do
@@ -471,15 +471,15 @@ describe SystemNoteService, services: true do
shared_examples 'cross project mentionable' do
include GitlabMarkdownHelper
- it 'should contain cross reference to new noteable' do
+ it 'contains cross reference to new noteable' do
expect(subject.note).to include cross_project_reference(new_project, new_noteable)
end
- it 'should mention referenced noteable' do
+ it 'mentions referenced noteable' do
expect(subject.note).to include new_noteable.to_reference
end
- it 'should mention referenced project' do
+ it 'mentions referenced project' do
expect(subject.note).to include new_project.to_reference
end
end
@@ -489,7 +489,7 @@ describe SystemNoteService, services: true do
it_behaves_like 'cross project mentionable'
- it 'should notify about noteable being moved to' do
+ it 'notifies about noteable being moved to' do
expect(subject.note).to match /Moved to/
end
end
@@ -499,7 +499,7 @@ describe SystemNoteService, services: true do
it_behaves_like 'cross project mentionable'
- it 'should notify about noteable being moved from' do
+ it 'notifies about noteable being moved from' do
expect(subject.note).to match /Moved from/
end
end
@@ -507,7 +507,7 @@ describe SystemNoteService, services: true do
context 'invalid direction' do
let(:direction) { :invalid }
- it 'should raise error' do
+ it 'raises error' do
expect { subject }.to raise_error StandardError, /Invalid direction/
end
end
diff --git a/spec/services/test_hook_service_spec.rb b/spec/services/test_hook_service_spec.rb
index f034f251ba4..4f6dd8c6d3f 100644
--- a/spec/services/test_hook_service_spec.rb
+++ b/spec/services/test_hook_service_spec.rb
@@ -5,8 +5,8 @@ describe TestHookService, services: true do
let(:project) { create :project }
let(:hook) { create :project_hook, project: project }
- describe :execute do
- it "should execute successfully" do
+ describe '#execute' do
+ it "executes successfully" do
stub_request(:post, hook.url).to_return(status: 200)
expect(TestHookService.new.execute(hook, user)).to be_truthy
end
diff --git a/spec/services/todo_service_spec.rb b/spec/services/todo_service_spec.rb
index b4522536724..cafcad3e3c0 100644
--- a/spec/services/todo_service_spec.rb
+++ b/spec/services/todo_service_spec.rb
@@ -35,8 +35,11 @@ describe TodoService, services: true do
should_not_create_any_todo { service.new_issue(unassigned_issue, author) }
end
- it 'does not create a todo if assignee is the current user' do
- should_not_create_any_todo { service.new_issue(unassigned_issue, john_doe) }
+ it 'creates a todo if assignee is the current user' do
+ unassigned_issue.update_attribute(:assignee, john_doe)
+ service.new_issue(unassigned_issue, john_doe)
+
+ should_create_todo(user: john_doe, target: unassigned_issue, author: john_doe, action: Todo::ASSIGNED)
end
it 'creates a todo for each valid mentioned user' do
@@ -44,7 +47,7 @@ describe TodoService, services: true do
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_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
@@ -57,7 +60,7 @@ describe TodoService, services: true do
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)
+ should_create_todo(user: john_doe, target: confidential_issue, author: john_doe, action: Todo::MENTIONED)
end
context 'when a private group is mentioned' do
@@ -87,7 +90,7 @@ describe TodoService, services: true do
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_create_todo(user: author, target: issue, action: Todo::MENTIONED)
should_not_create_todo(user: non_member, target: issue, action: Todo::MENTIONED)
end
@@ -105,7 +108,7 @@ describe TodoService, services: true do
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)
+ should_create_todo(user: john_doe, target: confidential_issue, author: john_doe, action: Todo::MENTIONED)
end
context 'issues with a task list' do
@@ -156,10 +159,11 @@ describe TodoService, services: true do
should_not_create_any_todo { service.reassigned_issue(issue, author) }
end
- it 'does not create a todo if new assignee is the current user' do
+ it 'creates a todo if new assignee is the current user' do
unassigned_issue.update_attribute(:assignee, john_doe)
+ service.reassigned_issue(unassigned_issue, john_doe)
- should_not_create_any_todo { service.reassigned_issue(unassigned_issue, john_doe) }
+ should_create_todo(user: john_doe, target: unassigned_issue, author: john_doe, action: Todo::ASSIGNED)
end
end
@@ -190,12 +194,12 @@ describe TodoService, services: true do
end
end
- describe '#mark_todos_as_done' do
- it 'marks related todos for the user as done' do
- first_todo = create(:todo, :assigned, user: john_doe, project: project, target: issue, author: author)
- second_todo = create(:todo, :assigned, user: john_doe, project: project, target: issue, author: author)
+ shared_examples 'marking todos as done' do |meth|
+ let!(:first_todo) { create(:todo, :assigned, user: john_doe, project: project, target: issue, author: author) }
+ let!(:second_todo) { create(:todo, :assigned, user: john_doe, project: project, target: issue, author: author) }
- service.mark_todos_as_done([first_todo, second_todo], john_doe)
+ it 'marks related todos for the user as done' do
+ service.send(meth, collection, john_doe)
expect(first_todo.reload).to be_done
expect(second_todo.reload).to be_done
@@ -203,20 +207,30 @@ describe TodoService, services: true do
describe 'cached counts' do
it 'updates when todos change' do
- todo = create(:todo, :assigned, user: john_doe, project: project, target: issue, author: author)
-
expect(john_doe.todos_done_count).to eq(0)
- expect(john_doe.todos_pending_count).to eq(1)
+ expect(john_doe.todos_pending_count).to eq(2)
expect(john_doe).to receive(:update_todos_count_cache).and_call_original
- service.mark_todos_as_done([todo], john_doe)
+ service.send(meth, collection, john_doe)
- expect(john_doe.todos_done_count).to eq(1)
+ expect(john_doe.todos_done_count).to eq(2)
expect(john_doe.todos_pending_count).to eq(0)
end
end
end
+ describe '#mark_todos_as_done' do
+ it_behaves_like 'marking todos as done', :mark_todos_as_done do
+ let(:collection) { [first_todo, second_todo] }
+ end
+ end
+
+ describe '#mark_todos_as_done_by_ids' do
+ it_behaves_like 'marking todos as done', :mark_todos_as_done_by_ids do
+ let(:collection) { [first_todo, second_todo].map(&:id) }
+ end
+ end
+
describe '#new_note' do
let!(:first_todo) { create(:todo, :assigned, user: john_doe, project: project, target: issue, author: author) }
let!(:second_todo) { create(:todo, :assigned, user: john_doe, project: project, target: issue, author: author) }
@@ -250,7 +264,7 @@ describe TodoService, services: true do
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_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
@@ -262,7 +276,7 @@ describe TodoService, services: true do
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)
+ should_create_todo(user: john_doe, target: confidential_issue, author: john_doe, action: Todo::MENTIONED, note: note_on_confidential_issue)
end
it 'creates a todo for each valid mentioned user when leaving a note on commit' do
@@ -270,7 +284,7 @@ describe TodoService, services: true do
should_create_todo(user: member, target_id: nil, target_type: 'Commit', commit_id: note_on_commit.commit_id, author: john_doe, action: Todo::MENTIONED, note: note_on_commit)
should_create_todo(user: author, target_id: nil, target_type: 'Commit', commit_id: note_on_commit.commit_id, author: john_doe, action: Todo::MENTIONED, note: note_on_commit)
- should_not_create_todo(user: john_doe, target_id: nil, target_type: 'Commit', commit_id: note_on_commit.commit_id, author: john_doe, action: Todo::MENTIONED, note: note_on_commit)
+ should_create_todo(user: john_doe, target_id: nil, target_type: 'Commit', commit_id: note_on_commit.commit_id, author: john_doe, action: Todo::MENTIONED, note: note_on_commit)
should_not_create_todo(user: non_member, target_id: nil, target_type: 'Commit', commit_id: note_on_commit.commit_id, author: john_doe, action: Todo::MENTIONED, note: note_on_commit)
end
@@ -286,6 +300,18 @@ describe TodoService, services: true do
should_create_todo(user: author, target: unassigned_issue, action: Todo::MARKED)
end
end
+
+ describe '#todo_exists?' do
+ it 'returns false when no todo exist for the given issuable' do
+ expect(service.todo_exist?(unassigned_issue, author)).to be_falsy
+ end
+
+ it 'returns true when a todo exist for the given issuable' do
+ service.mark_todo(unassigned_issue, author)
+
+ expect(service.todo_exist?(unassigned_issue, author)).to be_truthy
+ end
+ end
end
describe 'Merge Requests' do
@@ -312,7 +338,7 @@ describe TodoService, services: true do
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_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)
end
@@ -325,7 +351,7 @@ describe TodoService, services: true do
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_create_todo(user: author, target: mr_assigned, action: Todo::MENTIONED)
should_not_create_todo(user: non_member, target: mr_assigned, action: Todo::MENTIONED)
end
@@ -382,10 +408,11 @@ describe TodoService, services: true do
should_not_create_any_todo { service.reassigned_merge_request(mr_assigned, author) }
end
- it 'does not create a todo if new assignee is the current user' do
+ it 'creates a todo if new assignee is the current user' do
mr_assigned.update_attribute(:assignee, john_doe)
+ service.reassigned_merge_request(mr_assigned, john_doe)
- should_not_create_any_todo { service.reassigned_merge_request(mr_assigned, john_doe) }
+ should_create_todo(user: john_doe, target: mr_assigned, author: john_doe, action: Todo::ASSIGNED)
end
end
@@ -435,6 +462,24 @@ describe TodoService, services: true do
should_create_todo(user: author, target: mr_unassigned, action: Todo::MARKED)
end
end
+
+ describe '#new_note' do
+ let(:mention) { john_doe.to_reference }
+ let(:diff_note_on_merge_request) { create(:diff_note_on_merge_request, project: project, noteable: mr_unassigned, author: author, note: "Hey #{mention}") }
+ let(:legacy_diff_note_on_merge_request) { create(:legacy_diff_note_on_merge_request, project: project, noteable: mr_unassigned, author: author, note: "Hey #{mention}") }
+
+ it 'creates a todo for mentioned user on new diff note' do
+ service.new_note(diff_note_on_merge_request, author)
+
+ should_create_todo(user: john_doe, target: mr_unassigned, author: author, action: Todo::MENTIONED, note: diff_note_on_merge_request)
+ end
+
+ it 'creates a todo for mentioned user on legacy diff note' do
+ service.new_note(legacy_diff_note_on_merge_request, author)
+
+ should_create_todo(user: john_doe, target: mr_unassigned, author: author, action: Todo::MENTIONED, note: legacy_diff_note_on_merge_request)
+ end
+ end
end
it 'updates cached counts when a todo is created' do
@@ -449,6 +494,63 @@ describe TodoService, services: true do
expect(john_doe.todos_pending_count).to eq(1)
end
+ describe '#mark_todos_as_done' do
+ let(:issue) { create(:issue, project: project, author: author, assignee: john_doe) }
+ let(:another_issue) { create(:issue, project: project, author: author, assignee: john_doe) }
+
+ it 'marks a relation of todos as done' do
+ create(:todo, :mentioned, user: john_doe, target: issue, project: project)
+
+ todos = TodosFinder.new(john_doe, {}).execute
+ expect { TodoService.new.mark_todos_as_done(todos, john_doe) }
+ .to change { john_doe.todos.done.count }.from(0).to(1)
+ end
+
+ it 'marks an array of todos as done' do
+ todo = create(:todo, :mentioned, user: john_doe, target: issue, project: project)
+
+ expect { TodoService.new.mark_todos_as_done([todo], john_doe) }
+ .to change { todo.reload.state }.from('pending').to('done')
+ end
+
+ it 'returns the number of updated todos' do # Needed on API
+ todo = create(:todo, :mentioned, user: john_doe, target: issue, project: project)
+
+ expect(TodoService.new.mark_todos_as_done([todo], john_doe)).to eq(1)
+ end
+
+ context 'when some of the todos are done already' do
+ before do
+ create(:todo, :mentioned, user: john_doe, target: issue, project: project)
+ create(:todo, :mentioned, user: john_doe, target: another_issue, project: project)
+ end
+
+ it 'returns the number of those still pending' do
+ TodoService.new.mark_pending_todos_as_done(issue, john_doe)
+
+ expect(TodoService.new.mark_todos_as_done(Todo.all, john_doe)).to eq(1)
+ end
+
+ it 'returns 0 if all are done' do
+ TodoService.new.mark_pending_todos_as_done(issue, john_doe)
+ TodoService.new.mark_pending_todos_as_done(another_issue, john_doe)
+
+ expect(TodoService.new.mark_todos_as_done(Todo.all, john_doe)).to eq(0)
+ end
+ end
+
+ it 'caches the number of todos of a user', :caching do
+ create(:todo, :mentioned, user: john_doe, target: issue, project: project)
+ todo = create(:todo, :mentioned, user: john_doe, target: issue, project: project)
+ TodoService.new.mark_todos_as_done([todo], john_doe)
+
+ expect_any_instance_of(TodosFinder).not_to receive(:execute)
+
+ expect(john_doe.todos_done_count).to eq(1)
+ expect(john_doe.todos_pending_count).to eq(1)
+ end
+ end
+
def should_create_todo(attributes = {})
attributes.reverse_merge!(
project: project,
diff --git a/spec/simplecov_env.rb b/spec/simplecov_env.rb
new file mode 100644
index 00000000000..b507d38f472
--- /dev/null
+++ b/spec/simplecov_env.rb
@@ -0,0 +1,55 @@
+require 'simplecov'
+require 'active_support/core_ext/numeric/time'
+
+module SimpleCovEnv
+ extend self
+
+ def start!
+ return unless ENV['SIMPLECOV']
+
+ configure_profile
+ configure_job
+
+ SimpleCov.start
+ end
+
+ def configure_job
+ SimpleCov.configure do
+ if ENV['CI_BUILD_NAME']
+ coverage_dir "coverage/#{ENV['CI_BUILD_NAME']}"
+ command_name ENV['CI_BUILD_NAME']
+ end
+
+ if ENV['CI']
+ SimpleCov.at_exit do
+ # In CI environment don't generate formatted reports
+ # Only generate .resultset.json
+ SimpleCov.result
+ end
+ end
+ end
+ end
+
+ def configure_profile
+ SimpleCov.configure do
+ load_profile 'test_frameworks'
+ track_files '{app,lib}/**/*.rb'
+
+ add_filter '/vendor/ruby/'
+ add_filter 'config/initializers/'
+
+ add_group 'Controllers', 'app/controllers'
+ add_group 'Models', 'app/models'
+ add_group 'Mailers', 'app/mailers'
+ add_group 'Helpers', 'app/helpers'
+ add_group 'Workers', %w(app/jobs app/workers)
+ add_group 'Libraries', 'lib'
+ add_group 'Services', 'app/services'
+ add_group 'Finders', 'app/finders'
+ add_group 'Uploaders', 'app/uploaders'
+ add_group 'Validators', 'app/validators'
+
+ merge_timeout 365.days
+ end
+ end
+end
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index 606da1b7605..c144cd85487 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -1,7 +1,5 @@
-if ENV['SIMPLECOV']
- require 'simplecov'
- SimpleCov.start :rails
-end
+require './spec/simplecov_env'
+SimpleCovEnv.start!
ENV["RAILS_ENV"] ||= 'test'
@@ -33,9 +31,9 @@ RSpec.configure do |config|
config.include LoginHelpers, type: :request
config.include StubConfiguration
config.include EmailHelpers
- config.include RelativeUrl, type: feature
config.include TestEnv
config.include ActiveJob::TestHelper
+ config.include ActiveSupport::Testing::TimeHelpers
config.include StubGitlabCalls
config.include StubGitlabData
@@ -45,6 +43,13 @@ RSpec.configure do |config|
config.before(:suite) do
TestEnv.init
end
+
+ config.around(:each, :caching) do |example|
+ caching_store = Rails.cache
+ Rails.cache = ActiveSupport::Cache::MemoryStore.new if example.metadata[:caching]
+ example.run
+ Rails.cache = caching_store
+ end
end
FactoryGirl::SyntaxRunner.class_eval do
diff --git a/spec/support/api/members_shared_examples.rb b/spec/support/api/members_shared_examples.rb
new file mode 100644
index 00000000000..dab71a35a55
--- /dev/null
+++ b/spec/support/api/members_shared_examples.rb
@@ -0,0 +1,11 @@
+shared_examples 'a 404 response when source is private' do
+ before do
+ source.update_column(:visibility_level, Gitlab::VisibilityLevel::PRIVATE)
+ end
+
+ it 'returns 404' do
+ route
+
+ expect(response).to have_http_status(404)
+ end
+end
diff --git a/spec/support/api/schema_matcher.rb b/spec/support/api/schema_matcher.rb
new file mode 100644
index 00000000000..e42d727672b
--- /dev/null
+++ b/spec/support/api/schema_matcher.rb
@@ -0,0 +1,8 @@
+RSpec::Matchers.define :match_response_schema do |schema, **options|
+ match do |response|
+ schema_directory = "#{Dir.pwd}/spec/fixtures/api/schemas"
+ schema_path = "#{schema_directory}/#{schema}.json"
+
+ JSON::Validator.validate!(schema_path, response.body, options)
+ end
+end
diff --git a/spec/support/api_helpers.rb b/spec/support/api_helpers.rb
index 1b3cafb497c..68b196d9033 100644
--- a/spec/support/api_helpers.rb
+++ b/spec/support/api_helpers.rb
@@ -24,8 +24,11 @@ module ApiHelpers
(path.index('?') ? '' : '?') +
# Append private_token if given a User object
- (user.respond_to?(:private_token) ?
- "&private_token=#{user.private_token}" : "")
+ if user.respond_to?(:private_token)
+ "&private_token=#{user.private_token}"
+ else
+ ''
+ end
end
def ci_api(path, user = nil)
@@ -35,8 +38,11 @@ module ApiHelpers
(path.index('?') ? '' : '?') +
# Append private_token if given a User object
- (user.respond_to?(:private_token) ?
- "&private_token=#{user.private_token}" : "")
+ if user.respond_to?(:private_token)
+ "&private_token=#{user.private_token}"
+ else
+ ''
+ end
end
def json_response
diff --git a/spec/support/capybara_helpers.rb b/spec/support/capybara_helpers.rb
index 9b5c3065eed..b57a3493aff 100644
--- a/spec/support/capybara_helpers.rb
+++ b/spec/support/capybara_helpers.rb
@@ -27,6 +27,14 @@ module CapybaraHelpers
end
end
end
+
+ # Refresh the page. Calling `visit current_url` doesn't seem to work consistently.
+ #
+ def refresh
+ url = current_url
+ visit 'about:blank'
+ visit url
+ end
end
RSpec.configure do |config|
diff --git a/spec/support/email_helpers.rb b/spec/support/email_helpers.rb
index a85ab22ce36..0bfc4685532 100644
--- a/spec/support/email_helpers.rb
+++ b/spec/support/email_helpers.rb
@@ -3,6 +3,16 @@ module EmailHelpers
ActionMailer::Base.deliveries.map(&:to).flatten.count(user.email) == 1
end
+ def reset_delivered_emails!
+ ActionMailer::Base.deliveries.clear
+ end
+
+ def should_only_email(*users)
+ users.each {|user| should_email(user) }
+ recipients = ActionMailer::Base.deliveries.flat_map(&:to)
+ expect(recipients.count).to eq(users.count)
+ end
+
def should_email(user)
expect(sent_to_user?(user)).to be_truthy
end
diff --git a/spec/support/fake_u2f_device.rb b/spec/support/fake_u2f_device.rb
index 553fe9f1fbc..8c407b867fe 100644
--- a/spec/support/fake_u2f_device.rb
+++ b/spec/support/fake_u2f_device.rb
@@ -1,6 +1,9 @@
class FakeU2fDevice
- def initialize(page)
+ attr_reader :name
+
+ def initialize(page, name)
@page = page
+ @name = name
end
def respond_to_u2f_registration
@@ -18,8 +21,8 @@ class FakeU2fDevice
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])
+ challenge = @page.evaluate_script('gon.u2f.challenge')
+ json_response = u2f_device(app_id).sign_response(challenge)
@page.execute_script("
u2f.sign = function(appId, challenges, signRequests, callback) {
diff --git a/spec/support/import_export/import_export.yml b/spec/support/import_export/import_export.yml
index 3ceec506401..17136dee000 100644
--- a/spec/support/import_export/import_export.yml
+++ b/spec/support/import_export/import_export.yml
@@ -7,6 +7,8 @@ project_tree:
- :merge_request_test
- commit_statuses:
- :commit
+ - project_members:
+ - :user
included_attributes:
project:
@@ -14,6 +16,8 @@ included_attributes:
- :path
merge_requests:
- :id
+ user:
+ - :email
excluded_attributes:
merge_requests:
diff --git a/spec/support/issuable_create_service_slash_commands_shared_examples.rb b/spec/support/issuable_create_service_slash_commands_shared_examples.rb
new file mode 100644
index 00000000000..5f9645ed44f
--- /dev/null
+++ b/spec/support/issuable_create_service_slash_commands_shared_examples.rb
@@ -0,0 +1,83 @@
+# Specifications for behavior common to all objects with executable attributes.
+# It can take a `default_params`.
+
+shared_examples 'new issuable record that supports slash commands' do
+ let!(:project) { create(:project) }
+ let(:user) { create(:user).tap { |u| project.team << [u, :master] } }
+ let(:assignee) { create(:user) }
+ let!(:milestone) { create(:milestone, project: project) }
+ let!(:labels) { create_list(:label, 3, project: project) }
+ let(:base_params) { { title: FFaker::Lorem.sentence(3) } }
+ let(:params) { base_params.merge(defined?(default_params) ? default_params : {}).merge(example_params) }
+ let(:issuable) { described_class.new(project, user, params).execute }
+
+ context 'with labels in command only' do
+ let(:example_params) do
+ {
+ description: "/label ~#{labels.first.name} ~#{labels.second.name}\n/unlabel ~#{labels.third.name}"
+ }
+ end
+
+ it 'attaches labels to issuable' do
+ expect(issuable).to be_persisted
+ expect(issuable.label_ids).to match_array([labels.first.id, labels.second.id])
+ end
+ end
+
+ context 'with labels in params and command' do
+ let(:example_params) do
+ {
+ label_ids: [labels.second.id],
+ description: "/label ~#{labels.first.name}\n/unlabel ~#{labels.third.name}"
+ }
+ end
+
+ it 'attaches all labels to issuable' do
+ expect(issuable).to be_persisted
+ expect(issuable.label_ids).to match_array([labels.first.id, labels.second.id])
+ end
+ end
+
+ context 'with assignee and milestone in command only' do
+ let(:example_params) do
+ {
+ description: %(/assign @#{assignee.username}\n/milestone %"#{milestone.name}")
+ }
+ end
+
+ it 'assigns and sets milestone to issuable' do
+ expect(issuable).to be_persisted
+ expect(issuable.assignee).to eq(assignee)
+ expect(issuable.milestone).to eq(milestone)
+ end
+ end
+
+ context 'with assignee and milestone in params and command' do
+ let(:example_params) do
+ {
+ assignee: build_stubbed(:user),
+ milestone_id: double(:milestone),
+ description: %(/assign @#{assignee.username}\n/milestone %"#{milestone.name}")
+ }
+ end
+
+ it 'assigns and sets milestone to issuable from command' do
+ expect(issuable).to be_persisted
+ expect(issuable.assignee).to eq(assignee)
+ expect(issuable.milestone).to eq(milestone)
+ end
+ end
+
+ describe '/close' do
+ let(:example_params) do
+ {
+ description: '/close'
+ }
+ end
+
+ it 'returns an open issue' do
+ expect(issuable).to be_persisted
+ expect(issuable).to be_open
+ end
+ end
+end
diff --git a/spec/support/issuable_slash_commands_shared_examples.rb b/spec/support/issuable_slash_commands_shared_examples.rb
new file mode 100644
index 00000000000..d2a49ea5c5e
--- /dev/null
+++ b/spec/support/issuable_slash_commands_shared_examples.rb
@@ -0,0 +1,289 @@
+# Specifications for behavior common to all objects with executable attributes.
+# It takes a `issuable_type`, and expect an `issuable`.
+
+shared_examples 'issuable record that supports slash commands in its description and notes' do |issuable_type|
+ let(:master) { create(:user) }
+ let(:assignee) { create(:user, username: 'bob') }
+ let(:guest) { create(:user) }
+ let(:project) { create(:project, :public) }
+ let!(:milestone) { create(:milestone, project: project, title: 'ASAP') }
+ let!(:label_bug) { create(:label, project: project, title: 'bug') }
+ let!(:label_feature) { create(:label, project: project, title: 'feature') }
+ let(:new_url_opts) { {} }
+
+ before do
+ project.team << [master, :master]
+ project.team << [assignee, :developer]
+ project.team << [guest, :guest]
+ login_with(master)
+ end
+
+ describe "new #{issuable_type}" do
+ context 'with commands in the description' do
+ it "creates the #{issuable_type} and interpret commands accordingly" do
+ visit public_send("new_namespace_project_#{issuable_type}_path", project.namespace, project, new_url_opts)
+ fill_in "#{issuable_type}_title", with: 'bug 345'
+ fill_in "#{issuable_type}_description", with: "bug description\n/label ~bug\n/milestone %\"ASAP\""
+ click_button "Submit #{issuable_type}".humanize
+
+ issuable = project.public_send(issuable_type.to_s.pluralize).first
+
+ expect(issuable.description).to eq "bug description"
+ expect(issuable.labels).to eq [label_bug]
+ expect(issuable.milestone).to eq milestone
+ expect(page).to have_content 'bug 345'
+ expect(page).to have_content 'bug description'
+ end
+ end
+ end
+
+ describe "note on #{issuable_type}" do
+ before do
+ visit public_send("namespace_project_#{issuable_type}_path", project.namespace, project, issuable)
+ end
+
+ context 'with a note containing commands' do
+ it 'creates a note without the commands and interpret the commands accordingly' do
+ page.within('.js-main-target-form') do
+ fill_in 'note[note]', with: "Awesome!\n/assign @bob\n/label ~bug\n/milestone %\"ASAP\""
+ click_button 'Comment'
+ end
+
+ expect(page).to have_content 'Awesome!'
+ expect(page).not_to have_content '/assign @bob'
+ expect(page).not_to have_content '/label ~bug'
+ expect(page).not_to have_content '/milestone %"ASAP"'
+
+ issuable.reload
+ note = issuable.notes.user.first
+
+ expect(note.note).to eq "Awesome!"
+ expect(issuable.assignee).to eq assignee
+ expect(issuable.labels).to eq [label_bug]
+ expect(issuable.milestone).to eq milestone
+ end
+ end
+
+ context 'with a note containing only commands' do
+ it 'does not create a note but interpret the commands accordingly' do
+ page.within('.js-main-target-form') do
+ fill_in 'note[note]', with: "/assign @bob\n/label ~bug\n/milestone %\"ASAP\""
+ click_button 'Comment'
+ end
+
+ expect(page).not_to have_content '/assign @bob'
+ expect(page).not_to have_content '/label ~bug'
+ expect(page).not_to have_content '/milestone %"ASAP"'
+ expect(page).to have_content 'Your commands have been executed!'
+
+ issuable.reload
+
+ expect(issuable.notes.user).to be_empty
+ expect(issuable.assignee).to eq assignee
+ expect(issuable.labels).to eq [label_bug]
+ expect(issuable.milestone).to eq milestone
+ end
+ end
+
+ context "with a note closing the #{issuable_type}" do
+ before do
+ expect(issuable).to be_open
+ end
+
+ context "when current user can close #{issuable_type}" do
+ it "closes the #{issuable_type}" do
+ page.within('.js-main-target-form') do
+ fill_in 'note[note]', with: "/close"
+ click_button 'Comment'
+ end
+
+ expect(page).not_to have_content '/close'
+ expect(page).to have_content 'Your commands have been executed!'
+
+ expect(issuable.reload).to be_closed
+ end
+ end
+
+ context "when current user cannot close #{issuable_type}" do
+ before do
+ logout
+ login_with(guest)
+ visit public_send("namespace_project_#{issuable_type}_path", project.namespace, project, issuable)
+ end
+
+ it "does not close the #{issuable_type}" do
+ page.within('.js-main-target-form') do
+ fill_in 'note[note]', with: "/close"
+ click_button 'Comment'
+ end
+
+ expect(page).not_to have_content '/close'
+ expect(page).not_to have_content 'Your commands have been executed!'
+
+ expect(issuable).to be_open
+ end
+ end
+ end
+
+ context "with a note reopening the #{issuable_type}" do
+ before do
+ issuable.close
+ expect(issuable).to be_closed
+ end
+
+ context "when current user can reopen #{issuable_type}" do
+ it "reopens the #{issuable_type}" do
+ page.within('.js-main-target-form') do
+ fill_in 'note[note]', with: "/reopen"
+ click_button 'Comment'
+ end
+
+ expect(page).not_to have_content '/reopen'
+ expect(page).to have_content 'Your commands have been executed!'
+
+ expect(issuable.reload).to be_open
+ end
+ end
+
+ context "when current user cannot reopen #{issuable_type}" do
+ before do
+ logout
+ login_with(guest)
+ visit public_send("namespace_project_#{issuable_type}_path", project.namespace, project, issuable)
+ end
+
+ it "does not reopen the #{issuable_type}" do
+ page.within('.js-main-target-form') do
+ fill_in 'note[note]', with: "/reopen"
+ click_button 'Comment'
+ end
+
+ expect(page).not_to have_content '/reopen'
+ expect(page).not_to have_content 'Your commands have been executed!'
+
+ expect(issuable).to be_closed
+ end
+ end
+ end
+
+ context "with a note changing the #{issuable_type}'s title" do
+ context "when current user can change title of #{issuable_type}" do
+ it "reopens the #{issuable_type}" do
+ page.within('.js-main-target-form') do
+ fill_in 'note[note]', with: "/title Awesome new title"
+ click_button 'Comment'
+ end
+
+ expect(page).not_to have_content '/title'
+ expect(page).to have_content 'Your commands have been executed!'
+
+ expect(issuable.reload.title).to eq 'Awesome new title'
+ end
+ end
+
+ context "when current user cannot change title of #{issuable_type}" do
+ before do
+ logout
+ login_with(guest)
+ visit public_send("namespace_project_#{issuable_type}_path", project.namespace, project, issuable)
+ end
+
+ it "does not reopen the #{issuable_type}" do
+ page.within('.js-main-target-form') do
+ fill_in 'note[note]', with: "/title Awesome new title"
+ click_button 'Comment'
+ end
+
+ expect(page).not_to have_content '/title'
+ expect(page).not_to have_content 'Your commands have been executed!'
+
+ expect(issuable.reload.title).not_to eq 'Awesome new title'
+ end
+ end
+ end
+
+ context "with a note marking the #{issuable_type} as todo" do
+ it "creates a new todo for the #{issuable_type}" do
+ page.within('.js-main-target-form') do
+ fill_in 'note[note]', with: "/todo"
+ click_button 'Comment'
+ end
+
+ expect(page).not_to have_content '/todo'
+ expect(page).to have_content 'Your commands have been executed!'
+
+ todos = TodosFinder.new(master).execute
+ todo = todos.first
+
+ expect(todos.size).to eq 1
+ expect(todo).to be_pending
+ expect(todo.target).to eq issuable
+ expect(todo.author).to eq master
+ expect(todo.user).to eq master
+ end
+ end
+
+ context "with a note marking the #{issuable_type} as done" do
+ before do
+ TodoService.new.mark_todo(issuable, master)
+ end
+
+ it "creates a new todo for the #{issuable_type}" do
+ todos = TodosFinder.new(master).execute
+ todo = todos.first
+
+ expect(todos.size).to eq 1
+ expect(todos.first).to be_pending
+ expect(todo.target).to eq issuable
+ expect(todo.author).to eq master
+ expect(todo.user).to eq master
+
+ page.within('.js-main-target-form') do
+ fill_in 'note[note]', with: "/done"
+ click_button 'Comment'
+ end
+
+ expect(page).not_to have_content '/done'
+ expect(page).to have_content 'Your commands have been executed!'
+
+ expect(todo.reload).to be_done
+ end
+ end
+
+ context "with a note subscribing to the #{issuable_type}" do
+ it "creates a new todo for the #{issuable_type}" do
+ expect(issuable.subscribed?(master)).to be_falsy
+
+ page.within('.js-main-target-form') do
+ fill_in 'note[note]', with: "/subscribe"
+ click_button 'Comment'
+ end
+
+ expect(page).not_to have_content '/subscribe'
+ expect(page).to have_content 'Your commands have been executed!'
+
+ expect(issuable.subscribed?(master)).to be_truthy
+ end
+ end
+
+ context "with a note unsubscribing to the #{issuable_type} as done" do
+ before do
+ issuable.subscribe(master)
+ end
+
+ it "creates a new todo for the #{issuable_type}" do
+ expect(issuable.subscribed?(master)).to be_truthy
+
+ page.within('.js-main-target-form') do
+ fill_in 'note[note]', with: "/unsubscribe"
+ click_button 'Comment'
+ end
+
+ expect(page).not_to have_content '/unsubscribe'
+ expect(page).to have_content 'Your commands have been executed!'
+
+ expect(issuable.subscribed?(master)).to be_falsy
+ end
+ end
+ end
+end
diff --git a/spec/support/issue_helpers.rb b/spec/support/issue_helpers.rb
new file mode 100644
index 00000000000..85241793743
--- /dev/null
+++ b/spec/support/issue_helpers.rb
@@ -0,0 +1,13 @@
+module IssueHelpers
+ def visit_issues(project, opts = {})
+ visit namespace_project_issues_path project.namespace, project, opts
+ end
+
+ def first_issue
+ page.all('ul.issues-list > li').first.text
+ end
+
+ def last_issue
+ page.all('ul.issues-list > li').last.text
+ end
+end
diff --git a/spec/support/login_helpers.rb b/spec/support/login_helpers.rb
index ffdf2bb0a8a..e5f76afbfc0 100644
--- a/spec/support/login_helpers.rb
+++ b/spec/support/login_helpers.rb
@@ -37,6 +37,40 @@ module LoginHelpers
Thread.current[:current_user] = user
end
+ def login_via(provider, user, uid)
+ mock_auth_hash(provider, uid, user.email)
+ visit new_user_session_path
+ click_link provider
+ end
+
+ def mock_auth_hash(provider, uid, email)
+ # The mock_auth configuration allows you to set per-provider (or default)
+ # authentication hashes to return during integration testing.
+ OmniAuth.config.mock_auth[provider.to_sym] = OmniAuth::AuthHash.new({
+ provider: provider,
+ uid: uid,
+ info: {
+ name: 'mockuser',
+ email: email,
+ image: 'mock_user_thumbnail_url'
+ },
+ credentials: {
+ token: 'mock_token',
+ secret: 'mock_secret'
+ },
+ extra: {
+ raw_info: {
+ info: {
+ name: 'mockuser',
+ email: email,
+ image: 'mock_user_thumbnail_url'
+ }
+ }
+ }
+ })
+ Rails.application.env_config['omniauth.auth'] = OmniAuth.config.mock_auth[:saml]
+ end
+
# Requires Javascript driver.
def logout
find(".header-user-dropdown-toggle").click
diff --git a/spec/support/matchers/markdown_matchers.rb b/spec/support/matchers/markdown_matchers.rb
index e005058ba5b..8c98b1f988c 100644
--- a/spec/support/matchers/markdown_matchers.rb
+++ b/spec/support/matchers/markdown_matchers.rb
@@ -178,6 +178,17 @@ module MarkdownMatchers
expect(actual).to have_selector('span.idiff.deletion', count: 2)
end
end
+
+ # VideoLinkFilter
+ matcher :parse_video_links do
+ set_default_markdown_messages
+
+ match do |actual|
+ video = actual.at_css('video')
+
+ expect(video['src']).to end_with('/assets/videos/gitlab-demo.mp4')
+ end
+ end
end
# Monkeypatch the matcher DSL so that we can reduce some noisy duplication for
diff --git a/spec/support/merge_request_helpers.rb b/spec/support/merge_request_helpers.rb
new file mode 100644
index 00000000000..d5801c8272f
--- /dev/null
+++ b/spec/support/merge_request_helpers.rb
@@ -0,0 +1,13 @@
+module MergeRequestHelpers
+ def visit_merge_requests(project, opts = {})
+ visit namespace_project_merge_requests_path project.namespace, project, opts
+ end
+
+ def first_merge_request
+ page.all('ul.mr-list > li').first.text
+ end
+
+ def last_merge_request
+ page.all('ul.mr-list > li').last.text
+ end
+end
diff --git a/spec/support/omni_auth.rb b/spec/support/omni_auth.rb
new file mode 100644
index 00000000000..0b1af4052ff
--- /dev/null
+++ b/spec/support/omni_auth.rb
@@ -0,0 +1 @@
+OmniAuth.config.test_mode = true
diff --git a/spec/support/relative_url.rb b/spec/support/relative_url.rb
deleted file mode 100644
index 72e3ccce75b..00000000000
--- a/spec/support/relative_url.rb
+++ /dev/null
@@ -1,8 +0,0 @@
-# Fix route helpers in tests (e.g. root_path, ...)
-module RelativeUrl
- extend ActiveSupport::Concern
-
- included do
- default_url_options[:script_name] = Rails.application.config.relative_url_root
- end
-end
diff --git a/spec/support/select2_helper.rb b/spec/support/select2_helper.rb
index 04d25b5e9e9..35cc51725c6 100644
--- a/spec/support/select2_helper.rb
+++ b/spec/support/select2_helper.rb
@@ -11,7 +11,7 @@
#
module Select2Helper
- def select2(value, options={})
+ def select2(value, options = {})
raise ArgumentError, 'options must be a Hash' unless options.kind_of?(Hash)
selector = options.fetch(:from)
diff --git a/spec/support/test_env.rb b/spec/support/test_env.rb
index 6b99b0f24cb..c7a45fc4ff9 100644
--- a/spec/support/test_env.rb
+++ b/spec/support/test_env.rb
@@ -5,19 +5,32 @@ module TestEnv
# When developing the seed repository, comment out the branch you will modify.
BRANCH_SHA = {
- 'empty-branch' => '7efb185',
- 'flatten-dir' => 'e56497b',
- 'feature' => '0b4bc9a',
- 'feature_conflict' => 'bb5206f',
- 'fix' => '48f0be4',
- 'improve/awesome' => '5937ac0',
- 'markdown' => '0ed8c6c',
- 'lfs' => 'be93687',
- 'master' => '5937ac0',
- "'test'" => 'e56497b',
- 'orphaned-branch' => '45127a9',
- 'binary-encoding' => '7b1cf43',
- 'gitattributes' => '5a62481',
+ 'empty-branch' => '7efb185',
+ 'ends-with.json' => '98b0d8b3',
+ 'flatten-dir' => 'e56497b',
+ 'feature' => '0b4bc9a',
+ 'feature_conflict' => 'bb5206f',
+ 'fix' => '48f0be4',
+ 'improve/awesome' => '5937ac0',
+ 'markdown' => '0ed8c6c',
+ 'lfs' => 'be93687',
+ 'master' => '5937ac0',
+ "'test'" => 'e56497b',
+ 'orphaned-branch' => '45127a9',
+ 'binary-encoding' => '7b1cf43',
+ 'gitattributes' => '5a62481',
+ 'expand-collapse-diffs' => '4842455',
+ 'expand-collapse-files' => '025db92',
+ 'expand-collapse-lines' => '238e82d',
+ 'video' => '8879059',
+ 'crlf-diff' => '5938907',
+ 'conflict-start' => '75284c7',
+ 'conflict-resolvable' => '1450cd6',
+ 'conflict-binary-file' => '259a6fb',
+ 'conflict-contains-conflict-markers' => '5e0964c',
+ 'conflict-missing-side' => 'eb227b3',
+ 'conflict-non-utf8' => 'd0a293c',
+ 'conflict-too-large' => '39fa04f',
}
# gitlab-test-fork is a fork of gitlab-fork, but we don't necessarily
diff --git a/spec/support/updating_mentions_shared_examples.rb b/spec/support/updating_mentions_shared_examples.rb
new file mode 100644
index 00000000000..e0c59a5c280
--- /dev/null
+++ b/spec/support/updating_mentions_shared_examples.rb
@@ -0,0 +1,32 @@
+RSpec.shared_examples 'updating mentions' do |service_class|
+ let(:mentioned_user) { create(:user) }
+ let(:service_class) { service_class }
+
+ before { project.team << [mentioned_user, :developer] }
+
+ def update_mentionable(opts)
+ reset_delivered_emails!
+
+ perform_enqueued_jobs do
+ service_class.new(project, user, opts).execute(mentionable)
+ end
+
+ mentionable.reload
+ end
+
+ context 'in title' do
+ before { update_mentionable(title: mentioned_user.to_reference) }
+
+ it 'emails only the newly-mentioned user' do
+ should_only_email(mentioned_user)
+ end
+ end
+
+ context 'in description' do
+ before { update_mentionable(description: mentioned_user.to_reference) }
+
+ it 'emails only the newly-mentioned user' do
+ should_only_email(mentioned_user)
+ end
+ end
+end
diff --git a/spec/tasks/gitlab/backup_rake_spec.rb b/spec/tasks/gitlab/backup_rake_spec.rb
index d2c056d8e14..548e7780c36 100644
--- a/spec/tasks/gitlab/backup_rake_spec.rb
+++ b/spec/tasks/gitlab/backup_rake_spec.rb
@@ -42,7 +42,7 @@ describe 'gitlab:app namespace rake task' do
before do
allow(Dir).to receive(:glob).and_return([])
allow(Dir).to receive(:chdir)
- allow(File).to receive(:exists?).and_return(true)
+ allow(File).to receive(:exist?).and_return(true)
allow(Kernel).to receive(:system).and_return(true)
allow(FileUtils).to receive(:cp_r).and_return(true)
allow(FileUtils).to receive(:mv).and_return(true)
@@ -53,7 +53,7 @@ describe 'gitlab:app namespace rake task' do
let(:gitlab_version) { Gitlab::VERSION }
- it 'should fail on mismatch' do
+ it 'fails on mismatch' do
allow(YAML).to receive(:load_file).
and_return({ gitlab_version: "not #{gitlab_version}" })
@@ -61,7 +61,7 @@ describe 'gitlab:app namespace rake task' do
to raise_error(SystemExit)
end
- it 'should invoke restoration on match' do
+ it 'invokes restoration on match' do
allow(YAML).to receive(:load_file).
and_return({ gitlab_version: gitlab_version })
expect(Rake::Task['gitlab:db:drop_tables']).to receive(:invoke)
@@ -107,7 +107,7 @@ describe 'gitlab:app namespace rake task' do
end
context 'archive file permissions' do
- it 'should set correct permissions on the tar file' do
+ it 'sets correct permissions on the tar file' do
expect(File.exist?(@backup_tar)).to be_truthy
expect(File::Stat.new(@backup_tar).mode.to_s(8)).to eq('100600')
end
@@ -127,7 +127,7 @@ describe 'gitlab:app namespace rake task' do
end
end
- it 'should set correct permissions on the tar contents' do
+ it 'sets 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 registry.tar.gz}
)
@@ -142,7 +142,7 @@ describe 'gitlab:app namespace rake task' do
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
+ it 'deletes temp directories' do
temp_dirs = Dir.glob(
File.join(Gitlab.config.backup.path, '{db,repositories,uploads,builds,artifacts,lfs,registry}')
)
@@ -153,7 +153,7 @@ describe 'gitlab:app namespace rake task' do
context 'registry disabled' do
let(:enable_registry) { false }
- it 'should not create registry.tar.gz' do
+ it 'does not create registry.tar.gz' do
tar_contents, exit_status = Gitlab::Popen.popen(
%W{tar -tvf #{@backup_tar}}
)
@@ -191,7 +191,7 @@ describe 'gitlab:app namespace rake task' do
FileUtils.rm(@backup_tar)
end
- it 'should include repositories in all repository storages' do
+ it 'includes repositories in all repository storages' do
tar_contents, exit_status = Gitlab::Popen.popen(
%W{tar -tvf #{@backup_tar} repositories}
)
diff --git a/spec/tasks/gitlab/db_rake_spec.rb b/spec/tasks/gitlab/db_rake_spec.rb
index 36d03a224e4..fc52c04e78d 100644
--- a/spec/tasks/gitlab/db_rake_spec.rb
+++ b/spec/tasks/gitlab/db_rake_spec.rb
@@ -19,7 +19,7 @@ describe 'gitlab:db namespace rake task' do
end
describe 'configure' do
- it 'should invoke db:migrate when schema has already been loaded' do
+ it 'invokes 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)
@@ -27,7 +27,7 @@ describe 'gitlab:db namespace rake task' do
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
+ it 'invokes 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)
@@ -35,7 +35,7 @@ describe 'gitlab:db namespace rake task' do
expect { run_rake_task('gitlab:db:configure') }.not_to raise_error
end
- it 'should not invoke any other rake tasks during an error' do
+ it 'does 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)
@@ -45,7 +45,7 @@ describe 'gitlab:db namespace rake task' do
allow(ActiveRecord::Base).to receive(:connection).and_call_original
end
- it 'should not invoke seed after a failed schema_load' do
+ it 'does 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)
diff --git a/spec/teaspoon_env.rb b/spec/teaspoon_env.rb
index 69b2b9b6d5b..5ea020f313c 100644
--- a/spec/teaspoon_env.rb
+++ b/spec/teaspoon_env.rb
@@ -38,7 +38,7 @@ Teaspoon.configure do |config|
# Specify a file matcher as a regular expression and all matching files will be loaded when the suite is run. These
# files need to be within an asset path. You can add asset paths using the `config.asset_paths`.
- suite.matcher = "{spec/javascripts,app/assets}/**/*_spec.{js,js.coffee,coffee}"
+ suite.matcher = "{spec/javascripts,app/assets}/**/*_spec.{js,js.es6,es6}"
# Load additional JS files, but requiring them in your spec helper is the preferred way to do this.
# suite.javascripts = []
@@ -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 = true
# You can have multiple coverage configs by passing a name to config.coverage.
# e.g. config.coverage :ci do |coverage|
@@ -158,15 +158,15 @@ 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-javascript"
# 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{vendor/}, %r{spec/}]
# 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.
diff --git a/spec/uploaders/file_uploader_spec.rb b/spec/uploaders/file_uploader_spec.rb
new file mode 100644
index 00000000000..e8300abed5d
--- /dev/null
+++ b/spec/uploaders/file_uploader_spec.rb
@@ -0,0 +1,45 @@
+require 'spec_helper'
+
+describe FileUploader do
+ let(:project) { create(:project) }
+
+ before do
+ @previous_enable_processing = FileUploader.enable_processing
+ FileUploader.enable_processing = false
+ @uploader = FileUploader.new(project)
+ end
+
+ after do
+ FileUploader.enable_processing = @previous_enable_processing
+ @uploader.remove!
+ end
+
+ describe '#image_or_video?' do
+ context 'given an image file' do
+ before do
+ @uploader.store!(File.new(Rails.root.join('spec', 'fixtures', 'rails_sample.jpg')))
+ end
+
+ it 'detects an image based on file extension' do
+ expect(@uploader.image_or_video?).to be true
+ end
+ end
+
+ context 'given an video file' do
+ before do
+ video_file = File.new(Rails.root.join('spec', 'fixtures', 'video_sample.mp4'))
+ @uploader.store!(video_file)
+ end
+
+ it 'detects a video based on file extension' do
+ expect(@uploader.image_or_video?).to be true
+ end
+ end
+
+ it 'does not return image_or_video? for other types' do
+ @uploader.store!(File.new(Rails.root.join('spec', 'fixtures', 'doc_sample.txt')))
+
+ expect(@uploader.image_or_video?).to be false
+ end
+ end
+end
diff --git a/spec/views/admin/dashboard/index.html.haml_spec.rb b/spec/views/admin/dashboard/index.html.haml_spec.rb
new file mode 100644
index 00000000000..dae858a52f6
--- /dev/null
+++ b/spec/views/admin/dashboard/index.html.haml_spec.rb
@@ -0,0 +1,20 @@
+require 'spec_helper'
+
+describe 'admin/dashboard/index.html.haml' do
+ include Devise::TestHelpers
+
+ before do
+ assign(:projects, create_list(:empty_project, 1))
+ assign(:users, create_list(:user, 1))
+ assign(:groups, create_list(:group, 1))
+
+ allow(view).to receive(:admin?).and_return(true)
+ end
+
+ it "shows version of GitLab Workhorse" do
+ render
+
+ expect(rendered).to have_content 'GitLab Workhorse'
+ expect(rendered).to have_content Gitlab::Workhorse.version
+ end
+end
diff --git a/spec/views/devise/shared/_signin_box.html.haml_spec.rb b/spec/views/devise/shared/_signin_box.html.haml_spec.rb
index 05a76ee4bdb..ee362e6fcb3 100644
--- a/spec/views/devise/shared/_signin_box.html.haml_spec.rb
+++ b/spec/views/devise/shared/_signin_box.html.haml_spec.rb
@@ -31,7 +31,7 @@ describe 'devise/shared/_signin_box' do
def enable_crowd
allow(view).to receive(:form_based_providers).and_return([:crowd])
allow(view).to receive(:crowd_enabled?).and_return(true)
- allow(view).to receive(:user_omniauth_authorize_path).with('crowd').
+ allow(view).to receive(:omniauth_authorize_path).with(:user, :crowd).
and_return('/crowd')
end
end
diff --git a/spec/views/layouts/_head.html.haml_spec.rb b/spec/views/layouts/_head.html.haml_spec.rb
new file mode 100644
index 00000000000..3fddfb3b62f
--- /dev/null
+++ b/spec/views/layouts/_head.html.haml_spec.rb
@@ -0,0 +1,36 @@
+require 'spec_helper'
+
+describe 'layouts/_head' do
+ before do
+ stub_template 'layouts/_user_styles.html.haml' => ''
+ end
+
+ it 'escapes HTML-safe strings in page_title' do
+ stub_helper_with_safe_string(:page_title)
+
+ render
+
+ expect(rendered).to match(%{content="foo&quot; http-equiv=&quot;refresh"})
+ end
+
+ it 'escapes HTML-safe strings in page_description' do
+ stub_helper_with_safe_string(:page_description)
+
+ render
+
+ expect(rendered).to match(%{content="foo&quot; http-equiv=&quot;refresh"})
+ end
+
+ it 'escapes HTML-safe strings in page_image' do
+ stub_helper_with_safe_string(:page_image)
+
+ render
+
+ expect(rendered).to match(%{content="foo&quot; http-equiv=&quot;refresh"})
+ end
+
+ def stub_helper_with_safe_string(method)
+ allow_any_instance_of(PageLayoutHelper).to receive(method)
+ .and_return(%q{foo" http-equiv="refresh}.html_safe)
+ end
+end
diff --git a/spec/views/projects/builds/show.html.haml_spec.rb b/spec/views/projects/builds/show.html.haml_spec.rb
index cd18d19ef5e..464051063d8 100644
--- a/spec/views/projects/builds/show.html.haml_spec.rb
+++ b/spec/views/projects/builds/show.html.haml_spec.rb
@@ -3,8 +3,12 @@ require 'spec_helper'
describe 'projects/builds/show' do
include Devise::TestHelpers
- let(:build) { create(:ci_build) }
- let(:project) { build.project }
+ let(:project) { create(:project) }
+ let(:pipeline) do
+ create(:ci_pipeline, project: project,
+ sha: project.commit.id)
+ end
+ let(:build) { create(:ci_build, pipeline: pipeline) }
before do
assign(:build, build)
@@ -34,4 +38,35 @@ describe 'projects/builds/show' do
expect(rendered).to have_link('Retry')
end
end
+
+ describe 'commit title in sidebar' do
+ let(:commit_title) { project.commit.title }
+
+ it 'shows commit title and not show commit message' do
+ render
+
+ expect(rendered).to have_css('p.build-light-text.append-bottom-0',
+ text: /\A\n#{Regexp.escape(commit_title)}\n\Z/)
+ end
+ end
+
+ describe 'shows trigger variables in sidebar' do
+ let(:trigger_request) { create(:ci_trigger_request_with_variables, pipeline: pipeline) }
+
+ before do
+ build.trigger_request = trigger_request
+ render
+ end
+
+ it 'shows trigger variables in separate lines' do
+ expect(rendered).to have_css('code', text: variable_regexp('TRIGGER_KEY_1', 'TRIGGER_VALUE_1'))
+ expect(rendered).to have_css('code', text: variable_regexp('TRIGGER_KEY_2', 'TRIGGER_VALUE_2'))
+ end
+ end
+
+ private
+
+ def variable_regexp(key, value)
+ /\A#{Regexp.escape("#{key}=#{value}")}\Z/
+ end
end
diff --git a/spec/views/projects/issues/_related_branches.html.haml_spec.rb b/spec/views/projects/issues/_related_branches.html.haml_spec.rb
new file mode 100644
index 00000000000..78af61f15a7
--- /dev/null
+++ b/spec/views/projects/issues/_related_branches.html.haml_spec.rb
@@ -0,0 +1,21 @@
+require 'spec_helper'
+
+describe 'projects/issues/_related_branches' do
+ include Devise::TestHelpers
+
+ let(:project) { create(:project) }
+ let(:branch) { project.repository.find_branch('feature') }
+ let!(:pipeline) { create(:ci_pipeline, project: project, sha: branch.target.id, ref: 'feature') }
+
+ before do
+ assign(:project, project)
+ assign(:related_branches, ['feature'])
+
+ render
+ end
+
+ it 'shows the related branches with their build status' do
+ expect(rendered).to match('feature')
+ expect(rendered).to have_css('.related-branch-ci-status')
+ end
+end
diff --git a/spec/views/projects/merge_requests/_heading.html.haml_spec.rb b/spec/views/projects/merge_requests/_heading.html.haml_spec.rb
new file mode 100644
index 00000000000..733b2dfa7ff
--- /dev/null
+++ b/spec/views/projects/merge_requests/_heading.html.haml_spec.rb
@@ -0,0 +1,26 @@
+require 'spec_helper'
+
+describe 'projects/merge_requests/widget/_heading' do
+ include Devise::TestHelpers
+
+ context 'when released to an environment' do
+ let(:project) { merge_request.target_project }
+ let(:merge_request) { create(:merge_request, :merged) }
+ let(:environment) { create(:environment, project: project) }
+ let!(:deployment) do
+ create(:deployment, environment: environment, sha: project.commit('master').id)
+ end
+
+ before do
+ assign(:merge_request, merge_request)
+ assign(:project, project)
+
+ render
+ end
+
+ it 'displays that the environment is deployed' do
+ expect(rendered).to match("Deployed to")
+ expect(rendered).to match("#{environment.name}")
+ end
+ end
+end
diff --git a/spec/views/projects/tree/show.html.haml_spec.rb b/spec/views/projects/tree/show.html.haml_spec.rb
new file mode 100644
index 00000000000..0f3fc1ee1ac
--- /dev/null
+++ b/spec/views/projects/tree/show.html.haml_spec.rb
@@ -0,0 +1,37 @@
+require 'spec_helper'
+
+describe 'projects/tree/show' do
+ include Devise::TestHelpers
+
+ let(:project) { create(:project) }
+ let(:repository) { project.repository }
+
+ before do
+ assign(:project, project)
+ assign(:repository, repository)
+
+ allow(view).to receive(:can?).and_return(true)
+ allow(view).to receive(:can_collaborate_with_project?).and_return(true)
+ end
+
+ context 'for branch names ending on .json' do
+ let(:ref) { 'ends-with.json' }
+ let(:commit) { repository.commit(ref) }
+ let(:path) { '' }
+ let(:tree) { repository.tree(commit.id, path) }
+
+ before do
+ assign(:ref, ref)
+ assign(:commit, commit)
+ assign(:id, commit.id)
+ assign(:tree, tree)
+ assign(:path, path)
+ end
+
+ it 'displays correctly' do
+ render
+ expect(rendered).to have_css('.js-project-refs-dropdown .dropdown-toggle-text', text: ref)
+ expect(rendered).to have_css('.readme-holder .file-content', text: ref)
+ end
+ end
+end
diff --git a/spec/workers/build_email_worker_spec.rb b/spec/workers/build_email_worker_spec.rb
index 98deae0a588..788b92c1b84 100644
--- a/spec/workers/build_email_worker_spec.rb
+++ b/spec/workers/build_email_worker_spec.rb
@@ -5,7 +5,7 @@ describe BuildEmailWorker do
let(:build) { create(:ci_build) }
let(:user) { create(:user) }
- let(:data) { Gitlab::BuildDataBuilder.build(build) }
+ let(:data) { Gitlab::DataBuilder::Build.build(build) }
subject { BuildEmailWorker.new }
diff --git a/spec/workers/email_receiver_worker_spec.rb b/spec/workers/email_receiver_worker_spec.rb
index de40a6f78af..fe70501eeac 100644
--- a/spec/workers/email_receiver_worker_spec.rb
+++ b/spec/workers/email_receiver_worker_spec.rb
@@ -17,7 +17,7 @@ describe EmailReceiverWorker do
context "when an error occurs" do
before do
- allow_any_instance_of(Gitlab::Email::Receiver).to receive(:execute).and_raise(Gitlab::Email::Receiver::EmptyEmailError)
+ allow_any_instance_of(Gitlab::Email::Receiver).to receive(:execute).and_raise(Gitlab::Email::EmptyEmailError)
end
it "sends out a rejection email" do
diff --git a/spec/workers/emails_on_push_worker_spec.rb b/spec/workers/emails_on_push_worker_spec.rb
index 439da765c2c..7ca2c29da1c 100644
--- a/spec/workers/emails_on_push_worker_spec.rb
+++ b/spec/workers/emails_on_push_worker_spec.rb
@@ -2,25 +2,84 @@ require 'spec_helper'
describe EmailsOnPushWorker do
include RepoHelpers
+ include EmailSpec::Matchers
let(:project) { create(:project) }
let(:user) { create(:user) }
- let(:data) { Gitlab::PushDataBuilder.build_sample(project, user) }
+ let(:data) { Gitlab::DataBuilder::Push.build_sample(project, user) }
let(:recipients) { user.email }
let(:perform) { subject.perform(project.id, recipients, data.stringify_keys) }
+ let(:email) { ActionMailer::Base.deliveries.last }
subject { EmailsOnPushWorker.new }
describe "#perform" do
- context "when there are no errors in sending" do
- let(:email) { ActionMailer::Base.deliveries.last }
+ context "when push is a new branch" do
+ before do
+ data_new_branch = data.stringify_keys.merge("before" => Gitlab::Git::BLANK_SHA)
+
+ subject.perform(project.id, recipients, data_new_branch)
+ end
+
+ it "sends a mail with the correct subject" do
+ expect(email.subject).to include("Pushed new branch")
+ end
+
+ it "sends the mail to the correct recipient" do
+ expect(email.to).to eq([user.email])
+ end
+ end
+
+ context "when push is a deleted branch" do
+ before do
+ data_deleted_branch = data.stringify_keys.merge("after" => Gitlab::Git::BLANK_SHA)
+
+ subject.perform(project.id, recipients, data_deleted_branch)
+ end
+
+ it "sends a mail with the correct subject" do
+ expect(email.subject).to include("Deleted branch")
+ end
+
+ it "sends the mail to the correct recipient" do
+ expect(email.to).to eq([user.email])
+ end
+ end
+
+ context "when push is a force push to delete commits" do
+ before do
+ data_force_push = data.stringify_keys.merge(
+ "after" => data[:before],
+ "before" => data[:after]
+ )
+
+ subject.perform(project.id, recipients, data_force_push)
+ end
+
+ it "sends a mail with the correct subject" do
+ expect(email.subject).to include('Change some files')
+ end
+ it "mentions force pushing in the body" do
+ expect(email).to have_body_text("force push")
+ end
+
+ it "sends the mail to the correct recipient" do
+ expect(email.to).to eq([user.email])
+ end
+ end
+
+ context "when there are no errors in sending" do
before { perform }
it "sends a mail with the correct subject" do
expect(email.subject).to include('Change some files')
end
+ it "does not mention force pushing in the body" do
+ expect(email).not_to have_body_text("force push")
+ end
+
it "sends the mail to the correct recipient" do
expect(email.to).to eq([user.email])
end
@@ -30,6 +89,7 @@ describe EmailsOnPushWorker do
before do
ActionMailer::Base.deliveries.clear
allow(Notify).to receive(:repository_push_email).and_raise(Net::SMTPFatalError)
+ allow(subject).to receive_message_chain(:logger, :info)
perform
end
diff --git a/spec/workers/git_garbage_collect_worker_spec.rb b/spec/workers/git_garbage_collect_worker_spec.rb
new file mode 100644
index 00000000000..c9f5aae0815
--- /dev/null
+++ b/spec/workers/git_garbage_collect_worker_spec.rb
@@ -0,0 +1,27 @@
+require 'spec_helper'
+
+describe GitGarbageCollectWorker do
+ let(:project) { create(:project) }
+ let(:shell) { Gitlab::Shell.new }
+
+ subject { GitGarbageCollectWorker.new }
+
+ before do
+ allow(subject).to receive(:gitlab_shell).and_return(shell)
+ end
+
+ describe "#perform" do
+ it "runs `git gc`" do
+ expect(shell).to receive(:gc).with(
+ project.repository_storage_path,
+ project.path_with_namespace).
+ and_return(true)
+ expect_any_instance_of(Repository).to receive(:after_create_branch).and_call_original
+ expect_any_instance_of(Repository).to receive(:branch_names).and_call_original
+ expect_any_instance_of(Repository).to receive(:branch_count).and_call_original
+ expect_any_instance_of(Repository).to receive(:has_visible_content?).and_call_original
+
+ subject.perform(project.id)
+ end
+ end
+end
diff --git a/spec/workers/group_destroy_worker_spec.rb b/spec/workers/group_destroy_worker_spec.rb
new file mode 100644
index 00000000000..4e4eaf9b2f7
--- /dev/null
+++ b/spec/workers/group_destroy_worker_spec.rb
@@ -0,0 +1,19 @@
+require 'spec_helper'
+
+describe GroupDestroyWorker do
+ let(:group) { create(:group) }
+ let(:user) { create(:admin) }
+ let!(:project) { create(:project, namespace: group) }
+
+ subject { GroupDestroyWorker.new }
+
+ describe "#perform" do
+ it "deletes the project" do
+ subject.perform(group.id, user.id)
+
+ expect(Group.all).not_to include(group)
+ expect(Project.all).not_to include(project)
+ expect(Dir.exist?(project.path)).to be_falsey
+ end
+ end
+end
diff --git a/spec/workers/post_receive_spec.rb b/spec/workers/post_receive_spec.rb
index 20b1a343c27..1d2cf7acddd 100644
--- a/spec/workers/post_receive_spec.rb
+++ b/spec/workers/post_receive_spec.rb
@@ -22,7 +22,7 @@ describe PostReceive do
context "branches" do
let(:changes) { "123456 789012 refs/heads/tést" }
- it "should call GitTagPushService" do
+ it "calls GitTagPushService" do
expect_any_instance_of(GitPushService).to receive(:execute).and_return(true)
expect_any_instance_of(GitTagPushService).not_to receive(:execute)
PostReceive.new.perform(pwd(project), key_id, base64_changes)
@@ -32,7 +32,7 @@ describe PostReceive do
context "tags" do
let(:changes) { "123456 789012 refs/tags/tag" }
- it "should call GitTagPushService" do
+ it "calls GitTagPushService" do
expect_any_instance_of(GitPushService).not_to receive(:execute)
expect_any_instance_of(GitTagPushService).to receive(:execute).and_return(true)
PostReceive.new.perform(pwd(project), key_id, base64_changes)
@@ -42,7 +42,7 @@ describe PostReceive do
context "merge-requests" do
let(:changes) { "123456 789012 refs/merge-requests/123" }
- it "should not call any of the services" do
+ it "does not call any of the services" do
expect_any_instance_of(GitPushService).not_to receive(:execute)
expect_any_instance_of(GitTagPushService).not_to receive(:execute)
PostReceive.new.perform(pwd(project), key_id, base64_changes)
@@ -53,7 +53,13 @@ describe PostReceive 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 }
+ before do
+ allow_any_instance_of(Ci::CreatePipelineService).to receive(:commit) do
+ OpenStruct.new(id: '123456')
+ end
+ allow_any_instance_of(Ci::CreatePipelineService).to receive(:branch?).and_return(true)
+ stub_ci_pipeline_to_return_yaml_file
+ end
it { expect{ subject }.to change{ Ci::Pipeline.count }.by(2) }
end
diff --git a/spec/workers/project_destroy_worker_spec.rb b/spec/workers/project_destroy_worker_spec.rb
new file mode 100644
index 00000000000..1b910d9b91e
--- /dev/null
+++ b/spec/workers/project_destroy_worker_spec.rb
@@ -0,0 +1,24 @@
+require 'spec_helper'
+
+describe ProjectDestroyWorker do
+ let(:project) { create(:project) }
+ let(:path) { project.repository.path_to_repo }
+
+ subject { ProjectDestroyWorker.new }
+
+ describe "#perform" do
+ it "deletes the project" do
+ subject.perform(project.id, project.owner, {})
+
+ expect(Project.all).not_to include(project)
+ expect(Dir.exist?(path)).to be_falsey
+ end
+
+ it "deletes the project but skips repo deletion" do
+ subject.perform(project.id, project.owner, { "skip_repo" => true })
+
+ expect(Project.all).not_to include(project)
+ expect(Dir.exist?(path)).to be_truthy
+ end
+ end
+end
diff --git a/spec/workers/remove_expired_group_links_worker_spec.rb b/spec/workers/remove_expired_group_links_worker_spec.rb
new file mode 100644
index 00000000000..689bc3d27b4
--- /dev/null
+++ b/spec/workers/remove_expired_group_links_worker_spec.rb
@@ -0,0 +1,24 @@
+require 'spec_helper'
+
+describe RemoveExpiredGroupLinksWorker do
+ describe '#perform' do
+ let!(:expired_project_group_link) { create(:project_group_link, expires_at: 1.hour.ago) }
+ let!(:project_group_link_expiring_in_future) { create(:project_group_link, expires_at: 10.days.from_now) }
+ let!(:non_expiring_project_group_link) { create(:project_group_link, expires_at: nil) }
+
+ it 'removes expired group links' do
+ expect { subject.perform }.to change { ProjectGroupLink.count }.by(-1)
+ expect(ProjectGroupLink.find_by(id: expired_project_group_link.id)).to be_nil
+ end
+
+ it 'leaves group links that expire in the future' do
+ subject.perform
+ expect(project_group_link_expiring_in_future.reload).to be_present
+ end
+
+ it 'leaves group links that do not expire at all' do
+ subject.perform
+ expect(non_expiring_project_group_link.reload).to be_present
+ end
+ end
+end
diff --git a/spec/workers/remove_expired_members_worker_spec.rb b/spec/workers/remove_expired_members_worker_spec.rb
new file mode 100644
index 00000000000..402aa1e714e
--- /dev/null
+++ b/spec/workers/remove_expired_members_worker_spec.rb
@@ -0,0 +1,58 @@
+require 'spec_helper'
+
+describe RemoveExpiredMembersWorker do
+ let(:worker) { RemoveExpiredMembersWorker.new }
+
+ describe '#perform' do
+ context 'project members' do
+ let!(:expired_project_member) { create(:project_member, expires_at: 1.hour.ago, access_level: GroupMember::DEVELOPER) }
+ let!(:project_member_expiring_in_future) { create(:project_member, expires_at: 10.days.from_now, access_level: GroupMember::DEVELOPER) }
+ let!(:non_expiring_project_member) { create(:project_member, expires_at: nil, access_level: GroupMember::DEVELOPER) }
+
+ it 'removes expired members' do
+ expect { worker.perform }.to change { Member.count }.by(-1)
+ expect(Member.find_by(id: expired_project_member.id)).to be_nil
+ end
+
+ it 'leaves members that expire in the future' do
+ worker.perform
+ expect(project_member_expiring_in_future.reload).to be_present
+ end
+
+ it 'leaves members that do not expire at all' do
+ worker.perform
+ expect(non_expiring_project_member.reload).to be_present
+ end
+ end
+
+ context 'group members' do
+ let!(:expired_group_member) { create(:group_member, expires_at: 1.hour.ago, access_level: GroupMember::DEVELOPER) }
+ let!(:group_member_expiring_in_future) { create(:group_member, expires_at: 10.days.from_now, access_level: GroupMember::DEVELOPER) }
+ let!(:non_expiring_group_member) { create(:group_member, expires_at: nil, access_level: GroupMember::DEVELOPER) }
+
+ it 'removes expired members' do
+ expect { worker.perform }.to change { Member.count }.by(-1)
+ expect(Member.find_by(id: expired_group_member.id)).to be_nil
+ end
+
+ it 'leaves members that expire in the future' do
+ worker.perform
+ expect(group_member_expiring_in_future.reload).to be_present
+ end
+
+ it 'leaves members that do not expire at all' do
+ worker.perform
+ expect(non_expiring_group_member.reload).to be_present
+ end
+ end
+
+ context 'when the last group owner expires' do
+ let!(:expired_group_owner) { create(:group_member, expires_at: 1.hour.ago, access_level: GroupMember::OWNER) }
+
+ it 'does not delete the owner' do
+ worker.perform
+ expect(expired_group_owner.reload).to be_present
+ end
+ end
+ end
+end
diff --git a/spec/workers/repository_fork_worker_spec.rb b/spec/workers/repository_fork_worker_spec.rb
index 5f762282b5e..60605460adb 100644
--- a/spec/workers/repository_fork_worker_spec.rb
+++ b/spec/workers/repository_fork_worker_spec.rb
@@ -14,21 +14,24 @@ describe RepositoryForkWorker do
describe "#perform" do
it "creates a new repository from a fork" do
expect(shell).to receive(:fork_repository).with(
- project.repository_storage_path,
+ '/test/path',
project.path_with_namespace,
+ project.repository_storage_path,
fork_project.namespace.path
).and_return(true)
subject.perform(
project.id,
+ '/test/path',
project.path_with_namespace,
fork_project.namespace.path)
end
it 'flushes various caches' do
expect(shell).to receive(:fork_repository).with(
- project.repository_storage_path,
+ '/test/path',
project.path_with_namespace,
+ project.repository_storage_path,
fork_project.namespace.path
).and_return(true)
@@ -38,7 +41,7 @@ describe RepositoryForkWorker do
expect_any_instance_of(Repository).to receive(:expire_exists_cache).
and_call_original
- subject.perform(project.id, project.path_with_namespace,
+ subject.perform(project.id, '/test/path', project.path_with_namespace,
fork_project.namespace.path)
end
@@ -49,6 +52,7 @@ describe RepositoryForkWorker do
subject.perform(
project.id,
+ '/test/path',
project.path_with_namespace,
fork_project.namespace.path)
end