summaryrefslogtreecommitdiff
path: root/spec
diff options
context:
space:
mode:
Diffstat (limited to 'spec')
-rw-r--r--spec/controllers/admin/hooks_controller_spec.rb28
-rw-r--r--spec/controllers/admin/services_controller_spec.rb32
-rw-r--r--spec/controllers/application_controller_spec.rb13
-rw-r--r--spec/controllers/blob_controller_spec.rb67
-rw-r--r--spec/controllers/dashboard/todos_controller_spec.rb4
-rw-r--r--spec/controllers/groups/milestones_controller_spec.rb12
-rw-r--r--spec/controllers/groups_controller_spec.rb137
-rw-r--r--spec/controllers/oauth/authorizations_controller_spec.rb55
-rw-r--r--spec/controllers/profiles/personal_access_tokens_controller_spec.rb (renamed from spec/controllers/profiles/personal_access_tokens_spec.rb)0
-rw-r--r--spec/controllers/projects/artifacts_controller_spec.rb188
-rw-r--r--spec/controllers/projects/blob_controller_spec.rb51
-rw-r--r--spec/controllers/projects/boards/issues_controller_spec.rb2
-rw-r--r--spec/controllers/projects/branches_controller_spec.rb135
-rw-r--r--spec/controllers/projects/builds_controller_spec.rb404
-rw-r--r--spec/controllers/projects/builds_controller_specs.rb47
-rw-r--r--spec/controllers/projects/deploy_keys_controller_spec.rb66
-rw-r--r--spec/controllers/projects/deployments_controller_spec.rb116
-rw-r--r--spec/controllers/projects/environments_controller_spec.rb44
-rw-r--r--spec/controllers/projects/issues_controller_spec.rb32
-rw-r--r--spec/controllers/projects/merge_requests_controller_spec.rb256
-rw-r--r--spec/controllers/projects/milestones_controller_spec.rb18
-rw-r--r--spec/controllers/projects/notes_controller_spec.rb41
-rw-r--r--spec/controllers/projects/pages_controller_spec.rb57
-rw-r--r--spec/controllers/projects/pages_domains_controller_spec.rb58
-rw-r--r--spec/controllers/projects/pipeline_schedules_controller_spec.rb87
-rw-r--r--spec/controllers/projects/pipelines_controller_spec.rb65
-rw-r--r--spec/controllers/projects/services_controller_spec.rb49
-rw-r--r--spec/controllers/projects/todos_controller_spec.rb (renamed from spec/controllers/projects/todo_controller_spec.rb)2
-rw-r--r--spec/controllers/projects/wikis_controller_spec.rb16
-rw-r--r--spec/controllers/projects_controller_spec.rb101
-rw-r--r--spec/controllers/snippets/notes_controller_spec.rb196
-rw-r--r--spec/controllers/snippets_controller_spec.rb222
-rw-r--r--spec/controllers/uploads_controller_spec.rb87
-rw-r--r--spec/controllers/users_controller_spec.rb215
-rw-r--r--spec/factories/ci/builds.rb13
-rw-r--r--spec/factories/ci/pipeline_schedule.rb (renamed from spec/factories/ci/trigger_schedules.rb)13
-rw-r--r--spec/factories/ci/variables.rb2
-rw-r--r--spec/factories/environments.rb10
-rw-r--r--spec/factories/groups.rb4
-rw-r--r--spec/factories/issues.rb4
-rw-r--r--spec/factories/merge_requests_closing_issues.rb6
-rw-r--r--spec/factories/notes.rb4
-rw-r--r--spec/factories/project_hooks.rb2
-rw-r--r--spec/factories/projects.rb4
-rw-r--r--spec/factories/services.rb15
-rw-r--r--spec/factories/users.rb4
-rw-r--r--spec/features/admin/admin_cohorts_spec.rb15
-rw-r--r--spec/features/admin/admin_hooks_spec.rb43
-rw-r--r--spec/features/admin/admin_labels_spec.rb2
-rw-r--r--spec/features/admin/admin_requests_profiles_spec.rb69
-rw-r--r--spec/features/admin/admin_users_spec.rb2
-rw-r--r--spec/features/admin/admin_uses_repository_checks_spec.rb2
-rw-r--r--spec/features/atom/dashboard_issues_spec.rb8
-rw-r--r--spec/features/atom/issues_spec.rb8
-rw-r--r--spec/features/atom/users_spec.rb2
-rw-r--r--spec/features/auto_deploy_spec.rb6
-rw-r--r--spec/features/boards/add_issues_modal_spec.rb1
-rw-r--r--spec/features/boards/boards_spec.rb3
-rw-r--r--spec/features/boards/issue_ordering_spec.rb2
-rw-r--r--spec/features/boards/modal_filter_spec.rb2
-rw-r--r--spec/features/boards/new_issue_spec.rb1
-rw-r--r--spec/features/boards/sidebar_spec.rb19
-rw-r--r--spec/features/boards/sub_group_project_spec.rb45
-rw-r--r--spec/features/calendar_spec.rb4
-rw-r--r--spec/features/commits_spec.rb2
-rw-r--r--spec/features/copy_as_gfm_spec.rb8
-rw-r--r--spec/features/cycle_analytics_spec.rb36
-rw-r--r--spec/features/dashboard/datetime_on_tooltips_spec.rb2
-rw-r--r--spec/features/dashboard/group_spec.rb2
-rw-r--r--spec/features/dashboard/groups_list_spec.rb2
-rw-r--r--spec/features/dashboard/issuables_counter_spec.rb8
-rw-r--r--spec/features/dashboard/issues_spec.rb7
-rw-r--r--spec/features/dashboard/milestone_filter_spec.rb60
-rw-r--r--spec/features/dashboard/project_member_activity_index_spec.rb2
-rw-r--r--spec/features/dashboard/shortcuts_spec.rb31
-rw-r--r--spec/features/dashboard/snippets_spec.rb47
-rw-r--r--spec/features/dashboard_issues_spec.rb6
-rw-r--r--spec/features/expand_collapse_diffs_spec.rb4
-rw-r--r--spec/features/explore/groups_list_spec.rb30
-rw-r--r--spec/features/gitlab_flavored_markdown_spec.rb42
-rw-r--r--spec/features/global_search_spec.rb2
-rw-r--r--spec/features/groups/group_settings_spec.rb80
-rw-r--r--spec/features/groups/issues_spec.rb16
-rw-r--r--spec/features/groups/members/sorting_spec.rb4
-rw-r--r--spec/features/issuables/issuable_list_spec.rb5
-rw-r--r--spec/features/issues/award_emoji_spec.rb3
-rw-r--r--spec/features/issues/award_spec.rb6
-rw-r--r--spec/features/issues/bulk_assignment_labels_spec.rb2
-rw-r--r--spec/features/issues/create_branch_merge_request_spec.rb91
-rw-r--r--spec/features/issues/create_issue_for_discussions_in_merge_request_spec.rb8
-rw-r--r--spec/features/issues/create_issue_for_single_discussion_in_merge_request_spec.rb (renamed from spec/features/issues/create_issue_for_single_discussion_in_merge_request.rb)6
-rw-r--r--spec/features/issues/filtered_search/dropdown_assignee_spec.rb1
-rw-r--r--spec/features/issues/filtered_search/dropdown_author_spec.rb1
-rw-r--r--spec/features/issues/filtered_search/dropdown_hint_spec.rb1
-rw-r--r--spec/features/issues/filtered_search/filter_issues_spec.rb13
-rw-r--r--spec/features/issues/filtered_search/recent_searches_spec.rb1
-rw-r--r--spec/features/issues/filtered_search/search_bar_spec.rb1
-rw-r--r--spec/features/issues/form_spec.rb93
-rw-r--r--spec/features/issues/gfm_autocomplete_spec.rb28
-rw-r--r--spec/features/issues/issue_sidebar_spec.rb27
-rw-r--r--spec/features/issues/new_branch_button_spec.rb62
-rw-r--r--spec/features/issues/note_polling_spec.rb75
-rw-r--r--spec/features/issues/notes_on_issues_spec.rb77
-rw-r--r--spec/features/issues/update_issues_spec.rb4
-rw-r--r--spec/features/issues/user_uses_slash_commands_spec.rb1
-rw-r--r--spec/features/issues_spec.rb65
-rw-r--r--spec/features/login_spec.rb4
-rw-r--r--spec/features/markdown_spec.rb2
-rw-r--r--spec/features/merge_requests/assign_issues_spec.rb4
-rw-r--r--spec/features/merge_requests/check_if_mergeable_with_unresolved_discussions_spec.rb10
-rw-r--r--spec/features/merge_requests/cherry_pick_spec.rb2
-rw-r--r--spec/features/merge_requests/closes_issues_spec.rb15
-rw-r--r--spec/features/merge_requests/conflicts_spec.rb4
-rw-r--r--spec/features/merge_requests/create_new_mr_spec.rb2
-rw-r--r--spec/features/merge_requests/created_from_fork_spec.rb2
-rw-r--r--spec/features/merge_requests/deleted_source_branch_spec.rb6
-rw-r--r--spec/features/merge_requests/diff_notes_avatars_spec.rb2
-rw-r--r--spec/features/merge_requests/diff_notes_resolve_spec.rb2
-rw-r--r--spec/features/merge_requests/diffs_spec.rb47
-rw-r--r--spec/features/merge_requests/edit_mr_spec.rb12
-rw-r--r--spec/features/merge_requests/filter_by_labels_spec.rb1
-rw-r--r--spec/features/merge_requests/filter_merge_requests_spec.rb1
-rw-r--r--spec/features/merge_requests/merge_commit_message_toggle_spec.rb19
-rw-r--r--spec/features/merge_requests/merge_immediately_with_pipeline_spec.rb12
-rw-r--r--spec/features/merge_requests/merge_when_pipeline_succeeds_spec.rb22
-rw-r--r--spec/features/merge_requests/mini_pipeline_graph_spec.rb4
-rw-r--r--spec/features/merge_requests/only_allow_merge_if_build_succeeds_spec.rb44
-rw-r--r--spec/features/merge_requests/pipelines_spec.rb2
-rw-r--r--spec/features/merge_requests/reset_filters_spec.rb1
-rw-r--r--spec/features/merge_requests/target_branch_spec.rb11
-rw-r--r--spec/features/merge_requests/update_merge_requests_spec.rb2
-rw-r--r--spec/features/merge_requests/user_posts_notes_spec.rb (renamed from spec/features/merge_requests/user_posts_notes.rb)1
-rw-r--r--spec/features/merge_requests/user_uses_slash_commands_spec.rb2
-rw-r--r--spec/features/merge_requests/versions_spec.rb78
-rw-r--r--spec/features/merge_requests/widget_deployments_spec.rb10
-rw-r--r--spec/features/merge_requests/widget_spec.rb52
-rw-r--r--spec/features/milestone_spec.rb2
-rw-r--r--spec/features/milestones/milestones_spec.rb4
-rw-r--r--spec/features/milestones/show_spec.rb2
-rw-r--r--spec/features/participants_autocomplete_spec.rb4
-rw-r--r--spec/features/profiles/account_spec.rb59
-rw-r--r--spec/features/profiles/preferences_spec.rb2
-rw-r--r--spec/features/projects/artifacts/file_spec.rb59
-rw-r--r--spec/features/projects/blobs/blob_show_spec.rb431
-rw-r--r--spec/features/projects/blobs/edit_spec.rb1
-rw-r--r--spec/features/projects/blobs/user_create_spec.rb1
-rw-r--r--spec/features/projects/branches/new_branch_ref_dropdown_spec.rb48
-rw-r--r--spec/features/projects/branches_spec.rb85
-rw-r--r--spec/features/projects/commit/cherry_pick_spec.rb7
-rw-r--r--spec/features/projects/commit/mini_pipeline_graph_spec.rb2
-rw-r--r--spec/features/projects/deploy_keys_spec.rb12
-rw-r--r--spec/features/projects/edit_spec.rb2
-rw-r--r--spec/features/projects/environments/environment_spec.rb6
-rw-r--r--spec/features/projects/features_visibility_spec.rb38
-rw-r--r--spec/features/projects/files/browse_files_spec.rb5
-rw-r--r--spec/features/projects/files/creating_a_file_spec.rb2
-rw-r--r--spec/features/projects/files/dockerfile_dropdown_spec.rb8
-rw-r--r--spec/features/projects/files/editing_a_file_spec.rb2
-rw-r--r--spec/features/projects/files/files_sort_submodules_with_folders_spec.rb2
-rw-r--r--spec/features/projects/files/find_file_keyboard_spec.rb2
-rw-r--r--spec/features/projects/files/find_files_spec.rb30
-rw-r--r--spec/features/projects/files/gitignore_dropdown_spec.rb2
-rw-r--r--spec/features/projects/files/gitlab_ci_yml_dropdown_spec.rb2
-rw-r--r--spec/features/projects/files/project_owner_creates_license_file_spec.rb2
-rw-r--r--spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb2
-rw-r--r--spec/features/projects/files/undo_template_spec.rb15
-rw-r--r--spec/features/projects/import_export/test_project_export.tar.gzbin679892 -> 681478 bytes
-rw-r--r--spec/features/projects/issuable_templates_spec.rb6
-rw-r--r--spec/features/projects/labels/update_prioritization_spec.rb1
-rw-r--r--spec/features/projects/members/group_links_spec.rb2
-rw-r--r--spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb1
-rw-r--r--spec/features/projects/members/sorting_spec.rb4
-rw-r--r--spec/features/projects/members/user_requests_access_spec.rb11
-rw-r--r--spec/features/projects/merge_request_button_spec.rb4
-rw-r--r--spec/features/projects/milestones/milestone_spec.rb23
-rw-r--r--spec/features/projects/pipeline_schedules_spec.rb141
-rw-r--r--spec/features/projects/pipelines/pipeline_spec.rb53
-rw-r--r--spec/features/projects/pipelines/pipelines_spec.rb54
-rw-r--r--spec/features/projects/project_settings_spec.rb152
-rw-r--r--spec/features/projects/ref_switcher_spec.rb1
-rw-r--r--spec/features/projects/settings/integration_settings_spec.rb94
-rw-r--r--spec/features/projects/snippets/show_spec.rb144
-rw-r--r--spec/features/projects/snippets_spec.rb24
-rw-r--r--spec/features/projects/user_create_dir_spec.rb1
-rw-r--r--spec/features/projects/view_on_env_spec.rb2
-rw-r--r--spec/features/projects/wiki/markdown_preview_spec.rb8
-rw-r--r--spec/features/projects/wiki/user_creates_wiki_page_spec.rb4
-rw-r--r--spec/features/projects_spec.rb14
-rw-r--r--spec/features/protected_branches/access_control_ce_spec.rb4
-rw-r--r--spec/features/protected_branches_spec.rb6
-rw-r--r--spec/features/protected_tags/access_control_ce_spec.rb5
-rw-r--r--spec/features/protected_tags_spec.rb5
-rw-r--r--spec/features/raven_js_spec.rb23
-rw-r--r--spec/features/search_spec.rb33
-rw-r--r--spec/features/security/project/internal_access_spec.rb69
-rw-r--r--spec/features/security/project/private_access_spec.rb91
-rw-r--r--spec/features/security/project/public_access_spec.rb69
-rw-r--r--spec/features/signup_spec.rb4
-rw-r--r--spec/features/snippets/create_snippet_spec.rb8
-rw-r--r--spec/features/snippets/explore_spec.rb25
-rw-r--r--spec/features/snippets/internal_snippet_spec.rb23
-rw-r--r--spec/features/snippets/notes_on_personal_snippets_spec.rb101
-rw-r--r--spec/features/snippets/public_snippets_spec.rb3
-rw-r--r--spec/features/snippets/show_spec.rb138
-rw-r--r--spec/features/task_lists_spec.rb38
-rw-r--r--spec/features/todos/todos_filtering_spec.rb6
-rw-r--r--spec/features/todos/todos_spec.rb85
-rw-r--r--spec/features/triggers_spec.rb53
-rw-r--r--spec/features/u2f_spec.rb2
-rw-r--r--spec/features/unsubscribe_links_spec.rb2
-rw-r--r--spec/features/user_callout_spec.rb2
-rw-r--r--spec/features/users/projects_spec.rb2
-rw-r--r--spec/features/users/snippets_spec.rb48
-rw-r--r--spec/features/users_spec.rb1
-rw-r--r--spec/features/variables_spec.rb2
-rw-r--r--spec/finders/groups_finder_spec.rb57
-rw-r--r--spec/finders/issues_finder_spec.rb36
-rw-r--r--spec/finders/merge_requests_finder_spec.rb8
-rw-r--r--spec/finders/notes_finder_spec.rb9
-rw-r--r--spec/finders/pipeline_schedules_finder_spec.rb41
-rw-r--r--spec/finders/pipelines_finder_spec.rb205
-rw-r--r--spec/finders/snippets_finder_spec.rb125
-rw-r--r--spec/fixtures/api/schemas/branch.json12
-rw-r--r--spec/fixtures/api/schemas/deployments.json58
-rw-r--r--spec/fixtures/api/schemas/entities/merge_request.json98
-rw-r--r--spec/fixtures/api/schemas/entities/merge_request_basic.json14
-rw-r--r--spec/fixtures/api/schemas/issue.json18
-rw-r--r--spec/fixtures/api/schemas/merge_request.json12
-rw-r--r--spec/fixtures/api/schemas/pipeline.json354
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/issues.json17
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/user/public.json2
-rw-r--r--spec/fixtures/emails/forwarded_new_issue.eml25
-rw-r--r--spec/fixtures/markdown.md.erb2
-rw-r--r--spec/helpers/application_helper_spec.rb52
-rw-r--r--spec/helpers/avatars_helper_spec.rb2
-rw-r--r--spec/helpers/award_emoji_helper_spec.rb61
-rw-r--r--spec/helpers/blob_helper_spec.rb137
-rw-r--r--spec/helpers/ci_status_helper_spec.rb40
-rw-r--r--spec/helpers/diff_helper_spec.rb4
-rw-r--r--spec/helpers/events_helper_spec.rb2
-rw-r--r--spec/helpers/icons_helper_spec.rb15
-rw-r--r--spec/helpers/issuables_helper_spec.rb17
-rw-r--r--spec/helpers/markup_helper_spec.rb (renamed from spec/helpers/gitlab_markdown_helper_spec.rb)35
-rw-r--r--spec/helpers/merge_requests_helper_spec.rb121
-rw-r--r--spec/helpers/notes_helper_spec.rb214
-rw-r--r--spec/helpers/projects_helper_spec.rb18
-rw-r--r--spec/helpers/submodule_helper_spec.rb12
-rw-r--r--spec/javascripts/autosave_spec.js134
-rw-r--r--spec/javascripts/behaviors/bind_in_out_spec.js12
-rw-r--r--spec/javascripts/behaviors/gl_emoji/unicode_support_map_spec.js47
-rw-r--r--spec/javascripts/blob/balsamiq/balsamiq_viewer_integration_spec.js51
-rw-r--r--spec/javascripts/blob/balsamiq/balsamiq_viewer_spec.js326
-rw-r--r--spec/javascripts/blob/blob_fork_suggestion_spec.js31
-rw-r--r--spec/javascripts/blob/create_branch_dropdown_spec.js1
-rw-r--r--spec/javascripts/blob/pdf/index_spec.js6
-rw-r--r--spec/javascripts/blob/pdf/test.pdfbin11956 -> 0 bytes
-rw-r--r--spec/javascripts/blob/target_branch_dropdown_spec.js5
-rw-r--r--spec/javascripts/blob/viewer/index_spec.js161
-rw-r--r--spec/javascripts/boards/board_card_spec.js8
-rw-r--r--spec/javascripts/boards/board_list_spec.js1
-rw-r--r--spec/javascripts/boards/boards_store_spec.js19
-rw-r--r--spec/javascripts/boards/issue_card_spec.js131
-rw-r--r--spec/javascripts/boards/issue_spec.js76
-rw-r--r--spec/javascripts/boards/list_spec.js25
-rw-r--r--spec/javascripts/boards/mock_data.js3
-rw-r--r--spec/javascripts/boards/modal_store_spec.js12
-rw-r--r--spec/javascripts/bootstrap_linked_tabs_spec.js8
-rw-r--r--spec/javascripts/ci_status_icon_spec.js44
-rw-r--r--spec/javascripts/commit/pipelines/mock_data.js89
-rw-r--r--spec/javascripts/commit/pipelines/pipelines_spec.js7
-rw-r--r--spec/javascripts/cycle_analytics/limit_warning_component_spec.js3
-rw-r--r--spec/javascripts/deploy_keys/components/action_btn_spec.js70
-rw-r--r--spec/javascripts/deploy_keys/components/app_spec.js142
-rw-r--r--spec/javascripts/deploy_keys/components/key_spec.js92
-rw-r--r--spec/javascripts/deploy_keys/components/keys_panel_spec.js70
-rw-r--r--spec/javascripts/droplab/constants_spec.js6
-rw-r--r--spec/javascripts/droplab/drop_down_spec.js57
-rw-r--r--spec/javascripts/droplab/hook_spec.js8
-rw-r--r--spec/javascripts/environments/environment_actions_spec.js12
-rw-r--r--spec/javascripts/environments/environment_rollback_spec.js24
-rw-r--r--spec/javascripts/environments/environment_spec.js5
-rw-r--r--spec/javascripts/environments/environment_stop_spec.js10
-rw-r--r--spec/javascripts/environments/folder/environments_folder_view_spec.js4
-rw-r--r--spec/javascripts/filtered_search/components/recent_searches_dropdown_content_spec.js20
-rw-r--r--spec/javascripts/filtered_search/filtered_search_manager_spec.js97
-rw-r--r--spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js117
-rw-r--r--spec/javascripts/filtered_search/recent_searches_root_spec.js31
-rw-r--r--spec/javascripts/filtered_search/services/recent_searches_service_error_spec.js18
-rw-r--r--spec/javascripts/filtered_search/services/recent_searches_service_spec.js95
-rw-r--r--spec/javascripts/fixtures/balsamiq.rb18
-rw-r--r--spec/javascripts/fixtures/balsamiq_viewer.html.haml1
-rw-r--r--spec/javascripts/fixtures/blob.rb29
-rw-r--r--spec/javascripts/fixtures/deploy_keys.rb36
-rw-r--r--spec/javascripts/fixtures/environments.rb30
-rw-r--r--spec/javascripts/fixtures/environments/metrics.html.haml62
-rw-r--r--spec/javascripts/fixtures/graph.html.haml1
-rw-r--r--spec/javascripts/fixtures/labels.rb56
-rw-r--r--spec/javascripts/fixtures/line_highlighter.html.haml2
-rw-r--r--spec/javascripts/fixtures/merge_requests.rb16
-rw-r--r--spec/javascripts/fixtures/mini_dropdown_graph.html.haml6
-rw-r--r--spec/javascripts/fixtures/pdf.rb18
-rw-r--r--spec/javascripts/fixtures/pipelines.rb35
-rw-r--r--spec/javascripts/fixtures/raw.rb24
-rw-r--r--spec/javascripts/gl_dropdown_spec.js47
-rw-r--r--spec/javascripts/gl_form_spec.js6
-rw-r--r--spec/javascripts/helpers/class_spec_helper_spec.js2
-rw-r--r--spec/javascripts/helpers/filtered_search_spec_helper.js7
-rw-r--r--spec/javascripts/helpers/user_mock_data_helper.js16
-rw-r--r--spec/javascripts/issuable_time_tracker_spec.js250
-rw-r--r--spec/javascripts/issue_show/components/app_spec.js60
-rw-r--r--spec/javascripts/issue_show/components/description_spec.js99
-rw-r--r--spec/javascripts/issue_show/components/title_spec.js67
-rw-r--r--spec/javascripts/issue_show/issue_title_spec.js22
-rw-r--r--spec/javascripts/issue_show/mock_data.js26
-rw-r--r--spec/javascripts/issue_spec.js10
-rw-r--r--spec/javascripts/labels_issue_sidebar_spec.js1
-rw-r--r--spec/javascripts/landing_spec.js160
-rw-r--r--spec/javascripts/lib/utils/accessor_spec.js78
-rw-r--r--spec/javascripts/lib/utils/ajax_cache_spec.js158
-rw-r--r--spec/javascripts/lib/utils/common_utils_spec.js11
-rw-r--r--spec/javascripts/lib/utils/poll_spec.js93
-rw-r--r--spec/javascripts/line_highlighter_spec.js8
-rw-r--r--spec/javascripts/merge_request_notes_spec.js61
-rw-r--r--spec/javascripts/merge_request_tabs_spec.js10
-rw-r--r--spec/javascripts/merge_request_widget_spec.js199
-rw-r--r--spec/javascripts/merged_buttons_spec.js44
-rw-r--r--spec/javascripts/mini_pipeline_graph_dropdown_spec.js106
-rw-r--r--spec/javascripts/monitoring/deployments_spec.js133
-rw-r--r--spec/javascripts/monitoring/prometheus_graph_spec.js4
-rw-r--r--spec/javascripts/notebook/cells/code_spec.js55
-rw-r--r--spec/javascripts/notebook/cells/markdown_spec.js41
-rw-r--r--spec/javascripts/notebook/cells/output/index_spec.js126
-rw-r--r--spec/javascripts/notebook/cells/prompt_spec.js56
-rw-r--r--spec/javascripts/notebook/index_spec.js98
-rw-r--r--spec/javascripts/notebook/lib/highlight_spec.js15
-rw-r--r--spec/javascripts/notes_spec.js332
-rw-r--r--spec/javascripts/pdf/index_spec.js61
-rw-r--r--spec/javascripts/pdf/page_spec.js57
-rw-r--r--spec/javascripts/pipeline_schedules/interval_pattern_input_spec.js217
-rw-r--r--spec/javascripts/pipeline_schedules/pipeline_schedule_callout_spec.js106
-rw-r--r--spec/javascripts/pipelines/graph/action_component_spec.js40
-rw-r--r--spec/javascripts/pipelines/graph/dropdown_action_component_spec.js30
-rw-r--r--spec/javascripts/pipelines/graph/graph_component_spec.js62
-rw-r--r--spec/javascripts/pipelines/graph/job_component_spec.js117
-rw-r--r--spec/javascripts/pipelines/graph/job_name_component_spec.js27
-rw-r--r--spec/javascripts/pipelines/graph/mock_data.js232
-rw-r--r--spec/javascripts/pipelines/graph/stage_column_component_spec.js42
-rw-r--r--spec/javascripts/pipelines/mock_data.js107
-rw-r--r--spec/javascripts/pipelines/pipelines_spec.js9
-rw-r--r--spec/javascripts/pipelines/stage_spec.js108
-rw-r--r--spec/javascripts/pipelines/time_ago_spec.js64
-rw-r--r--spec/javascripts/pipelines_spec.js32
-rw-r--r--spec/javascripts/project_title_spec.js1
-rw-r--r--spec/javascripts/raven/index_spec.js42
-rw-r--r--spec/javascripts/raven/raven_config_spec.js276
-rw-r--r--spec/javascripts/search_autocomplete_spec.js1
-rw-r--r--spec/javascripts/shortcuts_issuable_spec.js2
-rw-r--r--spec/javascripts/sidebar/assignee_title_spec.js80
-rw-r--r--spec/javascripts/sidebar/assignees_spec.js272
-rw-r--r--spec/javascripts/sidebar/mock_data.js109
-rw-r--r--spec/javascripts/sidebar/sidebar_assignees_spec.js46
-rw-r--r--spec/javascripts/sidebar/sidebar_mediator_spec.js41
-rw-r--r--spec/javascripts/sidebar/sidebar_service_spec.js33
-rw-r--r--spec/javascripts/sidebar/sidebar_store_spec.js80
-rw-r--r--spec/javascripts/signin_tabs_memoizer_spec.js90
-rw-r--r--spec/javascripts/subbable_resource_spec.js63
-rw-r--r--spec/javascripts/test_bundle.js5
-rw-r--r--spec/javascripts/u2f/mock_u2f_device.js6
-rw-r--r--spec/javascripts/version_check_image_spec.js2
-rw-r--r--spec/javascripts/visibility_select_spec.js6
-rw-r--r--spec/javascripts/vue_mr_widget/components/mr_widget_author_spec.js39
-rw-r--r--spec/javascripts/vue_mr_widget/components/mr_widget_author_time_spec.js61
-rw-r--r--spec/javascripts/vue_mr_widget/components/mr_widget_deployment_spec.js188
-rw-r--r--spec/javascripts/vue_mr_widget/components/mr_widget_header_spec.js102
-rw-r--r--spec/javascripts/vue_mr_widget/components/mr_widget_memory_usage_spec.js184
-rw-r--r--spec/javascripts/vue_mr_widget/components/mr_widget_merge_help_spec.js51
-rw-r--r--spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js131
-rw-r--r--spec/javascripts/vue_mr_widget/components/mr_widget_related_links_spec.js138
-rw-r--r--spec/javascripts/vue_mr_widget/components/states/mr_widget_archived_spec.js18
-rw-r--r--spec/javascripts/vue_mr_widget/components/states/mr_widget_auto_merge_failed_spec.js32
-rw-r--r--spec/javascripts/vue_mr_widget/components/states/mr_widget_checking_spec.js19
-rw-r--r--spec/javascripts/vue_mr_widget/components/states/mr_widget_closed_spec.js51
-rw-r--r--spec/javascripts/vue_mr_widget/components/states/mr_widget_conflicts_spec.js69
-rw-r--r--spec/javascripts/vue_mr_widget/components/states/mr_widget_failed_to_merge_spec.js122
-rw-r--r--spec/javascripts/vue_mr_widget/components/states/mr_widget_locked_spec.js33
-rw-r--r--spec/javascripts/vue_mr_widget/components/states/mr_widget_merge_when_pipeline_succeeds_spec.js213
-rw-r--r--spec/javascripts/vue_mr_widget/components/states/mr_widget_merged_spec.js174
-rw-r--r--spec/javascripts/vue_mr_widget/components/states/mr_widget_missing_branch_spec.js55
-rw-r--r--spec/javascripts/vue_mr_widget/components/states/mr_widget_not_allowed_spec.js17
-rw-r--r--spec/javascripts/vue_mr_widget/components/states/mr_widget_nothing_to_merge_spec.js17
-rw-r--r--spec/javascripts/vue_mr_widget/components/states/mr_widget_pipeline_blocked_spec.js16
-rw-r--r--spec/javascripts/vue_mr_widget/components/states/mr_widget_pipeline_failed_spec.js16
-rw-r--r--spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js389
-rw-r--r--spec/javascripts/vue_mr_widget/components/states/mr_widget_sha_mismatch_spec.js16
-rw-r--r--spec/javascripts/vue_mr_widget/components/states/mr_widget_unresolved_discussions_spec.js47
-rw-r--r--spec/javascripts/vue_mr_widget/components/states/mr_widget_wip_spec.js96
-rw-r--r--spec/javascripts/vue_mr_widget/mock_data.js214
-rw-r--r--spec/javascripts/vue_mr_widget/mr_widget_options_spec.js326
-rw-r--r--spec/javascripts/vue_mr_widget/services/mr_widget_service_spec.js46
-rw-r--r--spec/javascripts/vue_mr_widget/stores/get_state_key_spec.js65
-rw-r--r--spec/javascripts/vue_mr_widget/stores/mr_widget_store_spec.js22
-rw-r--r--spec/javascripts/vue_shared/ci_action_icons_spec.js27
-rw-r--r--spec/javascripts/vue_shared/ci_status_icon_spec.js27
-rw-r--r--spec/javascripts/vue_shared/components/ci_badge_link_spec.js89
-rw-r--r--spec/javascripts/vue_shared/components/ci_icon_spec.js139
-rw-r--r--spec/javascripts/vue_shared/components/commit_spec.js8
-rw-r--r--spec/javascripts/vue_shared/components/loading_icon_spec.js53
-rw-r--r--spec/javascripts/vue_shared/components/memory_graph_spec.js143
-rw-r--r--spec/javascripts/vue_shared/components/mock_data.js69
-rw-r--r--spec/javascripts/vue_shared/components/pipelines_table_row_spec.js92
-rw-r--r--spec/javascripts/vue_shared/components/pipelines_table_spec.js8
-rw-r--r--spec/javascripts/vue_shared/components/table_pagination_spec.js2
-rw-r--r--spec/javascripts/vue_shared/translate_spec.js90
-rw-r--r--spec/lib/banzai/filter/emoji_filter_spec.rb10
-rw-r--r--spec/lib/banzai/filter/external_link_filter_spec.rb85
-rw-r--r--spec/lib/banzai/filter/issuable_state_filter_spec.rb77
-rw-r--r--spec/lib/banzai/filter/redactor_filter_spec.rb2
-rw-r--r--spec/lib/banzai/renderer_spec.rb2
-rw-r--r--spec/lib/ci/gitlab_ci_yaml_processor_spec.rb12
-rw-r--r--spec/lib/constraints/group_url_constrainer_spec.rb39
-rw-r--r--spec/lib/constraints/project_url_constrainer_spec.rb21
-rw-r--r--spec/lib/constraints/user_url_constrainer_spec.rb21
-rw-r--r--spec/lib/expand_variables_spec.rb6
-rw-r--r--spec/lib/gitlab/asciidoc_spec.rb24
-rw-r--r--spec/lib/gitlab/auth_spec.rb6
-rw-r--r--spec/lib/gitlab/backup/manager_spec.rb2
-rw-r--r--spec/lib/gitlab/cache/ci/project_pipeline_status_spec.rb142
-rw-r--r--spec/lib/gitlab/changes_list_spec.rb2
-rw-r--r--spec/lib/gitlab/chat_commands/command_spec.rb14
-rw-r--r--spec/lib/gitlab/chat_commands/deploy_spec.rb16
-rw-r--r--spec/lib/gitlab/checks/change_access_spec.rb87
-rw-r--r--spec/lib/gitlab/checks/force_push_spec.rb6
-rw-r--r--spec/lib/gitlab/ci/build/artifacts/metadata/entry_spec.rb11
-rw-r--r--spec/lib/gitlab/ci/build/credentials/factory_spec.rb4
-rw-r--r--spec/lib/gitlab/ci/build/credentials/registry_spec.rb6
-rw-r--r--spec/lib/gitlab/ci/config/entry/global_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/cron_parser_spec.rb88
-rw-r--r--spec/lib/gitlab/ci/status/build/action_spec.rb56
-rw-r--r--spec/lib/gitlab/ci/status/build/factory_spec.rb51
-rw-r--r--spec/lib/gitlab/ci/status/build/play_spec.rb45
-rw-r--r--spec/lib/gitlab/ci/status/extended_spec.rb6
-rw-r--r--spec/lib/gitlab/ci/status/group/common_spec.rb20
-rw-r--r--spec/lib/gitlab/ci/status/group/factory_spec.rb13
-rw-r--r--spec/lib/gitlab/conflict/file_collection_spec.rb2
-rw-r--r--spec/lib/gitlab/contributions_calendar_spec.rb2
-rw-r--r--spec/lib/gitlab/current_settings_spec.rb4
-rw-r--r--spec/lib/gitlab/cycle_analytics/base_event_fetcher_spec.rb2
-rw-r--r--spec/lib/gitlab/cycle_analytics/events_spec.rb8
-rw-r--r--spec/lib/gitlab/data_builder/push_spec.rb1
-rw-r--r--spec/lib/gitlab/database/migration_helpers_spec.rb41
-rw-r--r--spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base_spec.rb197
-rw-r--r--spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces_spec.rb202
-rw-r--r--spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects_spec.rb102
-rw-r--r--spec/lib/gitlab/database/rename_reserved_paths_migration/v1_spec.rb54
-rw-r--r--spec/lib/gitlab/dependency_linker/gemfile_linker_spec.rb60
-rw-r--r--spec/lib/gitlab/dependency_linker_spec.rb13
-rw-r--r--spec/lib/gitlab/diff/highlight_spec.rb4
-rw-r--r--spec/lib/gitlab/diff/inline_diff_markdown_marker_spec.rb14
-rw-r--r--spec/lib/gitlab/diff/inline_diff_marker_spec.rb18
-rw-r--r--spec/lib/gitlab/diff/position_tracer_spec.rb17
-rw-r--r--spec/lib/gitlab/email/receiver_spec.rb28
-rw-r--r--spec/lib/gitlab/file_finder_spec.rb21
-rw-r--r--spec/lib/gitlab/git/diff_spec.rb2
-rw-r--r--spec/lib/gitlab/git/encoding_helper_spec.rb16
-rw-r--r--spec/lib/gitlab/git/repository_spec.rb148
-rw-r--r--spec/lib/gitlab/git/util_spec.rb4
-rw-r--r--spec/lib/gitlab/git_access_spec.rb2
-rw-r--r--spec/lib/gitlab/git_ref_validator_spec.rb (renamed from spec/lib/git_ref_validator_spec.rb)0
-rw-r--r--spec/lib/gitlab/gitaly_client/commit_spec.rb65
-rw-r--r--spec/lib/gitlab/gitaly_client/ref_spec.rb2
-rw-r--r--spec/lib/gitlab/github_import/issue_formatter_spec.rb10
-rw-r--r--spec/lib/gitlab/gl_repository_spec.rb19
-rw-r--r--spec/lib/gitlab/google_code_import/importer_spec.rb2
-rw-r--r--spec/lib/gitlab/health_checks/db_check_spec.rb (renamed from spec/lib/gitlab/healthchecks/db_check_spec.rb)0
-rw-r--r--spec/lib/gitlab/health_checks/fs_shards_check_spec.rb (renamed from spec/lib/gitlab/healthchecks/fs_shards_check_spec.rb)12
-rw-r--r--spec/lib/gitlab/health_checks/redis_check_spec.rb (renamed from spec/lib/gitlab/healthchecks/redis_check_spec.rb)0
-rw-r--r--spec/lib/gitlab/health_checks/simple_check_shared.rb (renamed from spec/lib/gitlab/healthchecks/simple_check_shared.rb)6
-rw-r--r--spec/lib/gitlab/highlight_spec.rb11
-rw-r--r--spec/lib/gitlab/i18n_spec.rb27
-rw-r--r--spec/lib/gitlab/import_export/all_models.yml22
-rw-r--r--spec/lib/gitlab/import_export/fork_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/project.json42
-rw-r--r--spec/lib/gitlab/import_export/project_tree_restorer_spec.rb4
-rw-r--r--spec/lib/gitlab/import_export/project_tree_saver_spec.rb15
-rw-r--r--spec/lib/gitlab/import_export/reader_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/relation_factory_spec.rb4
-rw-r--r--spec/lib/gitlab/import_export/safe_model_attributes.yml46
-rw-r--r--spec/lib/gitlab/import_export/wiki_repo_saver_spec.rb (renamed from spec/lib/gitlab/import_export/wiki_repo_bundler_spec.rb)0
-rw-r--r--spec/lib/gitlab/issuable_sorter_spec.rb62
-rw-r--r--spec/lib/gitlab/ldap/person_spec.rb6
-rw-r--r--spec/lib/gitlab/ldap/user_spec.rb25
-rw-r--r--spec/lib/gitlab/metrics_spec.rb28
-rw-r--r--spec/lib/gitlab/o_auth/user_spec.rb29
-rw-r--r--spec/lib/gitlab/other_markup_spec.rb (renamed from spec/lib/gitlab/other_markup.rb)6
-rw-r--r--spec/lib/gitlab/project_search_results_spec.rb79
-rw-r--r--spec/lib/gitlab/prometheus/queries/deployment_query_spec.rb36
-rw-r--r--spec/lib/gitlab/prometheus_client_spec.rb (renamed from spec/lib/gitlab/prometheus_spec.rb)52
-rw-r--r--spec/lib/gitlab/regex_spec.rb4
-rw-r--r--spec/lib/gitlab/repo_path_spec.rb26
-rw-r--r--spec/lib/gitlab/request_profiler_spec.rb27
-rw-r--r--spec/lib/gitlab/saml/user_spec.rb25
-rw-r--r--spec/lib/gitlab/search_results_spec.rb4
-rw-r--r--spec/lib/gitlab/shell_spec.rb (renamed from spec/lib/gitlab/backend/shell_spec.rb)41
-rw-r--r--spec/lib/gitlab/sidekiq_throttler_spec.rb4
-rw-r--r--spec/lib/gitlab/slash_commands/command_definition_spec.rb52
-rw-r--r--spec/lib/gitlab/slash_commands/dsl_spec.rb68
-rw-r--r--spec/lib/gitlab/string_range_marker_spec.rb36
-rw-r--r--spec/lib/gitlab/string_regex_marker_spec.rb18
-rw-r--r--spec/lib/gitlab/template/gitignore_template_spec.rb2
-rw-r--r--spec/lib/gitlab/template/gitlab_ci_yml_template_spec.rb2
-rw-r--r--spec/lib/gitlab/template/issue_template_spec.rb2
-rw-r--r--spec/lib/gitlab/template/merge_request_template_spec.rb2
-rw-r--r--spec/lib/gitlab/usage_data_spec.rb6
-rw-r--r--spec/lib/gitlab/user_access_spec.rb52
-rw-r--r--spec/lib/gitlab/workhorse_spec.rb21
-rw-r--r--spec/lib/light_url_builder_spec.rb119
-rw-r--r--spec/mailers/emails/merge_requests_spec.rb2
-rw-r--r--spec/mailers/emails/profile_spec.rb146
-rw-r--r--spec/mailers/notify_spec.rb86
-rw-r--r--spec/migrations/active_record/schema_spec.rb (renamed from spec/migrations/schema_spec.rb)0
-rw-r--r--spec/migrations/add_head_pipeline_for_each_merge_request_spec.rb33
-rw-r--r--spec/migrations/cleanup_namespaceless_pending_delete_projects_spec.rb32
-rw-r--r--spec/models/application_setting_spec.rb65
-rw-r--r--spec/models/blob_spec.rb370
-rw-r--r--spec/models/blob_viewer/base_spec.rb190
-rw-r--r--spec/models/blob_viewer/gitlab_ci_yml_spec.rb32
-rw-r--r--spec/models/blob_viewer/license_spec.rb34
-rw-r--r--spec/models/blob_viewer/route_map_spec.rb38
-rw-r--r--spec/models/blob_viewer/server_side_spec.rb25
-rw-r--r--spec/models/ci/artifact_blob_spec.rb44
-rw-r--r--spec/models/ci/build_spec.rb46
-rw-r--r--spec/models/ci/group_spec.rb44
-rw-r--r--spec/models/ci/pipeline_schedule_spec.rb112
-rw-r--r--spec/models/ci/pipeline_spec.rb128
-rw-r--r--spec/models/ci/stage_spec.rb37
-rw-r--r--spec/models/ci/trigger_schedule_spec.rb76
-rw-r--r--spec/models/ci/trigger_spec.rb1
-rw-r--r--spec/models/ci/variable_spec.rb2
-rw-r--r--spec/models/commit_spec.rb35
-rw-r--r--spec/models/commit_status_spec.rb24
-rw-r--r--spec/models/concerns/awardable_spec.rb4
-rw-r--r--spec/models/concerns/cache_markdown_field_spec.rb10
-rw-r--r--spec/models/concerns/discussion_on_diff_spec.rb6
-rw-r--r--spec/models/concerns/issuable_spec.rb144
-rw-r--r--spec/models/concerns/mentionable_spec.rb49
-rw-r--r--spec/models/concerns/milestoneish_spec.rb6
-rw-r--r--spec/models/concerns/noteable_spec.rb2
-rw-r--r--spec/models/concerns/relative_positioning_spec.rb2
-rw-r--r--spec/models/concerns/routable_spec.rb70
-rw-r--r--spec/models/concerns/spammable_spec.rb4
-rw-r--r--spec/models/concerns/strip_attribute_spec.rb2
-rw-r--r--spec/models/cycle_analytics/test_spec.rb1
-rw-r--r--spec/models/deployment_spec.rb28
-rw-r--r--spec/models/diff_discussion_spec.rb83
-rw-r--r--spec/models/diff_note_spec.rb52
-rw-r--r--spec/models/environment_spec.rb57
-rw-r--r--spec/models/event_spec.rb46
-rw-r--r--spec/models/global_milestone_spec.rb2
-rw-r--r--spec/models/group_spec.rb46
-rw-r--r--spec/models/hooks/system_hook_spec.rb21
-rw-r--r--spec/models/issue_collection_spec.rb2
-rw-r--r--spec/models/issue_spec.rb72
-rw-r--r--spec/models/legacy_diff_discussion_spec.rb22
-rw-r--r--spec/models/member_spec.rb6
-rw-r--r--spec/models/merge_request_spec.rb180
-rw-r--r--spec/models/namespace_spec.rb8
-rw-r--r--spec/models/network/graph_spec.rb36
-rw-r--r--spec/models/note_spec.rb86
-rw-r--r--spec/models/project_authorization_spec.rb2
-rw-r--r--spec/models/project_services/asana_service_spec.rb2
-rw-r--r--spec/models/project_services/chat_message/issue_message_spec.rb2
-rw-r--r--spec/models/project_services/chat_message/merge_message_spec.rb2
-rw-r--r--spec/models/project_services/chat_message/note_message_spec.rb2
-rw-r--r--spec/models/project_services/chat_message/pipeline_message_spec.rb25
-rw-r--r--spec/models/project_services/chat_message/push_message_spec.rb20
-rw-r--r--spec/models/project_services/chat_message/wiki_page_message_spec.rb4
-rw-r--r--spec/models/project_services/chat_notification_service_spec.rb4
-rw-r--r--spec/models/project_services/issue_tracker_service_spec.rb2
-rw-r--r--spec/models/project_services/kubernetes_service_spec.rb6
-rw-r--r--spec/models/project_services/pipelines_email_service_spec.rb (renamed from spec/models/project_services/pipeline_email_service_spec.rb)0
-rw-r--r--spec/models/project_services/pivotaltracker_service_spec.rb2
-rw-r--r--spec/models/project_services/prometheus_service_spec.rb35
-rw-r--r--spec/models/project_spec.rb82
-rw-r--r--spec/models/project_statistics_spec.rb4
-rw-r--r--spec/models/project_wiki_spec.rb21
-rw-r--r--spec/models/protected_branch/merge_access_level_spec.rb5
-rw-r--r--spec/models/protected_branch/push_access_level_spec.rb5
-rw-r--r--spec/models/redirect_route_spec.rb27
-rw-r--r--spec/models/repository_spec.rb132
-rw-r--r--spec/models/route_spec.rb114
-rw-r--r--spec/models/sent_notification_spec.rb8
-rw-r--r--spec/models/service_spec.rb55
-rw-r--r--spec/models/snippet_blob_spec.rb47
-rw-r--r--spec/models/snippet_spec.rb53
-rw-r--r--spec/models/todo_spec.rb46
-rw-r--r--spec/models/user_spec.rb138
-rw-r--r--spec/policies/ci/build_policy_spec.rb53
-rw-r--r--spec/policies/environment_policy_spec.rb57
-rw-r--r--spec/policies/issue_policy_spec.rb246
-rw-r--r--spec/policies/issues_policy_spec.rb193
-rw-r--r--spec/policies/personal_snippet_policy_spec.rb141
-rw-r--r--spec/policies/project_policy_spec.rb2
-rw-r--r--spec/policies/project_snippet_policy_spec.rb80
-rw-r--r--spec/presenters/merge_request_presenter_spec.rb356
-rw-r--r--spec/requests/api/access_requests_spec.rb4
-rw-r--r--spec/requests/api/api_internal_helpers_spec.rb32
-rw-r--r--spec/requests/api/award_emoji_spec.rb3
-rw-r--r--spec/requests/api/boards_spec.rb4
-rw-r--r--spec/requests/api/branches_spec.rb17
-rw-r--r--spec/requests/api/broadcast_messages_spec.rb4
-rw-r--r--spec/requests/api/commit_statuses_spec.rb8
-rw-r--r--spec/requests/api/commits_spec.rb3
-rw-r--r--spec/requests/api/deploy_keys_spec.rb4
-rw-r--r--spec/requests/api/deployments_spec.rb4
-rw-r--r--spec/requests/api/doorkeeper_access_spec.rb4
-rw-r--r--spec/requests/api/environments_spec.rb4
-rw-r--r--spec/requests/api/files_spec.rb5
-rw-r--r--spec/requests/api/groups_spec.rb7
-rw-r--r--spec/requests/api/helpers_spec.rb32
-rw-r--r--spec/requests/api/internal_spec.rb80
-rw-r--r--spec/requests/api/issues_spec.rb179
-rw-r--r--spec/requests/api/jobs_spec.rb67
-rw-r--r--spec/requests/api/keys_spec.rb10
-rw-r--r--spec/requests/api/labels_spec.rb4
-rw-r--r--spec/requests/api/lint_spec.rb4
-rw-r--r--spec/requests/api/members_spec.rb4
-rw-r--r--spec/requests/api/merge_request_diffs_spec.rb4
-rw-r--r--spec/requests/api/merge_requests_spec.rb103
-rw-r--r--spec/requests/api/milestones_spec.rb3
-rw-r--r--spec/requests/api/namespaces_spec.rb3
-rw-r--r--spec/requests/api/notes_spec.rb3
-rw-r--r--spec/requests/api/notification_settings_spec.rb4
-rw-r--r--spec/requests/api/oauth_tokens_spec.rb4
-rw-r--r--spec/requests/api/pipelines_spec.rb243
-rw-r--r--spec/requests/api/project_hooks_spec.rb3
-rw-r--r--spec/requests/api/project_snippets_spec.rb4
-rw-r--r--spec/requests/api/projects_spec.rb18
-rw-r--r--spec/requests/api/repositories_spec.rb3
-rw-r--r--spec/requests/api/runner_spec.rb1
-rw-r--r--spec/requests/api/runners_spec.rb4
-rw-r--r--spec/requests/api/services_spec.rb4
-rw-r--r--spec/requests/api/session_spec.rb4
-rw-r--r--spec/requests/api/settings_spec.rb4
-rw-r--r--spec/requests/api/sidekiq_metrics_spec.rb4
-rw-r--r--spec/requests/api/snippets_spec.rb3
-rw-r--r--spec/requests/api/system_hooks_spec.rb7
-rw-r--r--spec/requests/api/tags_spec.rb3
-rw-r--r--spec/requests/api/templates_spec.rb4
-rw-r--r--spec/requests/api/todos_spec.rb4
-rw-r--r--spec/requests/api/triggers_spec.rb2
-rw-r--r--spec/requests/api/users_spec.rb14
-rw-r--r--spec/requests/api/v3/award_emoji_spec.rb4
-rw-r--r--spec/requests/api/v3/boards_spec.rb4
-rw-r--r--spec/requests/api/v3/branches_spec.rb17
-rw-r--r--spec/requests/api/v3/broadcast_messages_spec.rb4
-rw-r--r--spec/requests/api/v3/builds_spec.rb4
-rw-r--r--spec/requests/api/v3/commits_spec.rb3
-rw-r--r--spec/requests/api/v3/deploy_keys_spec.rb4
-rw-r--r--spec/requests/api/v3/deployments_spec.rb14
-rw-r--r--spec/requests/api/v3/environments_spec.rb4
-rw-r--r--spec/requests/api/v3/files_spec.rb8
-rw-r--r--spec/requests/api/v3/groups_spec.rb7
-rw-r--r--spec/requests/api/v3/issues_spec.rb34
-rw-r--r--spec/requests/api/v3/labels_spec.rb4
-rw-r--r--spec/requests/api/v3/members_spec.rb4
-rw-r--r--spec/requests/api/v3/merge_request_diffs_spec.rb4
-rw-r--r--spec/requests/api/v3/merge_requests_spec.rb16
-rw-r--r--spec/requests/api/v3/milestones_spec.rb3
-rw-r--r--spec/requests/api/v3/notes_spec.rb4
-rw-r--r--spec/requests/api/v3/pipelines_spec.rb4
-rw-r--r--spec/requests/api/v3/project_hooks_spec.rb3
-rw-r--r--spec/requests/api/v3/project_snippets_spec.rb4
-rw-r--r--spec/requests/api/v3/projects_spec.rb7
-rw-r--r--spec/requests/api/v3/repositories_spec.rb3
-rw-r--r--spec/requests/api/v3/runners_spec.rb4
-rw-r--r--spec/requests/api/v3/services_spec.rb4
-rw-r--r--spec/requests/api/v3/settings_spec.rb4
-rw-r--r--spec/requests/api/v3/snippets_spec.rb3
-rw-r--r--spec/requests/api/v3/system_hooks_spec.rb7
-rw-r--r--spec/requests/api/v3/tags_spec.rb3
-rw-r--r--spec/requests/api/v3/templates_spec.rb4
-rw-r--r--spec/requests/api/v3/todos_spec.rb4
-rw-r--r--spec/requests/api/v3/triggers_spec.rb2
-rw-r--r--spec/requests/api/v3/users_spec.rb10
-rw-r--r--spec/requests/api/variables_spec.rb4
-rw-r--r--spec/requests/api/version_spec.rb4
-rw-r--r--spec/requests/ci/api/builds_spec.rb4
-rw-r--r--spec/requests/ci/api/runners_spec.rb1
-rw-r--r--spec/requests/ci/api/triggers_spec.rb2
-rw-r--r--spec/requests/git_http_spec.rb4
-rw-r--r--spec/requests/lfs_http_spec.rb6
-rw-r--r--spec/requests/openid_connect_spec.rb8
-rw-r--r--spec/requests/projects/artifacts_controller_spec.rb117
-rw-r--r--spec/requests/projects/cycle_analytics_events_spec.rb9
-rw-r--r--spec/requests/request_profiler_spec.rb44
-rw-r--r--spec/routing/admin_routing_spec.rb14
-rw-r--r--spec/routing/environments_spec.rb2
-rw-r--r--spec/routing/notifications_routing_spec.rb14
-rw-r--r--spec/routing/project_routing_spec.rb12
-rw-r--r--spec/rubocop/cop/migration/add_column_with_default_to_large_table_spec.rb44
-rw-r--r--spec/rubocop/cop/migration/reversible_add_column_with_default_spec.rb (renamed from spec/rubocop/cop/migration/add_column_with_default_spec.rb)4
-rw-r--r--spec/serializers/analytics_issue_entity_spec.rb (renamed from spec/serializers/analytics_generic_entity_spec.rb)2
-rw-r--r--spec/serializers/analytics_issue_serializer_spec.rb2
-rw-r--r--spec/serializers/build_action_entity_spec.rb3
-rw-r--r--spec/serializers/build_entity_spec.rb30
-rw-r--r--spec/serializers/build_serializer_spec.rb2
-rw-r--r--spec/serializers/deploy_key_entity_spec.rb38
-rw-r--r--spec/serializers/deployment_entity_spec.rb18
-rw-r--r--spec/serializers/environment_serializer_spec.rb2
-rw-r--r--spec/serializers/event_entity_spec.rb13
-rw-r--r--spec/serializers/label_serializer_spec.rb46
-rw-r--r--spec/serializers/merge_request_basic_serializer_spec.rb12
-rw-r--r--spec/serializers/merge_request_entity_spec.rb128
-rw-r--r--spec/serializers/merge_request_serializer_spec.rb37
-rw-r--r--spec/serializers/pipeline_entity_spec.rb4
-rw-r--r--spec/serializers/pipeline_serializer_spec.rb6
-rw-r--r--spec/serializers/stage_entity_spec.rb10
-rw-r--r--spec/serializers/status_entity_spec.rb6
-rw-r--r--spec/services/ci/create_pipeline_service_spec.rb36
-rw-r--r--spec/services/ci/expire_pipeline_cache_service_spec.rb27
-rw-r--r--spec/services/ci/play_build_service_spec.rb105
-rw-r--r--spec/services/ci/process_pipeline_service_spec.rb44
-rw-r--r--spec/services/ci/retry_build_service_spec.rb11
-rw-r--r--spec/services/ci/retry_pipeline_service_spec.rb46
-rw-r--r--spec/services/ci/stop_environments_service_spec.rb16
-rw-r--r--spec/services/cohorts_service_spec.rb2
-rw-r--r--spec/services/create_deployment_service_spec.rb2
-rw-r--r--spec/services/delete_merged_branches_service_spec.rb31
-rw-r--r--spec/services/git_push_service_spec.rb13
-rw-r--r--spec/services/issuable/bulk_update_service_spec.rb66
-rw-r--r--spec/services/issues/build_service_spec.rb2
-rw-r--r--spec/services/issues/close_service_spec.rb2
-rw-r--r--spec/services/issues/create_service_spec.rb102
-rw-r--r--spec/services/issues/resolve_discussions_spec.rb26
-rw-r--r--spec/services/issues/update_service_spec.rb77
-rw-r--r--spec/services/members/authorized_destroy_service_spec.rb6
-rw-r--r--spec/services/merge_requests/assign_issues_service_spec.rb10
-rw-r--r--spec/services/merge_requests/build_service_spec.rb10
-rw-r--r--spec/services/merge_requests/conflicts/list_service_spec.rb73
-rw-r--r--spec/services/merge_requests/conflicts/resolve_service_spec.rb (renamed from spec/services/merge_requests/resolve_service_spec.rb)41
-rw-r--r--spec/services/merge_requests/create_from_issue_service_spec.rb74
-rw-r--r--spec/services/merge_requests/create_service_spec.rb102
-rw-r--r--spec/services/merge_requests/get_urls_service_spec.rb4
-rw-r--r--spec/services/merge_requests/merge_request_diff_cache_service_spec.rb2
-rw-r--r--spec/services/merge_requests/merge_when_pipeline_succeeds_service_spec.rb6
-rw-r--r--spec/services/merge_requests/refresh_service_spec.rb2
-rw-r--r--spec/services/merge_requests/resolved_discussion_notification_service_spec.rb (renamed from spec/services/merge_requests/resolved_discussion_notification_service.rb)0
-rw-r--r--spec/services/merge_requests/update_service_spec.rb68
-rw-r--r--spec/services/notes/build_service_spec.rb74
-rw-r--r--spec/services/notes/slash_commands_service_spec.rb31
-rw-r--r--spec/services/notification_service_spec.rb82
-rw-r--r--spec/services/preview_markdown_service_spec.rb67
-rw-r--r--spec/services/projects/autocomplete_service_spec.rb2
-rw-r--r--spec/services/projects/create_service_spec.rb16
-rw-r--r--spec/services/projects/enable_deploy_key_service_spec.rb10
-rw-r--r--spec/services/projects/housekeeping_service_spec.rb2
-rw-r--r--spec/services/projects/participants_service_spec.rb5
-rw-r--r--spec/services/projects/propagate_service_template_spec.rb103
-rw-r--r--spec/services/slash_commands/interpret_service_spec.rb375
-rw-r--r--spec/services/system_note_service_spec.rb63
-rw-r--r--spec/services/todo_service_spec.rb26
-rw-r--r--spec/services/upload_service_spec.rb (renamed from spec/services/projects/upload_service_spec.rb)4
-rw-r--r--spec/services/users/destroy_service_spec.rb4
-rw-r--r--spec/services/users/migrate_to_ghost_user_service_spec.rb18
-rw-r--r--spec/sidekiq/cron/job_gem_dependency_spec.rb18
-rw-r--r--spec/spec_helper.rb5
-rw-r--r--spec/support/cycle_analytics_helpers.rb4
-rw-r--r--spec/support/fake_migration_classes.rb3
-rw-r--r--spec/support/features/issuable_slash_commands_shared_examples.rb25
-rw-r--r--spec/support/helpers/fake_blob_helpers.rb40
-rw-r--r--spec/support/import_export/export_file_helper.rb2
-rw-r--r--spec/support/import_export/import_export.yml8
-rw-r--r--spec/support/kubernetes_helpers.rb8
-rw-r--r--spec/support/login_helpers.rb4
-rw-r--r--spec/support/markdown_feature.rb4
-rw-r--r--spec/support/matchers/access_matchers.rb4
-rw-r--r--spec/support/matchers/gitlab_git_matchers.rb6
-rw-r--r--spec/support/milestone_tabs_examples.rb68
-rw-r--r--spec/support/prometheus_helpers.rb38
-rw-r--r--spec/support/repo_helpers.rb4
-rw-r--r--spec/support/services/issuable_create_service_shared_examples.rb52
-rw-r--r--spec/support/services/issuable_create_service_slash_commands_shared_examples.rb18
-rw-r--r--spec/support/services/issuable_update_service_shared_examples.rb48
-rw-r--r--spec/support/services/migrate_to_ghost_user_service_shared_examples.rb52
-rw-r--r--spec/support/slack_mattermost_notifications_shared_examples.rb14
-rw-r--r--spec/support/test_env.rb12
-rw-r--r--spec/support/time_tracking_shared_examples.rb13
-rw-r--r--spec/support/wait_for_ajax.rb5
-rw-r--r--spec/support/wait_for_requests.rb5
-rw-r--r--spec/support/wait_for_vue_resource.rb14
-rw-r--r--spec/support/workhorse_helpers.rb2
-rw-r--r--spec/tasks/config_lint_spec.rb4
-rw-r--r--spec/tasks/gitlab/backup_rake_spec.rb8
-rw-r--r--spec/tasks/gitlab/gitaly_rake_spec.rb2
-rw-r--r--spec/tasks/gitlab/shell_rake_spec.rb4
-rw-r--r--spec/unicorn/unicorn_spec.rb98
-rw-r--r--spec/uploaders/personal_file_uploader_spec.rb31
-rw-r--r--spec/validators/dynamic_path_validator_spec.rb266
-rw-r--r--spec/views/projects/_last_commit.html.haml_spec.rb22
-rw-r--r--spec/views/projects/blob/_viewer.html.haml_spec.rb97
-rw-r--r--spec/views/projects/commit/_commit_box.html.haml_spec.rb34
-rw-r--r--spec/views/projects/commit/show.html.haml_spec.rb44
-rw-r--r--spec/views/projects/environments/terminal.html.haml_spec.rb32
-rw-r--r--spec/views/projects/imports/new.html.haml_spec.rb22
-rw-r--r--spec/views/projects/pipelines/_stage.html.haml_spec.rb5
-rw-r--r--spec/views/projects/pipelines/show.html.haml_spec.rb67
-rw-r--r--spec/views/projects/tags/index.html.haml_spec.rb20
-rw-r--r--spec/views/projects/tree/show.html.haml_spec.rb2
-rw-r--r--spec/views/shared/notes/_form.html.haml_spec.rb (renamed from spec/views/projects/notes/_form.html.haml_spec.rb)2
-rw-r--r--spec/workers/delete_user_worker_spec.rb4
-rw-r--r--spec/workers/emails_on_push_worker_spec.rb2
-rw-r--r--spec/workers/expire_build_instance_artifacts_worker_spec.rb6
-rw-r--r--spec/workers/expire_pipeline_cache_worker_spec.rb44
-rw-r--r--spec/workers/git_garbage_collect_worker_spec.rb4
-rw-r--r--spec/workers/gitlab_usage_ping_worker_spec.rb2
-rw-r--r--spec/workers/group_destroy_worker_spec.rb2
-rw-r--r--spec/workers/merge_worker_spec.rb2
-rw-r--r--spec/workers/namespaceless_project_destroy_worker_spec.rb79
-rw-r--r--spec/workers/pipeline_metrics_worker_spec.rb2
-rw-r--r--spec/workers/pipeline_process_worker_spec.rb (renamed from spec/workers/pipeline_proccess_worker_spec.rb)0
-rw-r--r--spec/workers/pipeline_schedule_worker_spec.rb51
-rw-r--r--spec/workers/post_receive_spec.rb68
-rw-r--r--spec/workers/process_commit_worker_spec.rb8
-rw-r--r--spec/workers/project_destroy_worker_spec.rb2
-rw-r--r--spec/workers/propagate_service_template_worker_spec.rb29
-rw-r--r--spec/workers/remove_expired_members_worker_spec.rb2
-rw-r--r--spec/workers/remove_unreferenced_lfs_objects_worker_spec.rb2
-rw-r--r--spec/workers/repository_check/clear_worker_spec.rb2
-rw-r--r--spec/workers/repository_fork_worker_spec.rb2
-rw-r--r--spec/workers/trigger_schedule_worker_spec.rb73
830 files changed, 27205 insertions, 5489 deletions
diff --git a/spec/controllers/admin/hooks_controller_spec.rb b/spec/controllers/admin/hooks_controller_spec.rb
new file mode 100644
index 00000000000..1d1070e90f4
--- /dev/null
+++ b/spec/controllers/admin/hooks_controller_spec.rb
@@ -0,0 +1,28 @@
+require 'spec_helper'
+
+describe Admin::HooksController do
+ let(:admin) { create(:admin) }
+
+ before do
+ sign_in(admin)
+ end
+
+ describe 'POST #create' do
+ it 'sets all parameters' do
+ hook_params = {
+ enable_ssl_verification: true,
+ push_events: true,
+ tag_push_events: true,
+ repository_update_events: true,
+ token: "TEST TOKEN",
+ url: "http://example.com"
+ }
+
+ post :create, hook: hook_params
+
+ expect(response).to have_http_status(302)
+ expect(SystemHook.all.size).to eq(1)
+ expect(SystemHook.first).to have_attributes(hook_params)
+ end
+ end
+end
diff --git a/spec/controllers/admin/services_controller_spec.rb b/spec/controllers/admin/services_controller_spec.rb
index e5cdd52307e..c94616d8508 100644
--- a/spec/controllers/admin/services_controller_spec.rb
+++ b/spec/controllers/admin/services_controller_spec.rb
@@ -23,4 +23,36 @@ describe Admin::ServicesController do
end
end
end
+
+ describe "#update" do
+ let(:project) { create(:empty_project) }
+ let!(:service) do
+ RedmineService.create(
+ project: project,
+ active: false,
+ template: true,
+ properties: {
+ project_url: 'http://abc',
+ issues_url: 'http://abc',
+ new_issue_url: 'http://abc'
+ }
+ )
+ end
+
+ it 'calls the propagation worker when service is active' do
+ expect(PropagateServiceTemplateWorker).to receive(:perform_async).with(service.id)
+
+ put :update, id: service.id, service: { active: true }
+
+ expect(response).to have_http_status(302)
+ end
+
+ it 'does not call the propagation worker when service is not active' do
+ expect(PropagateServiceTemplateWorker).not_to receive(:perform_async)
+
+ put :update, id: service.id, service: { properties: {} }
+
+ expect(response).to have_http_status(302)
+ end
+ end
end
diff --git a/spec/controllers/application_controller_spec.rb b/spec/controllers/application_controller_spec.rb
index 760f33b09c1..d40aae04fc3 100644
--- a/spec/controllers/application_controller_spec.rb
+++ b/spec/controllers/application_controller_spec.rb
@@ -4,7 +4,7 @@ describe ApplicationController do
let(:user) { create(:user) }
describe '#check_password_expiration' do
- let(:controller) { ApplicationController.new }
+ let(:controller) { described_class.new }
it 'redirects if the user is over their password expiry' do
user.password_expires_at = Time.new(2002)
@@ -34,7 +34,7 @@ describe ApplicationController do
describe "#authenticate_user_from_token!" do
describe "authenticating a user from a private token" do
- controller(ApplicationController) do
+ controller(described_class) do
def index
render text: "authenticated"
end
@@ -66,7 +66,7 @@ describe ApplicationController do
end
describe "authenticating a user from a personal access token" do
- controller(ApplicationController) do
+ controller(described_class) do
def index
render text: 'authenticated'
end
@@ -106,16 +106,15 @@ describe ApplicationController do
controller.send(:route_not_found)
end
- it 'does redirect to login page if not authenticated' do
+ it 'does redirect to login page via authenticate_user! if not authenticated' do
allow(controller).to receive(:current_user).and_return(nil)
- expect(controller).to receive(:redirect_to)
- expect(controller).to receive(:new_user_session_path)
+ expect(controller).to receive(:authenticate_user!)
controller.send(:route_not_found)
end
end
context 'two-factor authentication' do
- let(:controller) { ApplicationController.new }
+ let(:controller) { described_class.new }
describe '#check_two_factor_requirement' do
subject { controller.send :check_two_factor_requirement }
diff --git a/spec/controllers/blob_controller_spec.rb b/spec/controllers/blob_controller_spec.rb
deleted file mode 100644
index 44e011fd3a8..00000000000
--- a/spec/controllers/blob_controller_spec.rb
+++ /dev/null
@@ -1,67 +0,0 @@
-require 'spec_helper'
-
-describe Projects::BlobController do
- let(:project) { create(:project, :repository) }
- let(:user) { create(:user) }
-
- before do
- sign_in(user)
-
- project.team << [user, :master]
-
- allow(project).to receive(:branches).and_return(['master', 'foo/bar/baz'])
- allow(project).to receive(:tags).and_return(['v1.0.0', 'v2.0.0'])
- controller.instance_variable_set(:@project, project)
- end
-
- describe "GET show" do
- render_views
-
- before do
- get(:show,
- namespace_id: project.namespace,
- project_id: project,
- id: id)
- end
-
- context "valid branch, valid file" do
- let(:id) { 'master/README.md' }
- it { is_expected.to respond_with(:success) }
- end
-
- context "valid branch, invalid file" do
- let(:id) { 'master/invalid-path.rb' }
- it { is_expected.to respond_with(:not_found) }
- end
-
- context "invalid branch, valid file" do
- let(:id) { 'invalid-branch/README.md' }
- it { is_expected.to respond_with(:not_found) }
- end
-
- context "binary file" do
- let(:id) { 'binary-encoding/encoding/binary-1.bin' }
- it { is_expected.to respond_with(:success) }
- end
- end
-
- describe 'GET show with tree path' do
- render_views
-
- before do
- get(:show,
- namespace_id: project.namespace,
- project_id: project,
- id: id)
- controller.instance_variable_set(:@blob, nil)
- end
-
- context 'redirect to tree' do
- let(:id) { 'markdown/doc' }
- it 'redirects' do
- expect(subject).
- to redirect_to("/#{project.path_with_namespace}/tree/markdown/doc")
- end
- end
- end
-end
diff --git a/spec/controllers/dashboard/todos_controller_spec.rb b/spec/controllers/dashboard/todos_controller_spec.rb
index 6075259ea99..085f3fd8543 100644
--- a/spec/controllers/dashboard/todos_controller_spec.rb
+++ b/spec/controllers/dashboard/todos_controller_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
describe Dashboard::TodosController do
- include ApiHelpers
-
let(:user) { create(:user) }
let(:author) { create(:user) }
let(:project) { create(:empty_project) }
@@ -16,7 +14,7 @@ describe Dashboard::TodosController do
describe 'GET #index' do
context 'when using pagination' do
let(:last_page) { user.todos.page.total_pages }
- let!(:issues) { create_list(:issue, 2, project: project, assignee: user) }
+ let!(:issues) { create_list(:issue, 2, project: project, assignees: [user]) }
before do
issues.each { |issue| todo_service.new_issue(issue, user) }
diff --git a/spec/controllers/groups/milestones_controller_spec.rb b/spec/controllers/groups/milestones_controller_spec.rb
index 6e4b5f78e33..7cf2996ffd0 100644
--- a/spec/controllers/groups/milestones_controller_spec.rb
+++ b/spec/controllers/groups/milestones_controller_spec.rb
@@ -6,6 +6,16 @@ describe Groups::MilestonesController do
let(:project2) { create(:empty_project, group: group) }
let(:user) { create(:user) }
let(:title) { '肯定不是中文的问题' }
+ let(:milestone) do
+ project_milestone = create(:milestone, project: project)
+
+ GroupMilestone.build(
+ group,
+ [project],
+ project_milestone.title
+ )
+ end
+ let(:milestone_path) { group_milestone_path(group, milestone.safe_title, title: milestone.title) }
before do
sign_in(user)
@@ -14,6 +24,8 @@ describe Groups::MilestonesController do
controller.instance_variable_set(:@group, group)
end
+ it_behaves_like 'milestone tabs'
+
describe "#create" do
it "creates group milestone with Chinese title" do
post :create,
diff --git a/spec/controllers/groups_controller_spec.rb b/spec/controllers/groups_controller_spec.rb
index cad82a34fb0..15dae3231ca 100644
--- a/spec/controllers/groups_controller_spec.rb
+++ b/spec/controllers/groups_controller_spec.rb
@@ -26,6 +26,41 @@ describe GroupsController do
end
end
+ describe 'GET #subgroups' do
+ let!(:public_subgroup) { create(:group, :public, parent: group) }
+ let!(:private_subgroup) { create(:group, :private, parent: group) }
+
+ context 'as a user' do
+ before do
+ sign_in(user)
+ end
+
+ it 'shows the public subgroups' do
+ get :subgroups, id: group.to_param
+
+ expect(assigns(:nested_groups)).to contain_exactly(public_subgroup)
+ end
+
+ context 'being member' do
+ it 'shows public and private subgroups the user is member of' do
+ private_subgroup.add_guest(user)
+
+ get :subgroups, id: group.to_param
+
+ expect(assigns(:nested_groups)).to contain_exactly(public_subgroup, private_subgroup)
+ end
+ end
+ end
+
+ context 'as a guest' do
+ it 'shows the public subgroups' do
+ get :subgroups, id: group.to_param
+
+ expect(assigns(:nested_groups)).to contain_exactly(public_subgroup)
+ end
+ end
+ end
+
describe 'GET #issues' do
let(:issue_1) { create(:issue, project: project) }
let(:issue_2) { create(:issue, project: project) }
@@ -33,7 +68,7 @@ describe GroupsController do
before do
create_list(:award_emoji, 3, awardable: issue_2)
create_list(:award_emoji, 2, awardable: issue_1)
- create_list(:award_emoji, 2, :downvote, awardable: issue_2,)
+ create_list(:award_emoji, 2, :downvote, awardable: issue_2)
sign_in(user)
end
@@ -49,6 +84,26 @@ describe GroupsController do
expect(assigns(:issues)).to eq [issue_2, issue_1]
end
end
+
+ context 'when requesting the canonical path with different casing' do
+ it 'redirects to the correct casing' do
+ get :issues, id: group.to_param.upcase
+
+ expect(response).to redirect_to(issues_group_path(group.to_param))
+ expect(controller).not_to set_flash[:notice]
+ end
+ end
+
+ context 'when requesting a redirected path' do
+ let(:redirect_route) { group.redirect_routes.create(path: 'old-path') }
+
+ it 'redirects to the canonical path' do
+ get :issues, id: redirect_route.path
+
+ expect(response).to redirect_to(issues_group_path(group.to_param))
+ expect(controller).to set_flash[:notice].to(group_moved_message(redirect_route, group))
+ end
+ end
end
describe 'GET #merge_requests' do
@@ -74,6 +129,26 @@ describe GroupsController do
expect(assigns(:merge_requests)).to eq [merge_request_2, merge_request_1]
end
end
+
+ context 'when requesting the canonical path with different casing' do
+ it 'redirects to the correct casing' do
+ get :merge_requests, id: group.to_param.upcase
+
+ expect(response).to redirect_to(merge_requests_group_path(group.to_param))
+ expect(controller).not_to set_flash[:notice]
+ end
+ end
+
+ context 'when requesting a redirected path' do
+ let(:redirect_route) { group.redirect_routes.create(path: 'old-path') }
+
+ it 'redirects to the canonical path' do
+ get :merge_requests, id: redirect_route.path
+
+ expect(response).to redirect_to(merge_requests_group_path(group.to_param))
+ expect(controller).to set_flash[:notice].to(group_moved_message(redirect_route, group))
+ end
+ end
end
describe 'DELETE #destroy' do
@@ -81,7 +156,7 @@ describe GroupsController do
it 'returns 404' do
sign_in(create(:user))
- delete :destroy, id: group.path
+ delete :destroy, id: group.to_param
expect(response.status).to eq(404)
end
@@ -94,15 +169,39 @@ describe GroupsController do
it 'schedules a group destroy' do
Sidekiq::Testing.fake! do
- expect { delete :destroy, id: group.path }.to change(GroupDestroyWorker.jobs, :size).by(1)
+ expect { delete :destroy, id: group.to_param }.to change(GroupDestroyWorker.jobs, :size).by(1)
end
end
it 'redirects to the root path' do
- delete :destroy, id: group.path
+ delete :destroy, id: group.to_param
expect(response).to redirect_to(root_path)
end
+
+ context 'when requesting the canonical path with different casing' do
+ it 'does not 404' do
+ delete :destroy, id: group.to_param.upcase
+
+ expect(response).not_to have_http_status(404)
+ end
+
+ it 'does not redirect to the correct casing' do
+ delete :destroy, id: group.to_param.upcase
+
+ expect(response).not_to redirect_to(group_path(group.to_param))
+ end
+ end
+
+ context 'when requesting a redirected path' do
+ let(:redirect_route) { group.redirect_routes.create(path: 'old-path') }
+
+ it 'returns not found' do
+ delete :destroy, id: redirect_route.path
+
+ expect(response).to have_http_status(404)
+ end
+ end
end
end
@@ -111,7 +210,7 @@ describe GroupsController do
sign_in(user)
end
- it 'updates the path succesfully' do
+ it 'updates the path successfully' do
post :update, id: group.to_param, group: { path: 'new_path' }
expect(response).to have_http_status(302)
@@ -125,5 +224,33 @@ describe GroupsController do
expect(assigns(:group).errors).not_to be_empty
expect(assigns(:group).path).not_to eq('new_path')
end
+
+ context 'when requesting the canonical path with different casing' do
+ it 'does not 404' do
+ post :update, id: group.to_param.upcase, group: { path: 'new_path' }
+
+ expect(response).not_to have_http_status(404)
+ end
+
+ it 'does not redirect to the correct casing' do
+ post :update, id: group.to_param.upcase, group: { path: 'new_path' }
+
+ expect(response).not_to redirect_to(group_path(group.to_param))
+ end
+ end
+
+ context 'when requesting a redirected path' do
+ let(:redirect_route) { group.redirect_routes.create(path: 'old-path') }
+
+ it 'returns not found' do
+ post :update, id: redirect_route.path, group: { path: 'new_path' }
+
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
+
+ def group_moved_message(redirect_route, group)
+ "Group '#{redirect_route.path}' was moved to '#{group.full_path}'. Please update any links and bookmarks that may still have the old path."
end
end
diff --git a/spec/controllers/oauth/authorizations_controller_spec.rb b/spec/controllers/oauth/authorizations_controller_spec.rb
new file mode 100644
index 00000000000..d321bfcea9d
--- /dev/null
+++ b/spec/controllers/oauth/authorizations_controller_spec.rb
@@ -0,0 +1,55 @@
+require 'spec_helper'
+
+describe Oauth::AuthorizationsController do
+ let(:user) { create(:user) }
+
+ let(:doorkeeper) do
+ Doorkeeper::Application.create(
+ name: "MyApp",
+ redirect_uri: 'http://example.com',
+ scopes: "")
+ end
+
+ let(:params) do
+ {
+ response_type: "code",
+ client_id: doorkeeper.uid,
+ redirect_uri: doorkeeper.redirect_uri,
+ state: 'state'
+ }
+ end
+
+ before do
+ sign_in(user)
+ end
+
+ describe 'GET #new' do
+ context 'without valid params' do
+ it 'returns 200 code and renders error view' do
+ get :new
+
+ expect(response).to have_http_status(200)
+ expect(response).to render_template('doorkeeper/authorizations/error')
+ end
+ end
+
+ context 'with valid params' do
+ it 'returns 200 code and renders view' do
+ get :new, params
+
+ expect(response).to have_http_status(200)
+ expect(response).to render_template('doorkeeper/authorizations/new')
+ end
+
+ it 'deletes session.user_return_to and redirects when skip authorization' do
+ request.session['user_return_to'] = 'http://example.com'
+ allow(controller).to receive(:skip_authorization?).and_return(true)
+
+ get :new, params
+
+ expect(request.session['user_return_to']).to be_nil
+ expect(response).to have_http_status(302)
+ end
+ end
+ end
+end
diff --git a/spec/controllers/profiles/personal_access_tokens_spec.rb b/spec/controllers/profiles/personal_access_tokens_controller_spec.rb
index 98a43e278b2..98a43e278b2 100644
--- a/spec/controllers/profiles/personal_access_tokens_spec.rb
+++ b/spec/controllers/profiles/personal_access_tokens_controller_spec.rb
diff --git a/spec/controllers/projects/artifacts_controller_spec.rb b/spec/controllers/projects/artifacts_controller_spec.rb
new file mode 100644
index 00000000000..eff9fab8da2
--- /dev/null
+++ b/spec/controllers/projects/artifacts_controller_spec.rb
@@ -0,0 +1,188 @@
+require 'spec_helper'
+
+describe Projects::ArtifactsController do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :repository) }
+
+ let(:pipeline) do
+ create(:ci_pipeline,
+ project: project,
+ sha: project.commit.sha,
+ ref: project.default_branch,
+ status: 'success')
+ end
+
+ let(:build) { create(:ci_build, :success, :artifacts, pipeline: pipeline) }
+
+ before do
+ project.team << [user, :developer]
+
+ sign_in(user)
+ end
+
+ describe 'GET download' do
+ it 'sends the artifacts file' do
+ expect(controller).to receive(:send_file).with(build.artifacts_file.path, disposition: 'attachment').and_call_original
+
+ get :download, namespace_id: project.namespace, project_id: project, build_id: build
+ end
+ end
+
+ describe 'GET browse' do
+ context 'when the directory exists' do
+ it 'renders the browse view' do
+ get :browse, namespace_id: project.namespace, project_id: project, build_id: build, path: 'other_artifacts_0.1.2'
+
+ expect(response).to render_template('projects/artifacts/browse')
+ end
+ end
+
+ context 'when the directory does not exist' do
+ it 'responds Not Found' do
+ get :browse, namespace_id: project.namespace, project_id: project, build_id: build, path: 'unknown'
+
+ expect(response).to be_not_found
+ end
+ end
+ end
+
+ describe 'GET file' do
+ context 'when the file exists' do
+ it 'renders the file view' do
+ get :file, namespace_id: project.namespace, project_id: project, build_id: build, path: 'ci_artifacts.txt'
+
+ expect(response).to render_template('projects/artifacts/file')
+ end
+ end
+
+ context 'when the file does not exist' do
+ it 'responds Not Found' do
+ get :file, namespace_id: project.namespace, project_id: project, build_id: build, path: 'unknown'
+
+ expect(response).to be_not_found
+ end
+ end
+ end
+
+ describe 'GET raw' do
+ context 'when the file exists' do
+ it 'serves the file using workhorse' do
+ get :raw, namespace_id: project.namespace, project_id: project, build_id: build, path: 'ci_artifacts.txt'
+
+ send_data = response.headers[Gitlab::Workhorse::SEND_DATA_HEADER]
+
+ expect(send_data).to start_with('artifacts-entry:')
+
+ base64_params = send_data.sub(/\Aartifacts\-entry:/, '')
+ params = JSON.parse(Base64.urlsafe_decode64(base64_params))
+
+ expect(params.keys).to eq(%w(Archive Entry))
+ expect(params['Archive']).to end_with('build_artifacts.zip')
+ expect(params['Entry']).to eq(Base64.encode64('ci_artifacts.txt'))
+ end
+ end
+
+ context 'when the file does not exist' do
+ it 'responds Not Found' do
+ get :raw, namespace_id: project.namespace, project_id: project, build_id: build, path: 'unknown'
+
+ expect(response).to be_not_found
+ end
+ end
+ end
+
+ describe 'GET latest_succeeded' do
+ def params_from_ref(ref = pipeline.ref, job = build.name, path = 'browse')
+ {
+ namespace_id: project.namespace,
+ project_id: project,
+ ref_name_and_path: File.join(ref, path),
+ job: job
+ }
+ end
+
+ context 'cannot find the 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 :latest_succeeded, params_from_ref('TAIL', build.name)
+ end
+
+ it_behaves_like 'not found'
+ end
+
+ context 'has no such build' do
+ before do
+ get :latest_succeeded, params_from_ref(pipeline.ref, 'NOBUILD')
+ end
+
+ it_behaves_like 'not found'
+ end
+
+ context 'has no path' do
+ before do
+ get :latest_succeeded, params_from_ref(pipeline.sha, build.name, '')
+ end
+
+ it_behaves_like 'not found'
+ end
+ end
+
+ context 'found the build and redirect' do
+ shared_examples 'redirect to the build' do
+ it 'redirects' do
+ path = browse_namespace_project_build_artifacts_path(
+ project.namespace,
+ project,
+ build)
+
+ expect(response).to redirect_to(path)
+ end
+ end
+
+ context 'with regular branch' do
+ before do
+ pipeline.update(ref: 'master',
+ sha: project.commit('master').sha)
+
+ get :latest_succeeded, params_from_ref('master')
+ end
+
+ it_behaves_like 'redirect to the build'
+ end
+
+ context 'with branch name containing slash' do
+ before do
+ pipeline.update(ref: 'improve/awesome',
+ sha: project.commit('improve/awesome').sha)
+
+ get :latest_succeeded, params_from_ref('improve/awesome')
+ end
+
+ it_behaves_like 'redirect to the build'
+ end
+
+ context 'with branch name and path containing slashes' do
+ before do
+ pipeline.update(ref: 'improve/awesome',
+ sha: project.commit('improve/awesome').sha)
+
+ get :latest_succeeded, params_from_ref('improve/awesome', build.name, 'file/README.md')
+ end
+
+ it 'redirects' do
+ path = file_namespace_project_build_artifacts_path(
+ project.namespace,
+ project,
+ build,
+ 'README.md')
+
+ expect(response).to redirect_to(path)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/controllers/projects/blob_controller_spec.rb b/spec/controllers/projects/blob_controller_spec.rb
index 0fd09d156c4..3b3caa9d3e6 100644
--- a/spec/controllers/projects/blob_controller_spec.rb
+++ b/spec/controllers/projects/blob_controller_spec.rb
@@ -3,6 +3,57 @@ require 'rails_helper'
describe Projects::BlobController do
let(:project) { create(:project, :public, :repository) }
+ describe "GET show" do
+ render_views
+
+ context 'with file path' do
+ before do
+ get(:show,
+ namespace_id: project.namespace,
+ project_id: project,
+ id: id)
+ end
+
+ context "valid branch, valid file" do
+ let(:id) { 'master/README.md' }
+ it { is_expected.to respond_with(:success) }
+ end
+
+ context "valid branch, invalid file" do
+ let(:id) { 'master/invalid-path.rb' }
+ it { is_expected.to respond_with(:not_found) }
+ end
+
+ context "invalid branch, valid file" do
+ let(:id) { 'invalid-branch/README.md' }
+ it { is_expected.to respond_with(:not_found) }
+ end
+
+ context "binary file" do
+ let(:id) { 'binary-encoding/encoding/binary-1.bin' }
+ it { is_expected.to respond_with(:success) }
+ end
+ end
+
+ context 'with tree path' do
+ before do
+ get(:show,
+ namespace_id: project.namespace,
+ project_id: project,
+ id: id)
+ controller.instance_variable_set(:@blob, nil)
+ end
+
+ context 'redirect to tree' do
+ let(:id) { 'markdown/doc' }
+ it 'redirects' do
+ expect(subject).
+ to redirect_to("/#{project.path_with_namespace}/tree/markdown/doc")
+ end
+ end
+ end
+ end
+
describe 'GET diff' do
let(:user) { create(:user) }
diff --git a/spec/controllers/projects/boards/issues_controller_spec.rb b/spec/controllers/projects/boards/issues_controller_spec.rb
index 15667e8d4b1..dc3b72c6de4 100644
--- a/spec/controllers/projects/boards/issues_controller_spec.rb
+++ b/spec/controllers/projects/boards/issues_controller_spec.rb
@@ -34,7 +34,7 @@ describe Projects::Boards::IssuesController do
issue = create(:labeled_issue, project: project, labels: [planning])
create(:labeled_issue, project: project, labels: [planning])
create(:labeled_issue, project: project, labels: [development], due_date: Date.tomorrow)
- create(:labeled_issue, project: project, labels: [development], assignee: johndoe)
+ create(:labeled_issue, project: project, labels: [development], assignees: [johndoe])
issue.subscribe(johndoe, project)
list_issues user: user, board: board, list: list2
diff --git a/spec/controllers/projects/branches_controller_spec.rb b/spec/controllers/projects/branches_controller_spec.rb
index d20e7368086..f285e5333d6 100644
--- a/spec/controllers/projects/branches_controller_spec.rb
+++ b/spec/controllers/projects/branches_controller_spec.rb
@@ -14,7 +14,7 @@ describe Projects::BranchesController do
controller.instance_variable_set(:@project, project)
end
- describe "POST create" do
+ describe "POST create with HTML format" do
render_views
context "on creation of a new branch" do
@@ -152,6 +152,42 @@ describe Projects::BranchesController do
end
end
+ describe 'POST create with JSON format' do
+ before do
+ sign_in(user)
+ end
+
+ context 'with valid params' do
+ it 'returns a successful 200 response' do
+ create_branch name: 'my-branch', ref: 'master'
+
+ expect(response).to have_http_status(200)
+ end
+
+ it 'returns the created branch' do
+ create_branch name: 'my-branch', ref: 'master'
+
+ expect(response).to match_response_schema('branch')
+ end
+ end
+
+ context 'with invalid params' do
+ it 'returns an unprocessable entity 422 response' do
+ create_branch name: "<script>alert('merge');</script>", ref: "<script>alert('ref');</script>"
+
+ expect(response).to have_http_status(422)
+ end
+ end
+
+ def create_branch(name:, ref:)
+ post :create, namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ branch_name: name,
+ ref: ref,
+ format: :json
+ end
+ end
+
describe "POST destroy with HTML format" do
render_views
@@ -177,33 +213,98 @@ describe Projects::BranchesController do
sign_in(user)
post :destroy,
- format: :js,
- id: branch,
- namespace_id: project.namespace,
- project_id: project
+ format: format,
+ id: branch,
+ namespace_id: project.namespace,
+ project_id: project
end
- context "valid branch name, valid source" do
+ context 'as JS' do
let(:branch) { "feature" }
+ let(:format) { :js }
- it { expect(response).to have_http_status(200) }
- end
+ context "valid branch name, valid source" do
+ let(:branch) { "feature" }
- context "valid branch name with unencoded slashes" do
- let(:branch) { "improve/awesome" }
+ it { expect(response).to have_http_status(200) }
+ it { expect(response.body).to be_blank }
+ end
+
+ context "valid branch name with unencoded slashes" do
+ let(:branch) { "improve/awesome" }
+
+ it { expect(response).to have_http_status(200) }
+ it { expect(response.body).to be_blank }
+ end
- it { expect(response).to have_http_status(200) }
+ context "valid branch name with encoded slashes" do
+ let(:branch) { "improve%2Fawesome" }
+
+ it { expect(response).to have_http_status(200) }
+ it { expect(response.body).to be_blank }
+ end
+
+ context "invalid branch name, valid ref" do
+ let(:branch) { "no-branch" }
+
+ it { expect(response).to have_http_status(404) }
+ it { expect(response.body).to be_blank }
+ end
end
- context "valid branch name with encoded slashes" do
- let(:branch) { "improve%2Fawesome" }
+ context 'as JSON' do
+ let(:branch) { "feature" }
+ let(:format) { :json }
+
+ context 'valid branch name, valid source' do
+ let(:branch) { "feature" }
+
+ it 'returns JSON response with message' do
+ expect(json_response).to eql("message" => 'Branch was removed')
+ end
+
+ it { expect(response).to have_http_status(200) }
+ end
+
+ context 'valid branch name with unencoded slashes' do
+ let(:branch) { "improve/awesome" }
+
+ it 'returns JSON response with message' do
+ expect(json_response).to eql('message' => 'Branch was removed')
+ end
+
+ it { expect(response).to have_http_status(200) }
+ end
+
+ context "valid branch name with encoded slashes" do
+ let(:branch) { 'improve%2Fawesome' }
+
+ it 'returns JSON response with message' do
+ expect(json_response).to eql('message' => 'Branch was removed')
+ end
+
+ it { expect(response).to have_http_status(200) }
+ end
- it { expect(response).to have_http_status(200) }
+ context 'invalid branch name, valid ref' do
+ let(:branch) { 'no-branch' }
+
+ it 'returns JSON response with message' do
+ expect(json_response).to eql('message' => 'No such branch')
+ end
+
+ it { expect(response).to have_http_status(404) }
+ end
end
- context "invalid branch name, valid ref" do
- let(:branch) { "no-branch" }
- it { expect(response).to have_http_status(404) }
+ context 'as HTML' do
+ let(:branch) { "feature" }
+ let(:format) { :html }
+
+ it 'redirects to branches path' do
+ expect(response)
+ .to redirect_to(namespace_project_branches_path(project.namespace, project))
+ end
end
end
diff --git a/spec/controllers/projects/builds_controller_spec.rb b/spec/controllers/projects/builds_controller_spec.rb
index faf3770f5e9..3ce23c17cdc 100644
--- a/spec/controllers/projects/builds_controller_spec.rb
+++ b/spec/controllers/projects/builds_controller_spec.rb
@@ -3,14 +3,67 @@ require 'spec_helper'
describe Projects::BuildsController do
include ApiHelpers
- let(:user) { create(:user) }
let(:project) { create(:empty_project, :public) }
-
- before do
- sign_in(user)
- end
+ let(:pipeline) { create(:ci_pipeline, project: project) }
+ let(:user) { create(:user) }
describe 'GET index' do
+ context 'when scope is pending' do
+ before do
+ create(:ci_build, :pending, pipeline: pipeline)
+
+ get_index(scope: 'pending')
+ end
+
+ it 'has only pending builds' do
+ expect(response).to have_http_status(:ok)
+ expect(assigns(:builds).first.status).to eq('pending')
+ end
+ end
+
+ context 'when scope is running' do
+ before do
+ create(:ci_build, :running, pipeline: pipeline)
+
+ get_index(scope: 'running')
+ end
+
+ it 'has only running builds' do
+ expect(response).to have_http_status(:ok)
+ expect(assigns(:builds).first.status).to eq('running')
+ end
+ end
+
+ context 'when scope is finished' do
+ before do
+ create(:ci_build, :success, pipeline: pipeline)
+
+ get_index(scope: 'finished')
+ end
+
+ it 'has only finished builds' do
+ expect(response).to have_http_status(:ok)
+ expect(assigns(:builds).first.status).to eq('success')
+ end
+ end
+
+ context 'when page is specified' do
+ let(:last_page) { project.builds.page.total_pages }
+
+ context 'when page number is eligible' do
+ before do
+ create_list(:ci_build, 2, pipeline: pipeline)
+
+ get_index(page: last_page.to_param)
+ end
+
+ it 'redirects to the page' do
+ expect(response).to have_http_status(:ok)
+ expect(assigns(:builds).current_page).to eq(last_page)
+ end
+ end
+ end
+
context 'number of queries' do
before do
Ci::Build::AVAILABLE_STATUSES.each do |status|
@@ -25,13 +78,8 @@ describe Projects::BuildsController do
RequestStore.clear!
end
- def render
- get :index, namespace_id: project.namespace,
- project_id: project
- end
-
it "verifies number of queries" do
- recorded = ActiveRecord::QueryRecorder.new { render }
+ recorded = ActiveRecord::QueryRecorder.new { get_index }
expect(recorded.count).to be_within(5).of(8)
end
@@ -41,10 +89,83 @@ describe Projects::BuildsController do
pipeline: pipeline, name: name, status: status)
end
end
+
+ def get_index(**extra_params)
+ params = {
+ namespace_id: project.namespace.to_param,
+ project_id: project
+ }
+
+ get :index, params.merge(extra_params)
+ end
+ end
+
+ describe 'GET show' do
+ context 'when build exists' do
+ let!(:build) { create(:ci_build, pipeline: pipeline) }
+
+ before do
+ get_show(id: build.id)
+ end
+
+ it 'has a build' do
+ expect(response).to have_http_status(:ok)
+ expect(assigns(:build).id).to eq(build.id)
+ end
+ end
+
+ context 'when build does not exist' do
+ before do
+ get_show(id: 1234)
+ end
+
+ it 'renders not_found' do
+ expect(response).to have_http_status(:not_found)
+ end
+ end
+
+ def get_show(**extra_params)
+ params = {
+ namespace_id: project.namespace.to_param,
+ project_id: project
+ }
+
+ get :show, params.merge(extra_params)
+ end
+ end
+
+ describe 'GET trace.json' do
+ before do
+ get_trace
+ end
+
+ context 'when build has a trace' do
+ let(:build) { create(:ci_build, :trace, pipeline: pipeline) }
+
+ it 'returns a trace' do
+ expect(response).to have_http_status(:ok)
+ expect(json_response['html']).to eq('BUILD TRACE')
+ end
+ end
+
+ context 'when build has no traces' do
+ let(:build) { create(:ci_build, pipeline: pipeline) }
+
+ it 'returns no traces' do
+ expect(response).to have_http_status(:ok)
+ expect(json_response['html']).to be_nil
+ end
+ end
+
+ def get_trace
+ get :trace, namespace_id: project.namespace,
+ project_id: project,
+ id: build.id,
+ format: :json
+ end
end
describe 'GET status.json' do
- let(:pipeline) { create(:ci_pipeline, project: project) }
let(:build) { create(:ci_build, pipeline: pipeline) }
let(:status) { build.detailed_status(double('user')) }
@@ -63,4 +184,263 @@ describe Projects::BuildsController do
expect(json_response['favicon']).to eq "/assets/ci_favicons/#{status.favicon}.ico"
end
end
+
+ describe 'GET trace.json' do
+ let(:pipeline) { create(:ci_pipeline, project: project) }
+ let(:build) { create(:ci_build, pipeline: pipeline) }
+ let(:user) { create(:user) }
+
+ context 'when user is logged in as developer' do
+ before do
+ project.add_developer(user)
+ sign_in(user)
+
+ get_trace
+ end
+
+ it 'traces build log' do
+ expect(response).to have_http_status(:ok)
+ expect(json_response['id']).to eq build.id
+ expect(json_response['status']).to eq build.status
+ end
+ end
+
+ context 'when user is logged in as non member' do
+ before do
+ sign_in(user)
+
+ get_trace
+ end
+
+ it 'traces build log' do
+ expect(response).to have_http_status(:ok)
+ expect(json_response['id']).to eq build.id
+ expect(json_response['status']).to eq build.status
+ end
+ end
+
+ def get_trace
+ get :trace, namespace_id: project.namespace,
+ project_id: project,
+ id: build.id,
+ format: :json
+ end
+ end
+
+ describe 'POST retry' do
+ before do
+ project.add_developer(user)
+ sign_in(user)
+
+ post_retry
+ end
+
+ context 'when build is retryable' do
+ let(:build) { create(:ci_build, :retryable, pipeline: pipeline) }
+
+ it 'redirects to the retried build page' do
+ expect(response).to have_http_status(:found)
+ expect(response).to redirect_to(namespace_project_build_path(id: Ci::Build.last.id))
+ end
+ end
+
+ context 'when build is not retryable' do
+ let(:build) { create(:ci_build, pipeline: pipeline) }
+
+ it 'renders unprocessable_entity' do
+ expect(response).to have_http_status(:unprocessable_entity)
+ end
+ end
+
+ def post_retry
+ post :retry, namespace_id: project.namespace,
+ project_id: project,
+ id: build.id
+ end
+ end
+
+ describe 'POST play' do
+ before do
+ project.add_master(user)
+ sign_in(user)
+
+ post_play
+ end
+
+ context 'when build is playable' do
+ let(:build) { create(:ci_build, :playable, pipeline: pipeline) }
+
+ it 'redirects to the played build page' do
+ expect(response).to have_http_status(:found)
+ expect(response).to redirect_to(namespace_project_build_path(id: build.id))
+ end
+
+ it 'transits to pending' do
+ expect(build.reload).to be_pending
+ end
+ end
+
+ context 'when build is not playable' do
+ let(:build) { create(:ci_build, pipeline: pipeline) }
+
+ it 'renders unprocessable_entity' do
+ expect(response).to have_http_status(:unprocessable_entity)
+ end
+ end
+
+ def post_play
+ post :play, namespace_id: project.namespace,
+ project_id: project,
+ id: build.id
+ end
+ end
+
+ describe 'POST cancel' do
+ before do
+ project.add_developer(user)
+ sign_in(user)
+
+ post_cancel
+ end
+
+ context 'when build is cancelable' do
+ let(:build) { create(:ci_build, :cancelable, pipeline: pipeline) }
+
+ it 'redirects to the canceled build page' do
+ expect(response).to have_http_status(:found)
+ expect(response).to redirect_to(namespace_project_build_path(id: build.id))
+ end
+
+ it 'transits to canceled' do
+ expect(build.reload).to be_canceled
+ end
+ end
+
+ context 'when build is not cancelable' do
+ let(:build) { create(:ci_build, :canceled, pipeline: pipeline) }
+
+ it 'returns unprocessable_entity' do
+ expect(response).to have_http_status(:unprocessable_entity)
+ end
+ end
+
+ def post_cancel
+ post :cancel, namespace_id: project.namespace,
+ project_id: project,
+ id: build.id
+ end
+ end
+
+ describe 'POST cancel_all' do
+ before do
+ project.add_developer(user)
+ sign_in(user)
+ end
+
+ context 'when builds are cancelable' do
+ before do
+ create_list(:ci_build, 2, :cancelable, pipeline: pipeline)
+
+ post_cancel_all
+ end
+
+ it 'redirects to a index page' do
+ expect(response).to have_http_status(:found)
+ expect(response).to redirect_to(namespace_project_builds_path)
+ end
+
+ it 'transits to canceled' do
+ expect(Ci::Build.all).to all(be_canceled)
+ end
+ end
+
+ context 'when builds are not cancelable' do
+ before do
+ create_list(:ci_build, 2, :canceled, pipeline: pipeline)
+
+ post_cancel_all
+ end
+
+ it 'redirects to a index page' do
+ expect(response).to have_http_status(:found)
+ expect(response).to redirect_to(namespace_project_builds_path)
+ end
+ end
+
+ def post_cancel_all
+ post :cancel_all, namespace_id: project.namespace,
+ project_id: project
+ end
+ end
+
+ describe 'POST erase' do
+ before do
+ project.add_developer(user)
+ sign_in(user)
+
+ post_erase
+ end
+
+ context 'when build is erasable' do
+ let(:build) { create(:ci_build, :erasable, :trace, pipeline: pipeline) }
+
+ it 'redirects to the erased build page' do
+ expect(response).to have_http_status(:found)
+ expect(response).to redirect_to(namespace_project_build_path(id: build.id))
+ end
+
+ it 'erases artifacts' do
+ expect(build.artifacts_file.exists?).to be_falsey
+ expect(build.artifacts_metadata.exists?).to be_falsey
+ end
+
+ it 'erases trace' do
+ expect(build.trace.exist?).to be_falsey
+ end
+ end
+
+ context 'when build is not erasable' do
+ let(:build) { create(:ci_build, :erased, pipeline: pipeline) }
+
+ it 'returns unprocessable_entity' do
+ expect(response).to have_http_status(:unprocessable_entity)
+ end
+ end
+
+ def post_erase
+ post :erase, namespace_id: project.namespace,
+ project_id: project,
+ id: build.id
+ end
+ end
+
+ describe 'GET raw' do
+ before do
+ get_raw
+ end
+
+ context 'when build has a trace file' do
+ let(:build) { create(:ci_build, :trace, pipeline: pipeline) }
+
+ it 'send a trace file' do
+ expect(response).to have_http_status(:ok)
+ expect(response.content_type).to eq 'text/plain; charset=utf-8'
+ expect(response.body).to eq 'BUILD TRACE'
+ end
+ end
+
+ context 'when build does not have a trace file' do
+ let(:build) { create(:ci_build, pipeline: pipeline) }
+
+ it 'returns not_found' do
+ expect(response).to have_http_status(:not_found)
+ end
+ end
+
+ def get_raw
+ post :raw, namespace_id: project.namespace,
+ project_id: project,
+ id: build.id
+ end
+ end
end
diff --git a/spec/controllers/projects/builds_controller_specs.rb b/spec/controllers/projects/builds_controller_specs.rb
deleted file mode 100644
index d501f7b3155..00000000000
--- a/spec/controllers/projects/builds_controller_specs.rb
+++ /dev/null
@@ -1,47 +0,0 @@
-require 'spec_helper'
-
-describe Projects::BuildsController do
- include ApiHelpers
-
- let(:project) { create(:empty_project, :public) }
-
- describe 'GET trace.json' do
- let(:pipeline) { create(:ci_pipeline, project: project) }
- let(:build) { create(:ci_build, pipeline: pipeline) }
- let(:user) { create(:user) }
-
- context 'when user is logged in as developer' do
- before do
- project.add_developer(user)
- sign_in(user)
- get_trace
- end
-
- it 'traces build log' do
- expect(response).to have_http_status(:ok)
- expect(json_response['id']).to eq build.id
- expect(json_response['status']).to eq build.status
- end
- end
-
- context 'when user is logged in as non member' do
- before do
- sign_in(user)
- get_trace
- end
-
- it 'traces build log' do
- expect(response).to have_http_status(:ok)
- expect(json_response['id']).to eq build.id
- expect(json_response['status']).to eq build.status
- end
- end
-
- def get_trace
- get :trace, namespace_id: project.namespace,
- project_id: project,
- id: build.id,
- format: :json
- end
- end
-end
diff --git a/spec/controllers/projects/deploy_keys_controller_spec.rb b/spec/controllers/projects/deploy_keys_controller_spec.rb
new file mode 100644
index 00000000000..efe1a78415b
--- /dev/null
+++ b/spec/controllers/projects/deploy_keys_controller_spec.rb
@@ -0,0 +1,66 @@
+require 'spec_helper'
+
+describe Projects::DeployKeysController do
+ let(:project) { create(:project, :repository) }
+ let(:user) { create(:user) }
+
+ before do
+ project.team << [user, :master]
+
+ sign_in(user)
+ end
+
+ describe 'GET index' do
+ let(:params) do
+ { namespace_id: project.namespace, project_id: project }
+ end
+
+ context 'when html requested' do
+ it 'redirects to blob' do
+ get :index, params
+
+ expect(response).to redirect_to(namespace_project_settings_repository_path(params))
+ end
+ end
+
+ context 'when json requested' do
+ let(:project2) { create(:empty_project, :internal)}
+ let(:project_private) { create(:empty_project, :private)}
+
+ let(:deploy_key_internal) do
+ create(:deploy_key, key: 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQCdMHEHyhRjbhEZVddFn6lTWdgEy5Q6Bz4nwGB76xWZI5YT/1WJOMEW+sL5zYd31kk7sd3FJ5L9ft8zWMWrr/iWXQikC2cqZK24H1xy+ZUmrRuJD4qGAaIVoyyzBL+avL+lF8J5lg6YSw8gwJY/lX64/vnJHUlWw2n5BF8IFOWhiw== dummy@gitlab.com')
+ end
+ let(:deploy_key_actual) do
+ create(:deploy_key, key: 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQDNd/UJWhPrpb+b/G5oL109y57yKuCxE+WUGJGYaj7WQKsYRJmLYh1mgjrl+KVyfsWpq4ylOxIfFSnN9xBBFN8mlb0Fma5DC7YsSsibJr3MZ19ZNBprwNcdogET7aW9I0In7Wu5f2KqI6e5W/spJHCy4JVxzVMUvk6Myab0LnJ2iQ== dummy@gitlab.com')
+ end
+ let!(:deploy_key_public) { create(:deploy_key, public: true) }
+
+ let!(:deploy_keys_project_internal) do
+ create(:deploy_keys_project, project: project2, deploy_key: deploy_key_internal)
+ end
+
+ let!(:deploy_keys_actual_project) do
+ create(:deploy_keys_project, project: project, deploy_key: deploy_key_actual)
+ end
+
+ let!(:deploy_keys_project_private) do
+ create(:deploy_keys_project, project: project_private, deploy_key: create(:another_deploy_key))
+ end
+
+ before do
+ project2.team << [user, :developer]
+ end
+
+ it 'returns json in a correct format' do
+ get :index, params.merge(format: :json)
+
+ json = JSON.parse(response.body)
+
+ expect(json.keys).to match_array(%w(enabled_keys available_project_keys public_keys))
+ expect(json['enabled_keys'].count).to eq(1)
+ expect(json['available_project_keys'].count).to eq(1)
+ expect(json['public_keys'].count).to eq(1)
+ end
+ end
+ end
+end
diff --git a/spec/controllers/projects/deployments_controller_spec.rb b/spec/controllers/projects/deployments_controller_spec.rb
new file mode 100644
index 00000000000..4c69443314d
--- /dev/null
+++ b/spec/controllers/projects/deployments_controller_spec.rb
@@ -0,0 +1,116 @@
+require 'spec_helper'
+
+describe Projects::DeploymentsController do
+ include ApiHelpers
+
+ let(:user) { create(:user) }
+ let(:project) { create(:empty_project) }
+ let(:environment) { create(:environment, name: 'production', project: project) }
+
+ before do
+ project.team << [user, :master]
+
+ sign_in(user)
+ end
+
+ describe 'GET #index' do
+ it 'returns list of deployments from last 8 hours' do
+ create(:deployment, environment: environment, created_at: 9.hours.ago)
+ create(:deployment, environment: environment, created_at: 7.hours.ago)
+ create(:deployment, environment: environment)
+
+ get :index, deployment_params(after: 8.hours.ago)
+
+ expect(response).to be_ok
+
+ expect(json_response['deployments'].count).to eq(2)
+ end
+
+ it 'returns a list with deployments information' do
+ create(:deployment, environment: environment)
+
+ get :index, deployment_params
+
+ expect(response).to be_ok
+ expect(response).to match_response_schema('deployments')
+ end
+ end
+
+ describe 'GET #metrics' do
+ let(:deployment) { create(:deployment, project: project, environment: environment) }
+
+ before do
+ allow(controller).to receive(:deployment).and_return(deployment)
+ end
+ context 'when metrics are disabled' do
+ before do
+ allow(deployment).to receive(:has_metrics?).and_return false
+ end
+
+ it 'responds with not found' do
+ get :metrics, deployment_params(id: deployment.id)
+
+ expect(response).to be_not_found
+ end
+ end
+
+ context 'when metrics are enabled' do
+ before do
+ allow(deployment).to receive(:has_metrics?).and_return true
+ end
+
+ context 'when environment has no metrics' do
+ before do
+ expect(deployment).to receive(:metrics).and_return(nil)
+ end
+
+ it 'returns a empty response 204 resposne' do
+ get :metrics, deployment_params(id: deployment.id)
+ expect(response).to have_http_status(204)
+ expect(response.body).to eq('')
+ end
+ end
+
+ context 'when environment has some metrics' do
+ let(:empty_metrics) do
+ {
+ success: true,
+ metrics: {},
+ last_update: 42
+ }
+ end
+
+ before do
+ expect(deployment).to receive(:metrics).and_return(empty_metrics)
+ end
+
+ it 'returns a metrics JSON document' do
+ get :metrics, deployment_params(id: deployment.id)
+
+ expect(response).to be_ok
+ expect(json_response['success']).to be(true)
+ expect(json_response['metrics']).to eq({})
+ expect(json_response['last_update']).to eq(42)
+ end
+ end
+
+ context 'when metrics service does not implement deployment metrics' do
+ before do
+ allow(deployment).to receive(:metrics).and_raise(NotImplementedError)
+ end
+
+ it 'responds with not found' do
+ get :metrics, deployment_params(id: deployment.id)
+
+ expect(response).to be_not_found
+ end
+ end
+ end
+ end
+
+ def deployment_params(opts = {})
+ opts.reverse_merge(namespace_id: project.namespace,
+ project_id: project,
+ environment_id: environment.id)
+ end
+end
diff --git a/spec/controllers/projects/environments_controller_spec.rb b/spec/controllers/projects/environments_controller_spec.rb
index 5525fbd8130..c0f8c36a018 100644
--- a/spec/controllers/projects/environments_controller_spec.rb
+++ b/spec/controllers/projects/environments_controller_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
describe Projects::EnvironmentsController do
- include ApiHelpers
-
let(:user) { create(:user) }
let(:project) { create(:empty_project) }
@@ -151,6 +149,48 @@ describe Projects::EnvironmentsController do
end
end
+ describe 'PATCH #stop' do
+ context 'when env not available' do
+ it 'returns 404' do
+ allow_any_instance_of(Environment).to receive(:available?) { false }
+
+ patch :stop, environment_params(format: :json)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ context 'when stop action' do
+ it 'returns action url' do
+ action = create(:ci_build, :manual)
+
+ allow_any_instance_of(Environment)
+ .to receive_messages(available?: true, stop_with_action!: action)
+
+ patch :stop, environment_params(format: :json)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to eq(
+ { 'redirect_url' =>
+ "http://test.host/#{project.path_with_namespace}/builds/#{action.id}" })
+ end
+ end
+
+ context 'when no stop action' do
+ it 'returns env url' do
+ allow_any_instance_of(Environment)
+ .to receive_messages(available?: true, stop_with_action!: nil)
+
+ patch :stop, environment_params(format: :json)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to eq(
+ { 'redirect_url' =>
+ "http://test.host/#{project.path_with_namespace}/environments/#{environment.id}" })
+ end
+ end
+ end
+
describe 'GET #terminal' do
context 'with valid id' do
it 'responds with a status code 200' do
diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb
index 79034b8d24d..1f79e72495a 100644
--- a/spec/controllers/projects/issues_controller_spec.rb
+++ b/spec/controllers/projects/issues_controller_spec.rb
@@ -173,12 +173,12 @@ describe Projects::IssuesController do
namespace_id: project.namespace.to_param,
project_id: project,
id: issue.iid,
- issue: { assignee_id: assignee.id },
+ issue: { assignee_ids: [assignee.id] },
format: :json
body = JSON.parse(response.body)
- expect(body['assignee'].keys)
- .to match_array(%w(name username avatar_url))
+ expect(body['assignees'].first.keys)
+ .to match_array(%w(id name username avatar_url))
end
end
@@ -348,7 +348,7 @@ describe Projects::IssuesController do
let(:admin) { create(:admin) }
let!(:issue) { create(:issue, project: project) }
let!(:unescaped_parameter_value) { create(:issue, :confidential, project: project, author: author) }
- let!(:request_forgery_timing_attack) { create(:issue, :confidential, project: project, assignee: assignee) }
+ let!(:request_forgery_timing_attack) { create(:issue, :confidential, project: project, assignees: [assignee]) }
describe 'GET #index' do
it 'does not list confidential issues for guests' do
@@ -756,4 +756,28 @@ describe Projects::IssuesController do
expect(response).to have_http_status(200)
end
end
+
+ describe 'POST create_merge_request' do
+ before do
+ project.add_developer(user)
+ sign_in(user)
+ end
+
+ it 'creates a new merge request' do
+ expect { create_merge_request }.to change(project.merge_requests, :count).by(1)
+ end
+
+ it 'render merge request as json' do
+ create_merge_request
+
+ expect(response).to match_response_schema('merge_request')
+ end
+
+ def create_merge_request
+ post :create_merge_request, namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ id: issue.to_param,
+ format: :json
+ end
+ end
end
diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb
index cc393bd24f2..0b3492a8fed 100644
--- a/spec/controllers/projects/merge_requests_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests_controller_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
describe Projects::MergeRequestsController do
- include ApiHelpers
-
let(:project) { create(:project) }
let(:user) { create(:user) }
let(:merge_request) { create(:merge_request_with_diffs, target_project: project, source_project: project) }
@@ -61,6 +59,18 @@ describe Projects::MergeRequestsController do
end
end
+ describe 'GET commit_change_content' do
+ it 'renders commit_change_content template' do
+ get :commit_change_content,
+ namespace_id: project.namespace.to_param,
+ project_id: project,
+ id: merge_request.iid,
+ format: 'html'
+
+ expect(response).to render_template('_commit_change_content')
+ end
+ end
+
shared_examples "loads labels" do |action|
it "loads labels into the @labels variable" do
get action,
@@ -73,63 +83,47 @@ describe Projects::MergeRequestsController do
end
describe "GET show" do
- shared_examples "export merge as" do |format|
- it "does generally work" do
- get(:show,
- namespace_id: project.namespace.to_param,
- project_id: project,
- id: merge_request.iid,
- format: format)
+ def go(extra_params = {})
+ params = {
+ namespace_id: project.namespace.to_param,
+ project_id: project,
+ id: merge_request.iid
+ }
- expect(response).to be_success
- end
+ get :show, params.merge(extra_params)
+ end
- it_behaves_like "loads labels", :show
+ it_behaves_like "loads labels", :show
- it "generates it" do
- expect_any_instance_of(MergeRequest).to receive(:"to_#{format}")
+ describe 'as html' do
+ it "renders merge request page" do
+ go(format: :html)
- get(:show,
- namespace_id: project.namespace.to_param,
- project_id: project,
- id: merge_request.iid,
- format: format)
+ expect(response).to be_success
end
+ end
- it "renders it" do
- get(:show,
- namespace_id: project.namespace.to_param,
- project_id: project,
- id: merge_request.iid,
- format: format)
+ describe 'as json' do
+ context 'with basic param' do
+ it 'renders basic MR entity as json' do
+ go(basic: true, format: :json)
- expect(response.body).to eq(merge_request.send(:"to_#{format}").to_s)
+ expect(response).to match_response_schema('entities/merge_request_basic')
+ end
end
- it "does not escape Html" do
- allow_any_instance_of(MergeRequest).to receive(:"to_#{format}").
- and_return('HTML entities &<>" ')
+ context 'without basic param' do
+ it 'renders the merge request in the json format' do
+ go(format: :json)
- get(:show,
- namespace_id: project.namespace.to_param,
- project_id: project,
- id: merge_request.iid,
- 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;')
+ expect(response).to match_response_schema('entities/merge_request')
+ end
end
end
describe "as diff" do
it "triggers workhorse to serve the request" do
- get(:show,
- namespace_id: project.namespace.to_param,
- project_id: project,
- id: merge_request.iid,
- format: :diff)
+ go(format: :diff)
expect(response.headers[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with("git-diff:")
end
@@ -137,11 +131,7 @@ describe Projects::MergeRequestsController do
describe "as patch" do
it 'triggers workhorse to serve the request' do
- get(:show,
- namespace_id: project.namespace.to_param,
- project_id: project,
- id: merge_request.iid,
- format: :patch)
+ go(format: :patch)
expect(response.headers[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with("git-format-patch:")
end
@@ -297,19 +287,18 @@ describe Projects::MergeRequestsController do
namespace_id: project.namespace,
project_id: project,
id: merge_request.iid,
- format: 'raw'
+ format: 'json'
}
end
- context 'when the user does not have access' do
+ context 'when user cannot access' do
before do
- project.team.truncate
- project.team << [user, :reporter]
- post :merge, base_params
+ project.add_reporter(user)
+ xhr :post, :merge, base_params
end
- it 'returns not found' do
- expect(response).to be_not_found
+ it 'returns 404' do
+ expect(response).to have_http_status(404)
end
end
@@ -321,7 +310,7 @@ describe Projects::MergeRequestsController do
end
it 'returns :failed' do
- expect(assigns(:status)).to eq(:failed)
+ expect(json_response).to eq('status' => 'failed')
end
end
@@ -329,7 +318,7 @@ describe Projects::MergeRequestsController do
before { post :merge, base_params.merge(sha: 'foo') }
it 'returns :sha_mismatch' do
- expect(assigns(:status)).to eq(:sha_mismatch)
+ expect(json_response).to eq('status' => 'sha_mismatch')
end
end
@@ -341,7 +330,7 @@ describe Projects::MergeRequestsController do
it 'returns :success' do
merge_with_sha
- expect(assigns(:status)).to eq(:success)
+ expect(json_response).to eq('status' => 'success')
end
it 'starts the merge immediately' do
@@ -356,13 +345,14 @@ describe Projects::MergeRequestsController do
end
before do
- create(:ci_empty_pipeline, project: project, sha: merge_request.diff_head_sha, ref: merge_request.source_branch)
+ pipeline = create(:ci_empty_pipeline, project: project, sha: merge_request.diff_head_sha, ref: merge_request.source_branch)
+ merge_request.update(head_pipeline: pipeline)
end
it 'returns :merge_when_pipeline_succeeds' do
merge_when_pipeline_succeeds
- expect(assigns(:status)).to eq(:merge_when_pipeline_succeeds)
+ expect(json_response).to eq('status' => 'merge_when_pipeline_succeeds')
end
it 'sets the MR to merge when the pipeline succeeds' do
@@ -384,7 +374,7 @@ describe Projects::MergeRequestsController do
it 'returns :merge_when_pipeline_succeeds' do
merge_when_pipeline_succeeds
- expect(assigns(:status)).to eq(:merge_when_pipeline_succeeds)
+ expect(json_response).to eq('status' => 'merge_when_pipeline_succeeds')
end
end
end
@@ -405,7 +395,7 @@ describe Projects::MergeRequestsController do
it 'returns :failed' do
merge_with_sha
- expect(assigns(:status)).to eq(:failed)
+ expect(json_response).to eq('status' => 'failed')
end
end
@@ -418,7 +408,7 @@ describe Projects::MergeRequestsController do
it 'returns :success' do
merge_with_sha
- expect(assigns(:status)).to eq(:success)
+ expect(json_response).to eq('status' => 'success')
end
end
end
@@ -436,7 +426,7 @@ describe Projects::MergeRequestsController do
it 'returns :success' do
merge_with_sha
- expect(assigns(:status)).to eq(:success)
+ expect(json_response).to eq('status' => 'success')
end
end
@@ -449,7 +439,7 @@ describe Projects::MergeRequestsController do
it 'returns :success' do
merge_with_sha
- expect(assigns(:status)).to eq(:success)
+ expect(json_response).to eq('status' => 'success')
end
end
end
@@ -833,18 +823,55 @@ describe Projects::MergeRequestsController do
end
end
- context 'POST remove_wip' do
- it 'removes the wip status' do
+ describe 'POST remove_wip' do
+ before do
merge_request.title = merge_request.wip_title
merge_request.save
- post :remove_wip,
- namespace_id: merge_request.project.namespace.to_param,
- project_id: merge_request.project,
- id: merge_request.iid
+ xhr :post, :remove_wip,
+ namespace_id: merge_request.project.namespace.to_param,
+ project_id: merge_request.project,
+ id: merge_request.iid,
+ format: :json
+ end
+ it 'removes the wip status' do
expect(merge_request.reload.title).to eq(merge_request.wipless_title)
end
+
+ it 'renders MergeRequest as JSON' do
+ expect(json_response.keys).to include('id', 'iid', 'description')
+ end
+ end
+
+ describe 'POST cancel_merge_when_pipeline_succeeds' do
+ subject do
+ xhr :post, :cancel_merge_when_pipeline_succeeds,
+ namespace_id: merge_request.project.namespace.to_param,
+ project_id: merge_request.project,
+ id: merge_request.iid,
+ format: :json
+ end
+
+ it 'calls MergeRequests::MergeWhenPipelineSucceedsService' do
+ mwps_service = double
+
+ allow(MergeRequests::MergeWhenPipelineSucceedsService)
+ .to receive(:new)
+ .and_return(mwps_service)
+
+ expect(mwps_service).to receive(:cancel).with(merge_request)
+
+ subject
+ end
+
+ it { is_expected.to have_http_status(:success) }
+
+ it 'renders MergeRequest as JSON' do
+ subject
+
+ expect(json_response.keys).to include('id', 'iid', 'description')
+ end
end
describe 'GET conflict_for_path' do
@@ -889,7 +916,9 @@ describe Projects::MergeRequestsController do
end
it 'returns the file in JSON format' do
- content = merge_request_with_conflicts.conflicts.file_for_path(path, path).content
+ content = MergeRequests::Conflicts::ListService.new(merge_request_with_conflicts).
+ file_for_path(path, path).
+ content
expect(json_response).to include('old_path' => path,
'new_path' => path,
@@ -1013,11 +1042,15 @@ describe Projects::MergeRequestsController do
context 'when a file has identical content to the conflict' do
before do
+ content = MergeRequests::Conflicts::ListService.new(merge_request_with_conflicts).
+ file_for_path('files/ruby/popen.rb', 'files/ruby/popen.rb').
+ content
+
resolved_files = [
{
'new_path' => 'files/ruby/popen.rb',
'old_path' => 'files/ruby/popen.rb',
- 'content' => merge_request_with_conflicts.conflicts.file_for_path('files/ruby/popen.rb', 'files/ruby/popen.rb').content
+ 'content' => content
}, {
'new_path' => 'files/ruby/regex.rb',
'old_path' => 'files/ruby/regex.rb',
@@ -1069,7 +1102,7 @@ describe Projects::MergeRequestsController do
end
it 'correctly pluralizes flash message on success' do
- issue2.update!(assignee: user)
+ issue2.assignees = [user]
post_assign_issues
@@ -1123,74 +1156,6 @@ describe Projects::MergeRequestsController do
end
end
- describe 'GET merge_widget_refresh' do
- let(:params) do
- {
- namespace_id: project.namespace,
- project_id: project,
- id: merge_request.iid,
- format: :raw
- }
- end
-
- before do
- project.team << [user, :developer]
- xhr :get, :merge_widget_refresh, params
- end
-
- context 'when merge in progress' do
- let(:merge_request) { create(:merge_request, source_project: project, in_progress_merge_commit_sha: 'sha') }
-
- it 'returns an OK response' do
- expect(response).to have_http_status(:ok)
- end
-
- it 'sets status to :success' do
- expect(assigns(:status)).to eq(:success)
- expect(response).to render_template('merge')
- end
- end
-
- context 'when merge request was merged already' do
- let(:merge_request) { create(:merge_request, source_project: project, state: :merged) }
-
- it 'returns an OK response' do
- expect(response).to have_http_status(:ok)
- end
-
- it 'sets status to :success' do
- expect(assigns(:status)).to eq(:success)
- expect(response).to render_template('merge')
- end
- end
-
- context 'when waiting for build' do
- let(:merge_request) { create(:merge_request, source_project: project, merge_when_pipeline_succeeds: true, merge_user: user) }
-
- it 'returns an OK response' do
- expect(response).to have_http_status(:ok)
- end
-
- it 'sets status to :merge_when_pipeline_succeeds' do
- expect(assigns(:status)).to eq(:merge_when_pipeline_succeeds)
- expect(response).to render_template('merge')
- end
- end
-
- context 'when MR does not have special state' do
- let(:merge_request) { create(:merge_request, source_project: project) }
-
- it 'returns an OK response' do
- expect(response).to have_http_status(:ok)
- end
-
- it 'sets status to success' do
- expect(assigns(:status)).to eq(:success)
- expect(response).to render_template('merge')
- end
- end
- end
-
describe 'GET pipeline_status.json' do
context 'when head_pipeline exists' do
let!(:pipeline) do
@@ -1201,7 +1166,10 @@ describe Projects::MergeRequestsController do
let(:status) { pipeline.detailed_status(double('user')) }
- before { get_pipeline_status }
+ before do
+ merge_request.update(head_pipeline: pipeline)
+ get_pipeline_status
+ end
it 'return a detailed head_pipeline status in json' do
expect(response).to have_http_status(:ok)
diff --git a/spec/controllers/projects/milestones_controller_spec.rb b/spec/controllers/projects/milestones_controller_spec.rb
index 14207bf6b7a..84a61b2784e 100644
--- a/spec/controllers/projects/milestones_controller_spec.rb
+++ b/spec/controllers/projects/milestones_controller_spec.rb
@@ -5,7 +5,9 @@ describe Projects::MilestonesController do
let(:user) { create(:user) }
let(:milestone) { create(:milestone, project: project) }
let(:issue) { create(:issue, project: project, milestone: milestone) }
+ let!(:label) { create(:label, project: project, title: 'Issue Label', issues: [issue]) }
let!(:merge_request) { create(:merge_request, source_project: project, target_project: project, milestone: milestone) }
+ let(:milestone_path) { namespace_project_milestone_path }
before do
sign_in(user)
@@ -13,6 +15,22 @@ describe Projects::MilestonesController do
controller.instance_variable_set(:@project, project)
end
+ it_behaves_like 'milestone tabs'
+
+ describe "#show" do
+ render_views
+
+ def view_milestone
+ get :show, namespace_id: project.namespace.id, project_id: project.id, id: milestone.iid
+ end
+
+ it 'shows milestone page' do
+ view_milestone
+
+ expect(response).to have_http_status(200)
+ end
+ end
+
describe "#destroy" do
it "removes milestone" do
expect(issue.milestone_id).to eq(milestone.id)
diff --git a/spec/controllers/projects/notes_controller_spec.rb b/spec/controllers/projects/notes_controller_spec.rb
index f140eaef5d5..45f4cf9180d 100644
--- a/spec/controllers/projects/notes_controller_spec.rb
+++ b/spec/controllers/projects/notes_controller_spec.rb
@@ -167,6 +167,47 @@ describe Projects::NotesController do
end
end
+ describe 'DELETE destroy' do
+ let(:request_params) do
+ {
+ namespace_id: project.namespace,
+ project_id: project,
+ id: note,
+ format: :js
+ }
+ end
+
+ context 'user is the author of a note' do
+ before do
+ sign_in(note.author)
+ project.team << [note.author, :developer]
+ end
+
+ it "returns status 200 for html" do
+ delete :destroy, request_params
+
+ expect(response).to have_http_status(200)
+ end
+
+ it "deletes the note" do
+ expect { delete :destroy, request_params }.to change { Note.count }.from(1).to(0)
+ end
+ end
+
+ context 'user is not the author of a note' do
+ before do
+ sign_in(user)
+ project.team << [user, :developer]
+ end
+
+ it "returns status 404" do
+ delete :destroy, request_params
+
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
+
describe 'POST toggle_award_emoji' do
before do
sign_in(user)
diff --git a/spec/controllers/projects/pages_controller_spec.rb b/spec/controllers/projects/pages_controller_spec.rb
new file mode 100644
index 00000000000..df35d8e86b9
--- /dev/null
+++ b/spec/controllers/projects/pages_controller_spec.rb
@@ -0,0 +1,57 @@
+require 'spec_helper'
+
+describe Projects::PagesController do
+ let(:user) { create(:user) }
+ let(:project) { create(:empty_project, :public, :access_requestable) }
+
+ let(:request_params) do
+ {
+ namespace_id: project.namespace,
+ project_id: project
+ }
+ end
+
+ before do
+ allow(Gitlab.config.pages).to receive(:enabled).and_return(true)
+ sign_in(user)
+ project.add_master(user)
+ end
+
+ describe 'GET show' do
+ it 'returns 200 status' do
+ get :show, request_params
+
+ expect(response).to have_http_status(200)
+ end
+ end
+
+ describe 'DELETE destroy' do
+ it 'returns 302 status' do
+ delete :destroy, request_params
+
+ expect(response).to have_http_status(302)
+ end
+ end
+
+ context 'pages disabled' do
+ before do
+ allow(Gitlab.config.pages).to receive(:enabled).and_return(false)
+ end
+
+ describe 'GET show' do
+ it 'returns 404 status' do
+ get :show, request_params
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ describe 'DELETE destroy' do
+ it 'returns 404 status' do
+ delete :destroy, request_params
+
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
+end
diff --git a/spec/controllers/projects/pages_domains_controller_spec.rb b/spec/controllers/projects/pages_domains_controller_spec.rb
index 2362df895a8..33853c4b9d0 100644
--- a/spec/controllers/projects/pages_domains_controller_spec.rb
+++ b/spec/controllers/projects/pages_domains_controller_spec.rb
@@ -1,8 +1,9 @@
require 'spec_helper'
describe Projects::PagesDomainsController do
- let(:user) { create(:user) }
- let(:project) { create(:project) }
+ let(:user) { create(:user) }
+ let(:project) { create(:empty_project) }
+ let!(:pages_domain) { create(:pages_domain, project: project) }
let(:request_params) do
{
@@ -11,14 +12,17 @@ describe Projects::PagesDomainsController do
}
end
+ let(:pages_domain_params) do
+ build(:pages_domain, :with_certificate, :with_key, domain: 'my.otherdomain.com').slice(:key, :certificate, :domain)
+ end
+
before do
+ allow(Gitlab.config.pages).to receive(:enabled).and_return(true)
sign_in(user)
- project.team << [user, :master]
+ project.add_master(user)
end
describe 'GET show' do
- let!(:pages_domain) { create(:pages_domain, project: project) }
-
it "displays the 'show' page" do
get(:show, request_params.merge(id: pages_domain.domain))
@@ -37,10 +41,6 @@ describe Projects::PagesDomainsController do
end
describe 'POST create' do
- let(:pages_domain_params) do
- build(:pages_domain, :with_certificate, :with_key).slice(:key, :certificate, :domain)
- end
-
it "creates a new pages domain" do
expect do
post(:create, request_params.merge(pages_domain: pages_domain_params))
@@ -51,8 +51,6 @@ describe Projects::PagesDomainsController do
end
describe 'DELETE destroy' do
- let!(:pages_domain) { create(:pages_domain, project: project) }
-
it "deletes the pages domain" do
expect do
delete(:destroy, request_params.merge(id: pages_domain.domain))
@@ -61,4 +59,42 @@ describe Projects::PagesDomainsController do
expect(response).to redirect_to(namespace_project_pages_path(project.namespace, project))
end
end
+
+ context 'pages disabled' do
+ before do
+ allow(Gitlab.config.pages).to receive(:enabled).and_return(false)
+ end
+
+ describe 'GET show' do
+ it 'returns 404 status' do
+ get(:show, request_params.merge(id: pages_domain.domain))
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ describe 'GET new' do
+ it 'returns 404 status' do
+ get :new, request_params
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ describe 'POST create' do
+ it "returns 404 status" do
+ post(:create, request_params.merge(pages_domain: pages_domain_params))
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ describe 'DELETE destroy' do
+ it "deletes the pages domain" do
+ delete(:destroy, request_params.merge(id: pages_domain.domain))
+
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
end
diff --git a/spec/controllers/projects/pipeline_schedules_controller_spec.rb b/spec/controllers/projects/pipeline_schedules_controller_spec.rb
new file mode 100644
index 00000000000..f8f95dd9bc8
--- /dev/null
+++ b/spec/controllers/projects/pipeline_schedules_controller_spec.rb
@@ -0,0 +1,87 @@
+require 'spec_helper'
+
+describe Projects::PipelineSchedulesController do
+ set(:project) { create(:empty_project, :public) }
+ let!(:pipeline_schedule) { create(:ci_pipeline_schedule, project: project) }
+
+ describe 'GET #index' do
+ let(:scope) { nil }
+ let!(:inactive_pipeline_schedule) do
+ create(:ci_pipeline_schedule, :inactive, project: project)
+ end
+
+ it 'renders the index view' do
+ visit_pipelines_schedules
+
+ expect(response).to have_http_status(:ok)
+ expect(response).to render_template(:index)
+ end
+
+ context 'when the scope is set to active' do
+ let(:scope) { 'active' }
+
+ before do
+ visit_pipelines_schedules
+ end
+
+ it 'only shows active pipeline schedules' do
+ expect(response).to have_http_status(:ok)
+ expect(assigns(:schedules)).to include(pipeline_schedule)
+ expect(assigns(:schedules)).not_to include(inactive_pipeline_schedule)
+ end
+ end
+
+ def visit_pipelines_schedules
+ get :index, namespace_id: project.namespace.to_param, project_id: project, scope: scope
+ end
+ end
+
+ describe 'GET edit' do
+ let(:user) { create(:user) }
+
+ before do
+ project.add_master(user)
+
+ sign_in(user)
+ end
+
+ it 'loads the pipeline schedule' do
+ get :edit, namespace_id: project.namespace.to_param, project_id: project, id: pipeline_schedule.id
+
+ expect(response).to have_http_status(:ok)
+ expect(assigns(:schedule)).to eq(pipeline_schedule)
+ end
+ end
+
+ describe 'DELETE #destroy' do
+ set(:user) { create(:user) }
+
+ context 'when a developer makes the request' do
+ before do
+ project.add_developer(user)
+ sign_in(user)
+
+ delete :destroy, namespace_id: project.namespace.to_param, project_id: project, id: pipeline_schedule.id
+ end
+
+ it 'does not delete the pipeline schedule' do
+ expect(response).not_to have_http_status(:ok)
+ end
+ end
+
+ context 'when a master makes the request' do
+ before do
+ project.add_master(user)
+ sign_in(user)
+ end
+
+ it 'destroys the pipeline schedule' do
+ expect do
+ delete :destroy, namespace_id: project.namespace.to_param, project_id: project, id: pipeline_schedule.id
+ end.to change { project.pipeline_schedules.count }.by(-1)
+
+ expect(response).to have_http_status(302)
+ end
+ end
+ end
+end
diff --git a/spec/controllers/projects/pipelines_controller_spec.rb b/spec/controllers/projects/pipelines_controller_spec.rb
index d9192177a06..fb4a4721a58 100644
--- a/spec/controllers/projects/pipelines_controller_spec.rb
+++ b/spec/controllers/projects/pipelines_controller_spec.rb
@@ -7,6 +7,8 @@ describe Projects::PipelinesController do
let(:project) { create(:empty_project, :public) }
before do
+ project.add_developer(user)
+
sign_in(user)
end
@@ -24,6 +26,7 @@ describe Projects::PipelinesController do
it 'returns JSON with serialized pipelines' do
expect(response).to have_http_status(:ok)
+ expect(response).to match_response_schema('pipeline')
expect(json_response).to include('pipelines')
expect(json_response['pipelines'].count).to eq 4
@@ -34,6 +37,34 @@ describe Projects::PipelinesController do
end
end
+ describe 'GET show JSON' do
+ let!(:pipeline) { create(:ci_pipeline_with_one_job, project: project) }
+
+ it 'returns the pipeline' do
+ get_pipeline_json
+
+ expect(response).to have_http_status(:ok)
+ expect(json_response).not_to be_an(Array)
+ expect(json_response['id']).to be(pipeline.id)
+ expect(json_response['details']).to have_key 'stages'
+ end
+
+ context 'when the pipeline has multiple jobs' do
+ it 'does not perform N + 1 queries' do
+ control_count = ActiveRecord::QueryRecorder.new { get_pipeline_json }.count
+
+ create(:ci_build, pipeline: pipeline)
+
+ # The plus 2 is needed to group and sort
+ expect { get_pipeline_json }.not_to exceed_query_limit(control_count + 2)
+ end
+ end
+
+ def get_pipeline_json
+ get :show, namespace_id: project.namespace, project_id: project, id: pipeline, format: :json
+ end
+ end
+
describe 'GET stages.json' do
let(:pipeline) { create(:ci_pipeline, project: project) }
@@ -89,4 +120,38 @@ describe Projects::PipelinesController do
expect(json_response['favicon']).to eq "/assets/ci_favicons/#{status.favicon}.ico"
end
end
+
+ describe 'POST retry.json' do
+ let!(:pipeline) { create(:ci_pipeline, :failed, project: project) }
+ let!(:build) { create(:ci_build, :failed, pipeline: pipeline) }
+
+ before do
+ post :retry, namespace_id: project.namespace,
+ project_id: project,
+ id: pipeline.id,
+ format: :json
+ end
+
+ it 'retries a pipeline without returning any content' do
+ expect(response).to have_http_status(:no_content)
+ expect(build.reload).to be_retried
+ end
+ end
+
+ describe 'POST cancel.json' do
+ let!(:pipeline) { create(:ci_pipeline, project: project) }
+ let!(:build) { create(:ci_build, :running, pipeline: pipeline) }
+
+ before do
+ post :cancel, namespace_id: project.namespace,
+ project_id: project,
+ id: pipeline.id,
+ format: :json
+ end
+
+ it 'cancels a pipeline without returning any content' do
+ expect(response).to have_http_status(:no_content)
+ expect(pipeline.reload).to be_canceled
+ end
+ end
end
diff --git a/spec/controllers/projects/services_controller_spec.rb b/spec/controllers/projects/services_controller_spec.rb
index 16365642a34..2d892f4a2b7 100644
--- a/spec/controllers/projects/services_controller_spec.rb
+++ b/spec/controllers/projects/services_controller_spec.rb
@@ -8,6 +8,7 @@ describe Projects::ServicesController do
before do
sign_in(user)
project.team << [user, :master]
+
controller.instance_variable_set(:@project, project)
controller.instance_variable_set(:@service, service)
end
@@ -18,20 +19,60 @@ describe Projects::ServicesController do
end
describe "#test" do
+ context 'when can_test? returns false' do
+ it 'renders 404' do
+ allow_any_instance_of(Service).to receive(:can_test?).and_return(false)
+
+ get :test, namespace_id: project.namespace.id, project_id: project.id, id: service.id, format: :html
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
context 'success' do
+ context 'with empty project' do
+ let(:project) { create(:empty_project) }
+
+ context 'with chat notification service' do
+ let(:service) { project.create_microsoft_teams_service(webhook: 'http://webhook.com') }
+
+ it 'redirects and show success message' do
+ allow_any_instance_of(MicrosoftTeams::Notifier).to receive(:ping).and_return(true)
+
+ get :test, namespace_id: project.namespace.id, project_id: project.id, id: service.id, format: :html
+
+ expect(response).to redirect_to(root_path)
+ expect(flash[:notice]).to eq('We sent a request to the provided URL')
+ end
+ end
+
+ 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).to redirect_to(root_path)
+ expect(flash[:notice]).to eq('We sent a request to the provided URL')
+ end
+ end
+
it "redirects and show success message" do
- expect(service).to receive(:test).and_return({ success: true, result: 'done' })
+ 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('/')
+
+ expect(response).to redirect_to(root_path)
expect(flash[:notice]).to eq('We sent a request to the provided URL')
end
end
context 'failure' do
it "redirects and show failure message" do
- expect(service).to receive(:test).and_return({ success: false, result: 'Bad test' })
+ 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('/')
+
+ expect(response).to redirect_to(root_path)
expect(flash[:alert]).to eq('We tried to send a request to the provided URL but an error occurred: Bad test')
end
end
diff --git a/spec/controllers/projects/todo_controller_spec.rb b/spec/controllers/projects/todos_controller_spec.rb
index 9a7beeff6fe..c5a4153d991 100644
--- a/spec/controllers/projects/todo_controller_spec.rb
+++ b/spec/controllers/projects/todos_controller_spec.rb
@@ -1,8 +1,6 @@
require('spec_helper')
describe Projects::TodosController do
- include ApiHelpers
-
let(:user) { create(:user) }
let(:project) { create(:empty_project) }
let(:issue) { create(:issue, project: project) }
diff --git a/spec/controllers/projects/wikis_controller_spec.rb b/spec/controllers/projects/wikis_controller_spec.rb
new file mode 100644
index 00000000000..92addf30307
--- /dev/null
+++ b/spec/controllers/projects/wikis_controller_spec.rb
@@ -0,0 +1,16 @@
+require 'spec_helper'
+
+describe Projects::WikisController do
+ let(:project) { create(:project_empty_repo, :public) }
+ let(:user) { create(:user) }
+
+ describe 'POST #preview_markdown' do
+ it 'renders json in a correct format' do
+ sign_in(user)
+
+ post :preview_markdown, namespace_id: project.namespace, project_id: project, id: 'page/path', text: '*Markdown* text'
+
+ expect(JSON.parse(response.body).keys).to match_array(%w(body references))
+ end
+ end
+end
diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb
index a88ffc1ea6a..e230944d52e 100644
--- a/spec/controllers/projects_controller_spec.rb
+++ b/spec/controllers/projects_controller_spec.rb
@@ -185,6 +185,7 @@ describe ProjectsController do
expect(assigns(:project)).to eq(public_project)
expect(response).to redirect_to("/#{public_project.full_path}")
+ expect(controller).not_to set_flash[:notice]
end
end
end
@@ -218,19 +219,33 @@ describe ProjectsController do
expect(response).to redirect_to(namespace_project_path)
end
end
+
+ context 'when requesting a redirected path' do
+ let!(:redirect_route) { public_project.redirect_routes.create!(path: "foo/bar") }
+
+ it 'redirects to the canonical path' do
+ get :show, namespace_id: 'foo', id: 'bar'
+
+ expect(response).to redirect_to(public_project)
+ expect(controller).to set_flash[:notice].to(project_moved_message(redirect_route, public_project))
+ end
+ end
end
describe "#update" do
render_views
let(:admin) { create(:admin) }
+ let(:project) { create(:project, :repository) }
+ let(:new_path) { 'renamed_path' }
+ let(:project_params) { { path: new_path } }
+
+ before do
+ sign_in(admin)
+ end
it "sets the repository to the right path after a rename" do
- project = create(:project, :repository)
- new_path = 'renamed_path'
- project_params = { path: new_path }
controller.instance_variable_set(:@project, project)
- sign_in(admin)
put :update,
namespace_id: project.namespace,
@@ -241,6 +256,34 @@ describe ProjectsController do
expect(assigns(:repository).path).to eq(project.repository.path)
expect(response).to have_http_status(302)
end
+
+ context 'when requesting the canonical path' do
+ it "is case-insensitive" do
+ controller.instance_variable_set(:@project, project)
+
+ put :update,
+ namespace_id: 'FOo',
+ id: 'baR',
+ project: project_params
+
+ expect(project.repository.path).to include(new_path)
+ expect(assigns(:repository).path).to eq(project.repository.path)
+ expect(response).to have_http_status(302)
+ end
+ end
+
+ context 'when requesting a redirected path' do
+ let!(:redirect_route) { project.redirect_routes.create!(path: "foo/bar") }
+
+ it 'returns not found' do
+ put :update,
+ namespace_id: 'foo',
+ id: 'bar',
+ project: project_params
+
+ expect(response).to have_http_status(404)
+ end
+ end
end
describe "#destroy" do
@@ -276,6 +319,31 @@ describe ProjectsController do
expect(merge_request.reload.state).to eq('closed')
end
end
+
+ context 'when requesting the canonical path' do
+ it "is case-insensitive" do
+ controller.instance_variable_set(:@project, project)
+ sign_in(admin)
+
+ orig_id = project.id
+ delete :destroy, namespace_id: project.namespace, id: project.path.upcase
+
+ expect { Project.find(orig_id) }.to raise_error(ActiveRecord::RecordNotFound)
+ expect(response).to have_http_status(302)
+ expect(response).to redirect_to(dashboard_projects_path)
+ end
+ end
+
+ context 'when requesting a redirected path' do
+ let!(:redirect_route) { project.redirect_routes.create!(path: "foo/bar") }
+
+ it 'returns not found' do
+ sign_in(admin)
+ delete :destroy, namespace_id: 'foo', id: 'bar'
+
+ expect(response).to have_http_status(404)
+ end
+ end
end
describe 'PUT #new_issue_address' do
@@ -397,5 +465,30 @@ describe ProjectsController do
expect(parsed_body["Tags"]).to include("v1.0.0")
expect(parsed_body["Commits"]).to include("123456")
end
+
+ context 'when requesting a redirected path' do
+ let!(:redirect_route) { public_project.redirect_routes.create!(path: "foo/bar") }
+
+ it 'redirects to the canonical path' do
+ get :refs, namespace_id: 'foo', id: 'bar'
+
+ expect(response).to redirect_to(refs_namespace_project_path(namespace_id: public_project.namespace, id: public_project))
+ expect(controller).to set_flash[:notice].to(project_moved_message(redirect_route, public_project))
+ end
+ end
+ end
+
+ describe 'POST #preview_markdown' do
+ it 'renders json in a correct format' do
+ sign_in(user)
+
+ post :preview_markdown, namespace_id: public_project.namespace, id: public_project, text: '*Markdown* text'
+
+ expect(JSON.parse(response.body).keys).to match_array(%w(body references))
+ end
+ end
+
+ def project_moved_message(redirect_route, project)
+ "Project '#{redirect_route.path}' was moved to '#{project.full_path}'. Please update any links and bookmarks that may still have the old path."
end
end
diff --git a/spec/controllers/snippets/notes_controller_spec.rb b/spec/controllers/snippets/notes_controller_spec.rb
new file mode 100644
index 00000000000..1c494b8c7ab
--- /dev/null
+++ b/spec/controllers/snippets/notes_controller_spec.rb
@@ -0,0 +1,196 @@
+require 'spec_helper'
+
+describe Snippets::NotesController do
+ let(:user) { create(:user) }
+
+ let(:private_snippet) { create(:personal_snippet, :private) }
+ let(:internal_snippet) { create(:personal_snippet, :internal) }
+ let(:public_snippet) { create(:personal_snippet, :public) }
+
+ let(:note_on_private) { create(:note_on_personal_snippet, noteable: private_snippet) }
+ let(:note_on_internal) { create(:note_on_personal_snippet, noteable: internal_snippet) }
+ let(:note_on_public) { create(:note_on_personal_snippet, noteable: public_snippet) }
+
+ describe 'GET index' do
+ context 'when a snippet is public' do
+ before do
+ note_on_public
+
+ get :index, { snippet_id: public_snippet }
+ end
+
+ it "returns status 200" do
+ expect(response).to have_http_status(200)
+ end
+
+ it "returns not empty array of notes" do
+ expect(JSON.parse(response.body)["notes"].empty?).to be_falsey
+ end
+ end
+
+ context 'when a snippet is internal' do
+ before do
+ note_on_internal
+ end
+
+ context 'when user not logged in' do
+ it "returns status 404" do
+ get :index, { snippet_id: internal_snippet }
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ context 'when user logged in' do
+ before do
+ sign_in(user)
+ end
+
+ it "returns status 200" do
+ get :index, { snippet_id: internal_snippet }
+
+ expect(response).to have_http_status(200)
+ end
+ end
+ end
+
+ context 'when a snippet is private' do
+ before do
+ note_on_private
+ end
+
+ context 'when user not logged in' do
+ it "returns status 404" do
+ get :index, { snippet_id: private_snippet }
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ context 'when user other than author logged in' do
+ before do
+ sign_in(user)
+ end
+
+ it "returns status 404" do
+ get :index, { snippet_id: private_snippet }
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ context 'when author logged in' do
+ before do
+ note_on_private
+
+ sign_in(private_snippet.author)
+ end
+
+ it "returns status 200" do
+ get :index, { snippet_id: private_snippet }
+
+ expect(response).to have_http_status(200)
+ end
+
+ it "returns 1 note" do
+ get :index, { snippet_id: private_snippet }
+
+ expect(JSON.parse(response.body)['notes'].count).to eq(1)
+ end
+ end
+ end
+
+ context 'dont show non visible notes' do
+ before do
+ note_on_public
+
+ sign_in(user)
+
+ expect_any_instance_of(Note).to receive(:cross_reference_not_visible_for?).and_return(true)
+ end
+
+ it "does not return any note" do
+ get :index, { snippet_id: public_snippet }
+
+ expect(JSON.parse(response.body)['notes'].count).to eq(0)
+ end
+ end
+ end
+
+ describe 'DELETE destroy' do
+ let(:request_params) do
+ {
+ snippet_id: public_snippet,
+ id: note_on_public,
+ format: :js
+ }
+ end
+
+ context 'when user is the author of a note' do
+ before do
+ sign_in(note_on_public.author)
+ end
+
+ it "returns status 200" do
+ delete :destroy, request_params
+
+ expect(response).to have_http_status(200)
+ end
+
+ it "deletes the note" do
+ expect{ delete :destroy, request_params }.to change{ Note.count }.from(1).to(0)
+ end
+
+ context 'system note' do
+ before do
+ expect_any_instance_of(Note).to receive(:system?).and_return(true)
+ end
+
+ it "does not delete the note" do
+ expect{ delete :destroy, request_params }.not_to change{ Note.count }
+ end
+ end
+ end
+
+ context 'when user is not the author of a note' do
+ before do
+ sign_in(user)
+
+ note_on_public
+ end
+
+ it "returns status 404" do
+ delete :destroy, request_params
+
+ expect(response).to have_http_status(404)
+ end
+
+ it "does not update the note" do
+ expect{ delete :destroy, request_params }.not_to change{ Note.count }
+ end
+ end
+ end
+
+ describe 'POST toggle_award_emoji' do
+ let(:note) { create(:note_on_personal_snippet, noteable: public_snippet) }
+ before do
+ sign_in(user)
+ end
+
+ subject { post(:toggle_award_emoji, snippet_id: public_snippet, id: note.id, name: "thumbsup") }
+
+ it "toggles the award emoji" do
+ expect { subject }.to change { note.award_emoji.count }.by(1)
+
+ expect(response).to have_http_status(200)
+ end
+
+ it "removes the already awarded emoji when it exists" do
+ note.toggle_award_emoji('thumbsup', user) # create award emoji before
+
+ expect { subject }.to change { AwardEmoji.count }.by(-1)
+
+ expect(response).to have_http_status(200)
+ end
+ end
+end
diff --git a/spec/controllers/snippets_controller_spec.rb b/spec/controllers/snippets_controller_spec.rb
index 5de3b9890ef..930415a4778 100644
--- a/spec/controllers/snippets_controller_spec.rb
+++ b/spec/controllers/snippets_controller_spec.rb
@@ -3,6 +3,34 @@ require 'spec_helper'
describe SnippetsController do
let(:user) { create(:user) }
+ describe 'GET #index' do
+ let(:user) { create(:user) }
+
+ context 'when username parameter is present' do
+ it 'renders snippets of a user when username is present' do
+ get :index, username: user.username
+
+ expect(response).to render_template(:index)
+ end
+ end
+
+ context 'when username parameter is not present' do
+ it 'redirects to explore snippets page when user is not logged in' do
+ get :index
+
+ expect(response).to redirect_to(explore_snippets_path)
+ end
+
+ it 'redirects to snippets dashboard page when user is logged in' do
+ sign_in(user)
+
+ get :index
+
+ expect(response).to redirect_to(dashboard_snippets_path)
+ end
+ end
+ end
+
describe 'GET #new' do
context 'when signed in' do
before do
@@ -132,7 +160,7 @@ describe SnippetsController do
it 'responds with status 404' do
get :show, id: 'doesntexist'
- expect(response).to have_http_status(404)
+ expect(response).to redirect_to(new_user_session_path)
end
end
end
@@ -350,144 +378,138 @@ describe SnippetsController do
end
end
- %w(raw download).each do |action|
- describe "GET #{action}" do
- context 'when the personal snippet is private' do
- let(:personal_snippet) { create(:personal_snippet, :private, author: user) }
+ describe "GET #raw" do
+ context 'when the personal snippet is private' do
+ let(:personal_snippet) { create(:personal_snippet, :private, author: user) }
- context 'when signed in' do
- before do
- sign_in(user)
- end
+ context 'when signed in' do
+ before do
+ sign_in(user)
+ end
- context 'when signed in user is not the author' do
- let(:other_author) { create(:author) }
- let(:other_personal_snippet) { create(:personal_snippet, :private, author: other_author) }
+ context 'when signed in user is not the author' do
+ let(:other_author) { create(:author) }
+ let(:other_personal_snippet) { create(:personal_snippet, :private, author: other_author) }
- it 'responds with status 404' do
- get action, id: other_personal_snippet.to_param
+ it 'responds with status 404' do
+ get :raw, id: other_personal_snippet.to_param
- expect(response).to have_http_status(404)
- end
+ expect(response).to have_http_status(404)
end
+ end
- context 'when signed in user is the author' do
- before { get action, id: personal_snippet.to_param }
+ context 'when signed in user is the author' do
+ before { get :raw, id: personal_snippet.to_param }
- it 'responds with status 200' do
- expect(assigns(:snippet)).to eq(personal_snippet)
- expect(response).to have_http_status(200)
- end
+ it 'responds with status 200' do
+ expect(assigns(:snippet)).to eq(personal_snippet)
+ expect(response).to have_http_status(200)
+ end
- it 'has expected headers' do
- expect(response.header['Content-Type']).to eq('text/plain; charset=utf-8')
+ it 'has expected headers' do
+ expect(response.header['Content-Type']).to eq('text/plain; charset=utf-8')
- if action == :download
- expect(response.header['Content-Disposition']).to match(/attachment/)
- elsif action == :raw
- expect(response.header['Content-Disposition']).to match(/inline/)
- end
- end
+ expect(response.header['Content-Disposition']).to match(/inline/)
end
end
+ end
- context 'when not signed in' do
- it 'redirects to the sign in page' do
- get action, id: personal_snippet.to_param
+ context 'when not signed in' do
+ it 'redirects to the sign in page' do
+ get :raw, id: personal_snippet.to_param
- expect(response).to redirect_to(new_user_session_path)
- end
+ expect(response).to redirect_to(new_user_session_path)
end
end
+ end
- context 'when the personal snippet is internal' do
- let(:personal_snippet) { create(:personal_snippet, :internal, author: user) }
+ context 'when the personal snippet is internal' do
+ let(:personal_snippet) { create(:personal_snippet, :internal, author: user) }
- context 'when signed in' do
- before do
- sign_in(user)
- end
+ context 'when signed in' do
+ before do
+ sign_in(user)
+ end
- it 'responds with status 200' do
- get action, id: personal_snippet.to_param
+ it 'responds with status 200' do
+ get :raw, id: personal_snippet.to_param
- expect(assigns(:snippet)).to eq(personal_snippet)
- expect(response).to have_http_status(200)
- end
+ expect(assigns(:snippet)).to eq(personal_snippet)
+ expect(response).to have_http_status(200)
end
+ end
- context 'when not signed in' do
- it 'redirects to the sign in page' do
- get action, id: personal_snippet.to_param
+ context 'when not signed in' do
+ it 'redirects to the sign in page' do
+ get :raw, id: personal_snippet.to_param
- expect(response).to redirect_to(new_user_session_path)
- end
+ expect(response).to redirect_to(new_user_session_path)
end
end
+ end
- context 'when the personal snippet is public' do
- let(:personal_snippet) { create(:personal_snippet, :public, author: user) }
+ context 'when the personal snippet is public' do
+ let(:personal_snippet) { create(:personal_snippet, :public, author: user) }
- context 'when signed in' do
- before do
- sign_in(user)
- end
+ context 'when signed in' do
+ before do
+ sign_in(user)
+ end
- it 'responds with status 200' do
- get action, id: personal_snippet.to_param
+ it 'responds with status 200' do
+ get :raw, id: personal_snippet.to_param
- expect(assigns(:snippet)).to eq(personal_snippet)
- expect(response).to have_http_status(200)
- end
+ expect(assigns(:snippet)).to eq(personal_snippet)
+ expect(response).to have_http_status(200)
+ end
- context 'CRLF line ending' do
- let(:personal_snippet) do
- create(:personal_snippet, :public, author: user, content: "first line\r\nsecond line\r\nthird line")
- end
+ context 'CRLF line ending' do
+ let(:personal_snippet) do
+ create(:personal_snippet, :public, author: user, content: "first line\r\nsecond line\r\nthird line")
+ end
- it 'returns LF line endings by default' do
- get action, id: personal_snippet.to_param
+ it 'returns LF line endings by default' do
+ get :raw, id: personal_snippet.to_param
- expect(response.body).to eq("first line\nsecond line\nthird line")
- end
+ expect(response.body).to eq("first line\nsecond line\nthird line")
+ end
- it 'does not convert line endings when parameter present' do
- get action, id: personal_snippet.to_param, line_ending: :raw
+ it 'does not convert line endings when parameter present' do
+ get :raw, id: personal_snippet.to_param, line_ending: :raw
- expect(response.body).to eq("first line\r\nsecond line\r\nthird line")
- end
+ expect(response.body).to eq("first line\r\nsecond line\r\nthird line")
end
end
+ end
- context 'when not signed in' do
- it 'responds with status 200' do
- get action, id: personal_snippet.to_param
+ context 'when not signed in' do
+ it 'responds with status 200' do
+ get :raw, id: personal_snippet.to_param
- expect(assigns(:snippet)).to eq(personal_snippet)
- expect(response).to have_http_status(200)
- end
+ expect(assigns(:snippet)).to eq(personal_snippet)
+ expect(response).to have_http_status(200)
end
end
+ end
- context 'when the personal snippet does not exist' do
- context 'when signed in' do
- before do
- sign_in(user)
- end
+ context 'when the personal snippet does not exist' do
+ context 'when signed in' do
+ before do
+ sign_in(user)
+ end
- it 'responds with status 404' do
- get action, id: 'doesntexist'
+ it 'responds with status 404' do
+ get :raw, id: 'doesntexist'
- expect(response).to have_http_status(404)
- end
+ expect(response).to have_http_status(404)
end
+ end
- context 'when not signed in' do
- it 'responds with status 404' do
- get action, id: 'doesntexist'
+ context 'when not signed in' do
+ it 'redirects to the sign in path' do
+ get :raw, id: 'doesntexist'
- expect(response).to have_http_status(404)
- end
+ expect(response).to redirect_to(new_user_session_path)
end
end
end
@@ -521,4 +543,16 @@ describe SnippetsController do
end
end
end
+
+ describe 'POST #preview_markdown' do
+ let(:snippet) { create(:personal_snippet, :public) }
+
+ it 'renders json in a correct format' do
+ sign_in(user)
+
+ post :preview_markdown, id: snippet, text: '*Markdown* text'
+
+ expect(JSON.parse(response.body).keys).to match_array(%w(body references))
+ end
+ end
end
diff --git a/spec/controllers/uploads_controller_spec.rb b/spec/controllers/uploads_controller_spec.rb
index f67d26da0ac..7dedfe160a6 100644
--- a/spec/controllers/uploads_controller_spec.rb
+++ b/spec/controllers/uploads_controller_spec.rb
@@ -8,6 +8,93 @@ end
describe UploadsController do
let!(:user) { create(:user, avatar: fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "image/png")) }
+ describe 'POST create' do
+ let(:model) { 'personal_snippet' }
+ let(:snippet) { create(:personal_snippet, :public) }
+ let(:jpg) { fixture_file_upload(Rails.root + 'spec/fixtures/rails_sample.jpg', 'image/jpg') }
+ let(:txt) { fixture_file_upload(Rails.root + 'spec/fixtures/doc_sample.txt', 'text/plain') }
+
+ context 'when a user does not have permissions to upload a file' do
+ it "returns 401 when the user is not logged in" do
+ post :create, model: model, id: snippet.id, format: :json
+
+ expect(response).to have_http_status(401)
+ end
+
+ it "returns 404 when user can't comment on a snippet" do
+ private_snippet = create(:personal_snippet, :private)
+
+ sign_in(user)
+ post :create, model: model, id: private_snippet.id, format: :json
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ context 'when a user is logged in' do
+ before do
+ sign_in(user)
+ end
+
+ it "returns an error without file" do
+ post :create, model: model, id: snippet.id, format: :json
+
+ expect(response).to have_http_status(422)
+ end
+
+ it "returns an error with invalid model" do
+ expect { post :create, model: 'invalid', id: snippet.id, format: :json }
+ .to raise_error(ActionController::UrlGenerationError)
+ end
+
+ it "returns 404 status when object not found" do
+ post :create, model: model, id: 9999, format: :json
+
+ expect(response).to have_http_status(404)
+ end
+
+ context 'with valid image' do
+ before do
+ post :create, model: 'personal_snippet', id: snippet.id, file: jpg, format: :json
+ end
+
+ 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"
+ end
+
+ it 'creates a corresponding Upload record' do
+ upload = Upload.last
+
+ aggregate_failures do
+ expect(upload).to exist
+ expect(upload.model).to eq snippet
+ end
+ end
+ end
+
+ context 'with valid non-image file' do
+ before do
+ post :create, model: 'personal_snippet', id: snippet.id, 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"
+ end
+
+ it 'creates a corresponding Upload record' do
+ upload = Upload.last
+
+ aggregate_failures do
+ expect(upload).to exist
+ expect(upload.model).to eq snippet
+ end
+ end
+ end
+ end
+ end
+
describe "GET show" do
context 'Content-Disposition security measures' do
let(:project) { create(:empty_project, :public) }
diff --git a/spec/controllers/users_controller_spec.rb b/spec/controllers/users_controller_spec.rb
index bbe9aaf737f..1d61719f1d0 100644
--- a/spec/controllers/users_controller_spec.rb
+++ b/spec/controllers/users_controller_spec.rb
@@ -4,15 +4,6 @@ describe UsersController do
let(:user) { create(:user) }
describe 'GET #show' do
- it 'is case-insensitive' do
- user = create(:user, username: 'CamelCaseUser')
- sign_in(user)
-
- get :show, username: user.username.downcase
-
- expect(response).to be_success
- end
-
context 'with rendered views' do
render_views
@@ -45,9 +36,9 @@ describe UsersController do
end
context 'when logged out' do
- it 'renders 404' do
+ it 'redirects to login page' do
get :show, username: user.username
- expect(response).to have_http_status(404)
+ expect(response).to redirect_to new_user_session_path
end
end
@@ -61,6 +52,58 @@ describe UsersController do
end
end
end
+
+ context 'when requesting the canonical path' do
+ let(:user) { create(:user, username: 'CamelCaseUser') }
+
+ before { sign_in(user) }
+
+ context 'with exactly matching casing' do
+ it 'responds with success' do
+ get :show, username: user.username
+
+ expect(response).to be_success
+ end
+ end
+
+ context 'with different casing' do
+ it 'redirects to the correct casing' do
+ get :show, username: user.username.downcase
+
+ expect(response).to redirect_to(user)
+ expect(controller).not_to set_flash[:notice]
+ end
+ end
+ end
+
+ context 'when requesting a redirected path' do
+ let(:redirect_route) { user.namespace.redirect_routes.create(path: 'old-username') }
+
+ it 'redirects to the canonical path' do
+ get :show, username: redirect_route.path
+
+ expect(response).to redirect_to(user)
+ expect(controller).to set_flash[:notice].to(user_moved_message(redirect_route, user))
+ end
+ end
+
+ context 'when a user by that username does not exist' do
+ context 'when logged out' do
+ it 'redirects to login page' do
+ get :show, username: 'nonexistent'
+ expect(response).to redirect_to new_user_session_path
+ end
+ end
+
+ context 'when logged in' do
+ before { sign_in(user) }
+
+ it 'renders 404' do
+ get :show, username: 'nonexistent'
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
end
describe 'GET #calendar' do
@@ -88,11 +131,45 @@ describe UsersController do
expect(assigns(:contributions_calendar).projects.count).to eq(2)
end
end
+
+ context 'when requesting the canonical path' do
+ let(:user) { create(:user, username: 'CamelCaseUser') }
+
+ before { sign_in(user) }
+
+ context 'with exactly matching casing' do
+ it 'responds with success' do
+ get :calendar, username: user.username
+
+ expect(response).to be_success
+ end
+ end
+
+ context 'with different casing' do
+ it 'redirects to the correct casing' do
+ get :calendar, username: user.username.downcase
+
+ expect(response).to redirect_to(user_calendar_path(user))
+ expect(controller).not_to set_flash[:notice]
+ end
+ end
+ end
+
+ context 'when requesting a redirected path' do
+ let(:redirect_route) { user.namespace.redirect_routes.create(path: 'old-username') }
+
+ it 'redirects to the canonical path' do
+ get :calendar, username: redirect_route.path
+
+ expect(response).to redirect_to(user_calendar_path(user))
+ expect(controller).to set_flash[:notice].to(user_moved_message(redirect_route, user))
+ end
+ end
end
describe 'GET #calendar_activities' do
let!(:project) { create(:empty_project) }
- let!(:user) { create(:user) }
+ let(:user) { create(:user) }
before do
allow_any_instance_of(User).to receive(:contributed_projects_ids).and_return([project.id])
@@ -110,6 +187,38 @@ describe UsersController do
get :calendar_activities, username: user.username
expect(response).to render_template('calendar_activities')
end
+
+ context 'when requesting the canonical path' do
+ let(:user) { create(:user, username: 'CamelCaseUser') }
+
+ context 'with exactly matching casing' do
+ it 'responds with success' do
+ get :calendar_activities, username: user.username
+
+ expect(response).to be_success
+ end
+ end
+
+ context 'with different casing' do
+ it 'redirects to the correct casing' do
+ get :calendar_activities, username: user.username.downcase
+
+ expect(response).to redirect_to(user_calendar_activities_path(user))
+ expect(controller).not_to set_flash[:notice]
+ end
+ end
+ end
+
+ context 'when requesting a redirected path' do
+ let(:redirect_route) { user.namespace.redirect_routes.create(path: 'old-username') }
+
+ it 'redirects to the canonical path' do
+ get :calendar_activities, username: redirect_route.path
+
+ expect(response).to redirect_to(user_calendar_activities_path(user))
+ expect(controller).to set_flash[:notice].to(user_moved_message(redirect_route, user))
+ end
+ end
end
describe 'GET #snippets' do
@@ -132,5 +241,87 @@ describe UsersController do
expect(JSON.parse(response.body)).to have_key('html')
end
end
+
+ context 'when requesting the canonical path' do
+ let(:user) { create(:user, username: 'CamelCaseUser') }
+
+ context 'with exactly matching casing' do
+ it 'responds with success' do
+ get :snippets, username: user.username
+
+ expect(response).to be_success
+ end
+ end
+
+ context 'with different casing' do
+ it 'redirects to the correct casing' do
+ get :snippets, username: user.username.downcase
+
+ expect(response).to redirect_to(user_snippets_path(user))
+ expect(controller).not_to set_flash[:notice]
+ end
+ end
+ end
+
+ context 'when requesting a redirected path' do
+ let(:redirect_route) { user.namespace.redirect_routes.create(path: 'old-username') }
+
+ it 'redirects to the canonical path' do
+ get :snippets, username: redirect_route.path
+
+ expect(response).to redirect_to(user_snippets_path(user))
+ expect(controller).to set_flash[:notice].to(user_moved_message(redirect_route, user))
+ end
+ end
+ end
+
+ describe 'GET #exists' do
+ before do
+ sign_in(user)
+ end
+
+ context 'when user exists' do
+ it 'returns JSON indicating the user exists' do
+ get :exists, username: user.username
+
+ expected_json = { exists: true }.to_json
+ expect(response.body).to eq(expected_json)
+ end
+
+ context 'when the casing is different' do
+ let(:user) { create(:user, username: 'CamelCaseUser') }
+
+ it 'returns JSON indicating the user exists' do
+ get :exists, username: user.username.downcase
+
+ expected_json = { exists: true }.to_json
+ expect(response.body).to eq(expected_json)
+ end
+ end
+ end
+
+ context 'when the user does not exist' do
+ it 'returns JSON indicating the user does not exist' do
+ get :exists, username: 'foo'
+
+ expected_json = { exists: false }.to_json
+ expect(response.body).to eq(expected_json)
+ end
+
+ context 'when a user changed their username' do
+ let(:redirect_route) { user.namespace.redirect_routes.create(path: 'old-username') }
+
+ it 'returns JSON indicating a user by that username does not exist' do
+ get :exists, username: 'old-username'
+
+ expected_json = { exists: false }.to_json
+ expect(response.body).to eq(expected_json)
+ end
+ end
+ end
+ end
+
+ def user_moved_message(redirect_route, user)
+ "User '#{redirect_route.path}' was moved to '#{user.full_path}'. Please update any links and bookmarks that may still have the old path."
end
end
diff --git a/spec/factories/ci/builds.rb b/spec/factories/ci/builds.rb
index b62def83ee4..78ddd8d5584 100644
--- a/spec/factories/ci/builds.rb
+++ b/spec/factories/ci/builds.rb
@@ -79,6 +79,19 @@ FactoryGirl.define do
manual
end
+ trait :retryable do
+ success
+ end
+
+ trait :cancelable do
+ pending
+ end
+
+ trait :erasable do
+ success
+ artifacts
+ end
+
trait :tags do
tag_list [:docker, :ruby]
end
diff --git a/spec/factories/ci/trigger_schedules.rb b/spec/factories/ci/pipeline_schedule.rb
index 2390706fa41..a716da46ac6 100644
--- a/spec/factories/ci/trigger_schedules.rb
+++ b/spec/factories/ci/pipeline_schedule.rb
@@ -1,14 +1,11 @@
FactoryGirl.define do
- factory :ci_trigger_schedule, class: Ci::TriggerSchedule do
- trigger factory: :ci_trigger_for_trigger_schedule
+ factory :ci_pipeline_schedule, class: Ci::PipelineSchedule do
cron '0 1 * * *'
cron_timezone Gitlab::Ci::CronParser::VALID_SYNTAX_SAMPLE_TIME_ZONE
ref 'master'
active true
-
- after(:build) do |trigger_schedule, evaluator|
- trigger_schedule.project ||= trigger_schedule.trigger.project
- end
+ description "pipeline schedule"
+ project factory: :empty_project
trait :nightly do
cron '0 1 * * *'
@@ -24,5 +21,9 @@ FactoryGirl.define do
cron '0 1 22 * *'
cron_timezone Gitlab::Ci::CronParser::VALID_SYNTAX_SAMPLE_TIME_ZONE
end
+
+ trait :inactive do
+ active false
+ end
end
end
diff --git a/spec/factories/ci/variables.rb b/spec/factories/ci/variables.rb
index 6653f0bb5c3..c5fba597c1c 100644
--- a/spec/factories/ci/variables.rb
+++ b/spec/factories/ci/variables.rb
@@ -2,5 +2,7 @@ FactoryGirl.define do
factory :ci_variable, class: Ci::Variable do
sequence(:key) { |n| "VARIABLE_#{n}" }
value 'VARIABLE_VALUE'
+
+ project factory: :empty_project
end
end
diff --git a/spec/factories/environments.rb b/spec/factories/environments.rb
index 3fbf24b5c7d..d8d699fb3aa 100644
--- a/spec/factories/environments.rb
+++ b/spec/factories/environments.rb
@@ -18,15 +18,21 @@ FactoryGirl.define do
# interconnected objects to simulate a review app.
#
after(:create) do |environment, evaluator|
+ pipeline = create(:ci_pipeline, project: environment.project)
+
+ deployable = create(:ci_build, name: "#{environment.name}:deploy",
+ pipeline: pipeline)
+
deployment = create(:deployment,
environment: environment,
project: environment.project,
+ deployable: deployable,
ref: evaluator.ref,
sha: environment.project.commit(evaluator.ref).id)
teardown_build = create(:ci_build, :manual,
- name: "#{deployment.environment.name}:teardown",
- pipeline: deployment.deployable.pipeline)
+ name: "#{environment.name}:teardown",
+ pipeline: pipeline)
deployment.update_column(:on_stop, teardown_build.name)
environment.update_attribute(:deployments, [deployment])
diff --git a/spec/factories/groups.rb b/spec/factories/groups.rb
index 86f51ffca99..52f76b094a3 100644
--- a/spec/factories/groups.rb
+++ b/spec/factories/groups.rb
@@ -17,6 +17,10 @@ FactoryGirl.define do
visibility_level Gitlab::VisibilityLevel::PRIVATE
end
+ trait :with_avatar do
+ avatar { File.open(Rails.root.join('spec/fixtures/dk.png')) }
+ end
+
trait :access_requestable do
request_access_enabled true
end
diff --git a/spec/factories/issues.rb b/spec/factories/issues.rb
index 0b6977e3b17..f1fd1fd7f73 100644
--- a/spec/factories/issues.rb
+++ b/spec/factories/issues.rb
@@ -8,6 +8,10 @@ FactoryGirl.define do
confidential true
end
+ trait :opened do
+ state :opened
+ end
+
trait :closed do
state :closed
end
diff --git a/spec/factories/merge_requests_closing_issues.rb b/spec/factories/merge_requests_closing_issues.rb
new file mode 100644
index 00000000000..fdbdc00cad7
--- /dev/null
+++ b/spec/factories/merge_requests_closing_issues.rb
@@ -0,0 +1,6 @@
+FactoryGirl.define do
+ factory :merge_requests_closing_issues do
+ issue
+ merge_request
+ end
+end
diff --git a/spec/factories/notes.rb b/spec/factories/notes.rb
index 93f4903119c..046974dcd6e 100644
--- a/spec/factories/notes.rb
+++ b/spec/factories/notes.rb
@@ -5,7 +5,7 @@ include ActionDispatch::TestProcess
FactoryGirl.define do
factory :note do
project factory: :empty_project
- note "Note"
+ note { generate(:title) }
author
on_issue
@@ -29,6 +29,8 @@ FactoryGirl.define do
factory :discussion_note_on_commit, traits: [:on_commit], class: DiscussionNote
+ factory :discussion_note_on_personal_snippet, traits: [:on_personal_snippet], class: DiscussionNote
+
factory :legacy_diff_note_on_commit, traits: [:on_commit, :legacy_diff_note], class: LegacyDiffNote
factory :legacy_diff_note_on_merge_request, traits: [:on_merge_request, :legacy_diff_note], class: LegacyDiffNote do
diff --git a/spec/factories/project_hooks.rb b/spec/factories/project_hooks.rb
index 39c2a9dd1fb..0210e871a63 100644
--- a/spec/factories/project_hooks.rb
+++ b/spec/factories/project_hooks.rb
@@ -1,6 +1,7 @@
FactoryGirl.define do
factory :project_hook do
url { generate(:url) }
+ enable_ssl_verification false
trait :token do
token { SecureRandom.hex(10) }
@@ -11,6 +12,7 @@ FactoryGirl.define do
merge_requests_events true
tag_push_events true
issues_events true
+ confidential_issues_events true
note_events true
build_events true
pipeline_events true
diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb
index 0db2fe04edd..3580752a805 100644
--- a/spec/factories/projects.rb
+++ b/spec/factories/projects.rb
@@ -32,6 +32,10 @@ FactoryGirl.define do
request_access_enabled true
end
+ trait :with_avatar do
+ avatar { File.open(Rails.root.join('spec/fixtures/dk.png')) }
+ end
+
trait :repository do
# no-op... for now!
end
diff --git a/spec/factories/services.rb b/spec/factories/services.rb
index 88f6c265505..28ddd0da753 100644
--- a/spec/factories/services.rb
+++ b/spec/factories/services.rb
@@ -1,6 +1,19 @@
FactoryGirl.define do
factory :service do
project factory: :empty_project
+ type 'Service'
+ end
+
+ factory :custom_issue_tracker_service, class: CustomIssueTrackerService do
+ project factory: :empty_project
+ type 'CustomIssueTrackerService'
+ category 'issue_tracker'
+ active true
+ properties(
+ project_url: 'https://project.url.com',
+ issues_url: 'https://issues.url.com',
+ new_issue_url: 'https://newissue.url.com'
+ )
end
factory :kubernetes_service do
@@ -9,7 +22,7 @@ FactoryGirl.define do
properties({
namespace: 'somepath',
api_url: 'https://kubernetes.example.com',
- token: 'a' * 40,
+ token: 'a' * 40
})
end
diff --git a/spec/factories/users.rb b/spec/factories/users.rb
index e1ae94a08e4..33fa80772ff 100644
--- a/spec/factories/users.rb
+++ b/spec/factories/users.rb
@@ -29,6 +29,10 @@ FactoryGirl.define do
after(:build) { |user, _| user.block! }
end
+ trait :with_avatar do
+ avatar { File.open(Rails.root.join('spec/fixtures/dk.png')) }
+ end
+
trait :two_factor_via_otp do
before(:create) do |user|
user.otp_required_for_login = true
diff --git a/spec/features/admin/admin_cohorts_spec.rb b/spec/features/admin/admin_cohorts_spec.rb
new file mode 100644
index 00000000000..dd14ffdb2ce
--- /dev/null
+++ b/spec/features/admin/admin_cohorts_spec.rb
@@ -0,0 +1,15 @@
+require 'rails_helper'
+
+feature 'Admin cohorts page', feature: true do
+ before do
+ login_as :admin
+ end
+
+ scenario 'See users count per month' do
+ 2.times { create(:user) }
+
+ visit admin_cohorts_path
+
+ expect(page).to have_content("#{Time.now.strftime('%b %Y')} 3 0")
+ end
+end
diff --git a/spec/features/admin/admin_hooks_spec.rb b/spec/features/admin/admin_hooks_spec.rb
index fb519a9bf12..c5f24d412d7 100644
--- a/spec/features/admin/admin_hooks_spec.rb
+++ b/spec/features/admin/admin_hooks_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe "Admin::Hooks", feature: true do
+describe 'Admin::Hooks', feature: true do
before do
@project = create(:project)
login_as :admin
@@ -8,24 +8,24 @@ describe "Admin::Hooks", feature: true do
@system_hook = create(:system_hook)
end
- describe "GET /admin/hooks" do
- it "is ok" do
+ describe 'GET /admin/hooks' do
+ it 'is ok' do
visit admin_root_path
- page.within ".layout-nav" do
- click_on "Hooks"
+ page.within '.layout-nav' do
+ click_on 'Hooks'
end
expect(current_path).to eq(admin_hooks_path)
end
- it "has hooks list" do
+ it 'has hooks list' do
visit admin_hooks_path
expect(page).to have_content(@system_hook.url)
end
end
- describe "New Hook" do
+ describe 'New Hook' do
let(:url) { generate(:url) }
it 'adds new hook' do
@@ -40,11 +40,36 @@ describe "Admin::Hooks", feature: true do
end
end
- describe "Test" do
+ describe 'Update existing hook' do
+ let(:new_url) { generate(:url) }
+
+ it 'updates existing hook' do
+ visit admin_hooks_path
+
+ click_link 'Edit'
+ fill_in 'hook_url', with: new_url
+ check 'Enable SSL verification'
+ click_button 'Save changes'
+
+ expect(page).to have_content 'SSL Verification: enabled'
+ expect(current_path).to eq(admin_hooks_path)
+ expect(page).to have_content(new_url)
+ end
+ end
+
+ describe 'Remove existing hook' do
+ it 'remove existing hook' do
+ visit admin_hooks_path
+
+ expect { click_link 'Remove' }.to change(SystemHook, :count).by(-1)
+ end
+ end
+
+ describe 'Test' do
before do
WebMock.stub_request(:post, @system_hook.url)
visit admin_hooks_path
- click_link "Test hook"
+ click_link 'Test hook'
end
it { expect(current_path).to eq(admin_hooks_path) }
diff --git a/spec/features/admin/admin_labels_spec.rb b/spec/features/admin/admin_labels_spec.rb
index 6d6c9165c83..fa3d9ee25c0 100644
--- a/spec/features/admin/admin_labels_spec.rb
+++ b/spec/features/admin/admin_labels_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
RSpec.describe 'admin issues labels' do
- include WaitForAjax
-
let!(:bug_label) { Label.create(title: 'bug', template: true) }
let!(:feature_label) { Label.create(title: 'feature', template: true) }
diff --git a/spec/features/admin/admin_requests_profiles_spec.rb b/spec/features/admin/admin_requests_profiles_spec.rb
new file mode 100644
index 00000000000..e8ecb70306b
--- /dev/null
+++ b/spec/features/admin/admin_requests_profiles_spec.rb
@@ -0,0 +1,69 @@
+require 'spec_helper'
+
+describe 'Admin::RequestsProfilesController', feature: true do
+ before do
+ FileUtils.mkdir_p(Gitlab::RequestProfiler::PROFILES_DIR)
+ login_as(:admin)
+ end
+
+ after do
+ Gitlab::RequestProfiler.remove_all_profiles
+ end
+
+ describe 'GET /admin/requests_profiles' do
+ it 'shows the current profile token' do
+ allow(Rails).to receive(:cache).and_return(ActiveSupport::Cache::MemoryStore.new)
+
+ visit admin_requests_profiles_path
+
+ expect(page).to have_content("X-Profile-Token: #{Gitlab::RequestProfiler.profile_token}")
+ end
+
+ it 'lists all available profiles' do
+ time1 = 1.hour.ago
+ time2 = 2.hours.ago
+ time3 = 3.hours.ago
+ profile1 = "|gitlab-org|gitlab-ce_#{time1.to_i}.html"
+ profile2 = "|gitlab-org|gitlab-ce_#{time2.to_i}.html"
+ profile3 = "|gitlab-com|infrastructure_#{time3.to_i}.html"
+
+ FileUtils.touch("#{Gitlab::RequestProfiler::PROFILES_DIR}/#{profile1}")
+ FileUtils.touch("#{Gitlab::RequestProfiler::PROFILES_DIR}/#{profile2}")
+ FileUtils.touch("#{Gitlab::RequestProfiler::PROFILES_DIR}/#{profile3}")
+
+ visit admin_requests_profiles_path
+
+ within('.panel', text: '/gitlab-org/gitlab-ce') do
+ expect(page).to have_selector("a[href='#{admin_requests_profile_path(profile1)}']", text: time1.to_s(:long))
+ expect(page).to have_selector("a[href='#{admin_requests_profile_path(profile2)}']", text: time2.to_s(:long))
+ end
+
+ within('.panel', text: '/gitlab-com/infrastructure') do
+ expect(page).to have_selector("a[href='#{admin_requests_profile_path(profile3)}']", text: time3.to_s(:long))
+ end
+ end
+ end
+
+ describe 'GET /admin/requests_profiles/:profile' do
+ context 'when a profile exists' do
+ it 'displays the content of the profile' do
+ content = 'This is a request profile'
+ profile = "|gitlab-org|gitlab-ce_#{Time.now.to_i}.html"
+
+ File.write("#{Gitlab::RequestProfiler::PROFILES_DIR}/#{profile}", content)
+
+ visit admin_requests_profile_path(profile)
+
+ expect(page).to have_content(content)
+ end
+ end
+
+ context 'when a profile does not exist' do
+ it 'shows an error message' do
+ visit admin_requests_profile_path('|non|existent_12345.html')
+
+ expect(page).to have_content('Profile not found')
+ end
+ end
+ end
+end
diff --git a/spec/features/admin/admin_users_spec.rb b/spec/features/admin/admin_users_spec.rb
index f6c3bc6a58d..c5b1ef1295c 100644
--- a/spec/features/admin/admin_users_spec.rb
+++ b/spec/features/admin/admin_users_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
describe "Admin::Users", feature: true do
- include WaitForAjax
-
let!(:user) do
create(:omniauth_user, provider: 'twitter', extern_uid: '123456')
end
diff --git a/spec/features/admin/admin_uses_repository_checks_spec.rb b/spec/features/admin/admin_uses_repository_checks_spec.rb
index 855247de2ea..ab5c42365fe 100644
--- a/spec/features/admin/admin_uses_repository_checks_spec.rb
+++ b/spec/features/admin/admin_uses_repository_checks_spec.rb
@@ -23,7 +23,7 @@ feature 'Admin uses repository checks', feature: true do
project = create(:empty_project)
project.update_columns(
last_repository_check_failed: true,
- last_repository_check_at: Time.now,
+ last_repository_check_at: Time.now
)
visit_admin_project_page(project)
diff --git a/spec/features/atom/dashboard_issues_spec.rb b/spec/features/atom/dashboard_issues_spec.rb
index 58b14e09740..9ea325ab41b 100644
--- a/spec/features/atom/dashboard_issues_spec.rb
+++ b/spec/features/atom/dashboard_issues_spec.rb
@@ -32,7 +32,7 @@ describe "Dashboard Issues Feed", feature: true do
end
context "issue with basic fields" do
- let!(:issue2) { create(:issue, author: user, assignee: assignee, project: project2, description: 'test desc') }
+ let!(:issue2) { create(:issue, author: user, assignees: [assignee], project: project2, description: 'test desc') }
it "renders issue fields" do
visit issues_dashboard_path(:atom, private_token: user.private_token)
@@ -41,7 +41,7 @@ describe "Dashboard Issues Feed", feature: true do
expect(entry).to be_present
expect(entry).to have_selector('author email', text: issue2.author_public_email)
- expect(entry).to have_selector('assignee email', text: issue2.assignee_public_email)
+ expect(entry).to have_selector('assignees email', text: assignee.public_email)
expect(entry).not_to have_selector('labels')
expect(entry).not_to have_selector('milestone')
expect(entry).to have_selector('description', text: issue2.description)
@@ -51,7 +51,7 @@ describe "Dashboard Issues Feed", feature: true do
context "issue with label and milestone" do
let!(:milestone1) { create(:milestone, project: project1, title: 'v1') }
let!(:label1) { create(:label, project: project1, title: 'label1') }
- let!(:issue1) { create(:issue, author: user, assignee: assignee, project: project1, milestone: milestone1) }
+ let!(:issue1) { create(:issue, author: user, assignees: [assignee], project: project1, milestone: milestone1) }
before do
issue1.labels << label1
@@ -64,7 +64,7 @@ describe "Dashboard Issues Feed", feature: true do
expect(entry).to be_present
expect(entry).to have_selector('author email', text: issue1.author_public_email)
- expect(entry).to have_selector('assignee email', text: issue1.assignee_public_email)
+ expect(entry).to have_selector('assignees email', text: assignee.public_email)
expect(entry).to have_selector('labels label', text: label1.title)
expect(entry).to have_selector('milestone', text: milestone1.title)
expect(entry).not_to have_selector('description')
diff --git a/spec/features/atom/issues_spec.rb b/spec/features/atom/issues_spec.rb
index b3903ec2faf..4f6754ad541 100644
--- a/spec/features/atom/issues_spec.rb
+++ b/spec/features/atom/issues_spec.rb
@@ -6,7 +6,7 @@ describe 'Issues Feed', feature: true do
let!(:assignee) { create(:user, email: 'private2@example.com', public_email: 'public2@example.com') }
let!(:group) { create(:group) }
let!(:project) { create(:project) }
- let!(:issue) { create(:issue, author: user, assignee: assignee, project: project) }
+ let!(:issue) { create(:issue, author: user, assignees: [assignee], project: project) }
before do
project.team << [user, :developer]
@@ -22,7 +22,8 @@ describe 'Issues Feed', feature: true do
to have_content('application/atom+xml')
expect(body).to have_selector('title', text: "#{project.name} issues")
expect(body).to have_selector('author email', text: issue.author_public_email)
- expect(body).to have_selector('assignee email', text: issue.author_public_email)
+ expect(body).to have_selector('assignees assignee email', text: issue.assignees.first.public_email)
+ expect(body).to have_selector('assignee email', text: issue.assignees.first.public_email)
expect(body).to have_selector('entry summary', text: issue.title)
end
end
@@ -36,7 +37,8 @@ describe 'Issues Feed', feature: true do
to have_content('application/atom+xml')
expect(body).to have_selector('title', text: "#{project.name} issues")
expect(body).to have_selector('author email', text: issue.author_public_email)
- expect(body).to have_selector('assignee email', text: issue.author_public_email)
+ expect(body).to have_selector('assignees assignee email', text: issue.assignees.first.public_email)
+ expect(body).to have_selector('assignee email', text: issue.assignees.first.public_email)
expect(body).to have_selector('entry summary', text: issue.title)
end
end
diff --git a/spec/features/atom/users_spec.rb b/spec/features/atom/users_spec.rb
index 55e10a1a89b..7a2987e815d 100644
--- a/spec/features/atom/users_spec.rb
+++ b/spec/features/atom/users_spec.rb
@@ -53,7 +53,7 @@ describe "User Feed", feature: true do
end
it 'has XHTML summaries in issue descriptions' do
- expect(body).to match /we have a bug!<\/p>\n\n<hr ?\/>\n\n<p dir="auto">I guess/
+ expect(body).to match /<hr ?\/>/
end
it 'has XHTML summaries in notes' do
diff --git a/spec/features/auto_deploy_spec.rb b/spec/features/auto_deploy_spec.rb
index 6f36c74c911..eba1bca83a8 100644
--- a/spec/features/auto_deploy_spec.rb
+++ b/spec/features/auto_deploy_spec.rb
@@ -1,10 +1,8 @@
require 'spec_helper'
describe 'Auto deploy' do
- include WaitForAjax
-
let(:user) { create(:user) }
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
before do
project.create_kubernetes_service(
@@ -12,7 +10,7 @@ describe 'Auto deploy' do
properties: {
namespace: project.path,
api_url: 'https://kubernetes.example.com',
- token: 'a' * 40,
+ token: 'a' * 40
}
)
project.team << [user, :master]
diff --git a/spec/features/boards/add_issues_modal_spec.rb b/spec/features/boards/add_issues_modal_spec.rb
index 248c31115ad..505e0b5c355 100644
--- a/spec/features/boards/add_issues_modal_spec.rb
+++ b/spec/features/boards/add_issues_modal_spec.rb
@@ -1,7 +1,6 @@
require 'rails_helper'
describe 'Issue Boards add issue modal', :feature, :js do
- include WaitForAjax
include WaitForVueResource
let(:project) { create(:empty_project, :public) }
diff --git a/spec/features/boards/boards_spec.rb b/spec/features/boards/boards_spec.rb
index 30ad169e30e..18585488e26 100644
--- a/spec/features/boards/boards_spec.rb
+++ b/spec/features/boards/boards_spec.rb
@@ -1,7 +1,6 @@
require 'rails_helper'
describe 'Issue Boards', feature: true, js: true do
- include WaitForAjax
include WaitForVueResource
include DragTo
@@ -72,7 +71,7 @@ describe 'Issue Boards', feature: true, js: true do
let!(:list2) { create(:list, board: board, label: development, position: 1) }
let!(:confidential_issue) { create(:labeled_issue, :confidential, project: project, author: user, labels: [planning], relative_position: 9) }
- let!(:issue1) { create(:labeled_issue, project: project, assignee: user, labels: [planning], relative_position: 8) }
+ let!(:issue1) { create(:labeled_issue, project: project, assignees: [user], labels: [planning], relative_position: 8) }
let!(:issue2) { create(:labeled_issue, project: project, author: user2, labels: [planning], relative_position: 7) }
let!(:issue3) { create(:labeled_issue, project: project, labels: [planning], relative_position: 6) }
let!(:issue4) { create(:labeled_issue, project: project, labels: [planning], relative_position: 5) }
diff --git a/spec/features/boards/issue_ordering_spec.rb b/spec/features/boards/issue_ordering_spec.rb
index c50155a6d14..bfa2a72a256 100644
--- a/spec/features/boards/issue_ordering_spec.rb
+++ b/spec/features/boards/issue_ordering_spec.rb
@@ -38,6 +38,8 @@ describe 'Issue Boards', :feature, :js do
it 'moves un-ordered issue to top of list' do
drag(from_index: 3, to_index: 0)
+ wait_for_vue_resource
+
page.within(first('.board')) do
expect(first('.card')).to have_content(issue4.title)
end
diff --git a/spec/features/boards/modal_filter_spec.rb b/spec/features/boards/modal_filter_spec.rb
index 4a4c13e79c8..e1367c675e5 100644
--- a/spec/features/boards/modal_filter_spec.rb
+++ b/spec/features/boards/modal_filter_spec.rb
@@ -98,7 +98,7 @@ describe 'Issue Boards add issue modal filtering', :feature, :js do
end
context 'assignee' do
- let!(:issue) { create(:issue, project: project, assignee: user2) }
+ let!(:issue) { create(:issue, project: project, assignees: [user2]) }
before do
project.team << [user2, :developer]
diff --git a/spec/features/boards/new_issue_spec.rb b/spec/features/boards/new_issue_spec.rb
index e6d7cf106d4..f04a1a89e96 100644
--- a/spec/features/boards/new_issue_spec.rb
+++ b/spec/features/boards/new_issue_spec.rb
@@ -1,7 +1,6 @@
require 'rails_helper'
describe 'Issue Boards new issue', feature: true, js: true do
- include WaitForAjax
include WaitForVueResource
let(:project) { create(:empty_project, :public) }
diff --git a/spec/features/boards/sidebar_spec.rb b/spec/features/boards/sidebar_spec.rb
index 3332e07ec31..1238647d3f3 100644
--- a/spec/features/boards/sidebar_spec.rb
+++ b/spec/features/boards/sidebar_spec.rb
@@ -1,17 +1,17 @@
require 'rails_helper'
describe 'Issue Boards', feature: true, js: true do
- include WaitForAjax
include WaitForVueResource
let(:user) { create(:user) }
+ let(:user2) { create(:user) }
let(:project) { create(:empty_project, :public) }
let!(:milestone) { create(:milestone, project: project) }
let!(:development) { create(:label, project: project, name: 'Development') }
let!(:bug) { create(:label, project: project, name: 'Bug') }
let!(:regression) { create(:label, project: project, name: 'Regression') }
let!(:stretch) { create(:label, project: project, name: 'Stretch') }
- let!(:issue1) { create(:labeled_issue, project: project, assignee: user, milestone: milestone, labels: [development], relative_position: 2) }
+ let!(:issue1) { create(:labeled_issue, project: project, assignees: [user], milestone: milestone, labels: [development], relative_position: 2) }
let!(:issue2) { create(:labeled_issue, project: project, labels: [development, stretch], relative_position: 1) }
let(:board) { create(:board, project: project) }
let!(:list) { create(:list, board: board, label: development, position: 0) }
@@ -113,10 +113,11 @@ describe 'Issue Boards', feature: true, js: true do
page.within('.dropdown-menu-user') do
click_link 'Unassigned'
-
- wait_for_vue_resource
end
+ find('.dropdown-menu-toggle').click
+ wait_for_vue_resource
+
expect(page).to have_content('No assignee')
end
@@ -129,7 +130,7 @@ describe 'Issue Boards', feature: true, js: true do
page.within(find('.assignee')) do
expect(page).to have_content('No assignee')
- click_link 'assign yourself'
+ click_button 'assign yourself'
wait_for_vue_resource
@@ -139,7 +140,7 @@ describe 'Issue Boards', feature: true, js: true do
expect(card).to have_selector('.avatar')
end
- it 'resets assignee dropdown' do
+ it 'updates assignee dropdown' do
click_card(card)
page.within('.assignee') do
@@ -157,13 +158,13 @@ describe 'Issue Boards', feature: true, js: true do
end
page.within(first('.board')) do
- find('.card:nth-child(2)').click
+ find('.card:nth-child(2)').trigger('click')
end
page.within('.assignee') do
click_link 'Edit'
-
- expect(page).not_to have_selector('.is-active')
+
+ expect(find('.dropdown-menu')).to have_selector('.is-active')
end
end
end
diff --git a/spec/features/boards/sub_group_project_spec.rb b/spec/features/boards/sub_group_project_spec.rb
new file mode 100644
index 00000000000..6cd7fddd288
--- /dev/null
+++ b/spec/features/boards/sub_group_project_spec.rb
@@ -0,0 +1,45 @@
+require 'rails_helper'
+
+describe 'Sub-group project issue boards', :feature, :js do
+ include WaitForVueResource
+
+ let(:group) { create(:group) }
+ let(:nested_group_1) { create(:group, parent: group) }
+ let(:project) { create(:empty_project, group: nested_group_1) }
+ let(:board) { create(:board, project: project) }
+ let(:label) { create(:label, project: project) }
+ let(:user) { create(:user) }
+ let!(:list1) { create(:list, board: board, label: label, position: 0) }
+ let!(:issue) { create(:labeled_issue, project: project, labels: [label]) }
+
+ before do
+ project.add_master(user)
+
+ login_as(user)
+
+ visit namespace_project_board_path(project.namespace, project, board)
+ wait_for_vue_resource
+ end
+
+ it 'creates new label from sidebar' do
+ find('.card').click
+
+ page.within '.labels' do
+ click_link 'Edit'
+ click_link 'Create new label'
+ end
+
+ page.within '.dropdown-new-label' do
+ fill_in 'new_label_name', with: 'test label'
+ first('.suggest-colors-dropdown a').click
+
+ click_button 'Create'
+
+ wait_for_ajax
+ end
+
+ page.within '.labels' do
+ expect(page).to have_link 'test label'
+ end
+ end
+end
diff --git a/spec/features/calendar_spec.rb b/spec/features/calendar_spec.rb
index 35d090c4b7f..496faf87a16 100644
--- a/spec/features/calendar_spec.rb
+++ b/spec/features/calendar_spec.rb
@@ -1,10 +1,8 @@
require 'spec_helper'
feature 'Contributions Calendar', :feature, :js do
- include WaitForAjax
-
let(:user) { create(:user) }
- let(:contributed_project) { create(:project, :public) }
+ let(:contributed_project) { create(:empty_project, :public) }
let(:issue_note) { create(:note, project: contributed_project) }
# Ex/ Sunday Jan 1, 2016
diff --git a/spec/features/commits_spec.rb b/spec/features/commits_spec.rb
index 881f1fca4d1..e6c4ab24de5 100644
--- a/spec/features/commits_spec.rb
+++ b/spec/features/commits_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
describe 'Commits' do
include CiStatusHelper
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
describe 'CI' do
before do
diff --git a/spec/features/copy_as_gfm_spec.rb b/spec/features/copy_as_gfm_spec.rb
index 55df7e45f79..be615519a09 100644
--- a/spec/features/copy_as_gfm_spec.rb
+++ b/spec/features/copy_as_gfm_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe 'Copy as GFM', feature: true, js: true do
- include GitlabMarkdownHelper
+ include MarkupHelper
include RepoHelpers
include ActionView::Helpers::JavaScriptHelper
@@ -96,7 +96,7 @@ describe 'Copy as GFM', feature: true, js: true do
# issue link
"[Issue](#{namespace_project_issue_url(@project.namespace, @project, @feat.issue)})",
# issue link with note anchor
- "[Issue](#{namespace_project_issue_url(@project.namespace, @project, @feat.issue, anchor: 'note_123')})",
+ "[Issue](#{namespace_project_issue_url(@project.namespace, @project, @feat.issue, anchor: 'note_123')})"
)
verify(
@@ -433,7 +433,7 @@ describe 'Copy as GFM', feature: true, js: true do
end
describe 'Copying code' do
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
context 'from a diff' do
before do
@@ -479,6 +479,7 @@ describe 'Copy as GFM', feature: true, js: true do
context 'from a blob' do
before do
visit namespace_project_blob_path(project.namespace, project, File.join('master', 'files/ruby/popen.rb'))
+ wait_for_ajax
end
context 'selecting one word of text' do
@@ -520,6 +521,7 @@ describe 'Copy as GFM', feature: true, js: true do
context 'from a GFM code block' do
before do
visit namespace_project_blob_path(project.namespace, project, File.join('markdown', 'doc/api/users.md'))
+ wait_for_ajax
end
context 'selecting one word of text' do
diff --git a/spec/features/cycle_analytics_spec.rb b/spec/features/cycle_analytics_spec.rb
index 0648c89a5c7..cbeb73d9cae 100644
--- a/spec/features/cycle_analytics_spec.rb
+++ b/spec/features/cycle_analytics_spec.rb
@@ -1,21 +1,21 @@
require 'spec_helper'
feature 'Cycle Analytics', feature: true, js: true do
- include WaitForAjax
-
let(:user) { create(:user) }
let(:guest) { create(:user) }
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
let(:issue) { create(:issue, project: project, created_at: 2.days.ago) }
let(:milestone) { create(:milestone, project: project) }
- let(:mr) { create_merge_request_closing_issue(issue) }
+ let(:mr) { create_merge_request_closing_issue(issue, commit_message: "References #{issue.to_reference}") }
let(:pipeline) { create(:ci_empty_pipeline, status: 'created', project: project, ref: mr.source_branch, sha: mr.source_branch_sha) }
context 'as an allowed user' do
context 'when project is new' do
before do
- project.team << [user, :master]
+ project.add_master(user)
+
login_as(user)
+
visit namespace_project_cycle_analytics_path(project.namespace, project)
wait_for_ajax
end
@@ -32,9 +32,10 @@ feature 'Cycle Analytics', feature: true, js: true do
context "when there's cycle analytics data" do
before do
- project.team << [user, :master]
-
allow_any_instance_of(Gitlab::ReferenceExtractor).to receive(:issues).and_return([issue])
+ mr.update(head_pipeline: pipeline)
+ project.add_master(user)
+
create_cycle
deploy_master
@@ -64,11 +65,30 @@ feature 'Cycle Analytics', feature: true, js: true do
expect_issue_to_be_present
end
end
+
+ context "when my preferred language is Spanish" do
+ before do
+ user.update_attribute(:preferred_language, 'es')
+
+ project.team << [user, :master]
+ login_as(user)
+ visit namespace_project_cycle_analytics_path(project.namespace, project)
+ wait_for_ajax
+ end
+
+ it 'shows the content in Spanish' do
+ expect(page).to have_content('Estado del Pipeline')
+ end
+
+ it 'resets the language to English' do
+ expect(I18n.locale).to eq(:en)
+ end
+ end
end
context "as a guest" do
before do
- project.team << [guest, :guest]
+ project.add_guest(guest)
allow_any_instance_of(Gitlab::ReferenceExtractor).to receive(:issues).and_return([issue])
create_cycle
diff --git a/spec/features/dashboard/datetime_on_tooltips_spec.rb b/spec/features/dashboard/datetime_on_tooltips_spec.rb
index dc9d09fa396..0e9e3f78be2 100644
--- a/spec/features/dashboard/datetime_on_tooltips_spec.rb
+++ b/spec/features/dashboard/datetime_on_tooltips_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
feature 'Tooltips on .timeago dates', feature: true, js: true do
- include WaitForAjax
-
let(:user) { create(:user) }
let(:project) { create(:project, name: 'test', namespace: user.namespace) }
let(:created_date) { Date.yesterday.to_time }
diff --git a/spec/features/dashboard/group_spec.rb b/spec/features/dashboard/group_spec.rb
index 1d4b86ed4b4..8e20fdec8ad 100644
--- a/spec/features/dashboard/group_spec.rb
+++ b/spec/features/dashboard/group_spec.rb
@@ -7,7 +7,7 @@ RSpec.describe 'Dashboard Group', feature: true do
it 'creates new group', js: true do
visit dashboard_groups_path
- click_link 'New group'
+ find('.btn-new').trigger('click')
new_path = 'Samurai'
new_description = 'Tokugawa Shogunate'
diff --git a/spec/features/dashboard/groups_list_spec.rb b/spec/features/dashboard/groups_list_spec.rb
index ca04107d33a..52b4d82e856 100644
--- a/spec/features/dashboard/groups_list_spec.rb
+++ b/spec/features/dashboard/groups_list_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
describe 'Dashboard Groups page', js: true, feature: true do
- include WaitForAjax
-
let!(:user) { create :user }
let!(:group) { create(:group) }
let!(:nested_group) { create(:group, :nested) }
diff --git a/spec/features/dashboard/issuables_counter_spec.rb b/spec/features/dashboard/issuables_counter_spec.rb
index 4fca7577e74..354267dbee7 100644
--- a/spec/features/dashboard/issuables_counter_spec.rb
+++ b/spec/features/dashboard/issuables_counter_spec.rb
@@ -7,7 +7,7 @@ describe 'Navigation bar counter', feature: true, caching: true do
let(:merge_request) { create(:merge_request, source_project: project) }
before do
- issue.update(assignee: user)
+ issue.assignees = [user]
merge_request.update(assignee: user)
login_as(user)
end
@@ -17,7 +17,9 @@ describe 'Navigation bar counter', feature: true, caching: true do
expect_counters('issues', '1')
- issue.update(assignee: nil)
+ issue.assignees = []
+
+ user.invalidate_cache_counts
Timecop.travel(3.minutes.from_now) do
visit issues_path
@@ -33,6 +35,8 @@ describe 'Navigation bar counter', feature: true, caching: true do
merge_request.update(assignee: nil)
+ user.invalidate_cache_counts
+
Timecop.travel(3.minutes.from_now) do
visit merge_requests_path
diff --git a/spec/features/dashboard/issues_spec.rb b/spec/features/dashboard/issues_spec.rb
index f4420814c3a..86c7954e60c 100644
--- a/spec/features/dashboard/issues_spec.rb
+++ b/spec/features/dashboard/issues_spec.rb
@@ -11,7 +11,7 @@ RSpec.describe 'Dashboard Issues', feature: true do
let!(:authored_issue) { create :issue, author: current_user, project: project }
let!(:authored_issue_on_public_project) { create :issue, author: current_user, project: public_project }
- let!(:assigned_issue) { create :issue, assignee: current_user, project: project }
+ let!(:assigned_issue) { create :issue, assignees: [current_user], project: project }
let!(:other_issue) { create :issue, project: project }
before do
@@ -30,6 +30,11 @@ RSpec.describe 'Dashboard Issues', feature: true do
find('#assignee_id', visible: false).set('')
find('.js-author-search', match: :first).click
find('.dropdown-menu-author li a', match: :first, text: current_user.to_reference).click
+ find('.js-author-search', match: :first).click
+
+ page.within '.dropdown-menu-user' do
+ expect(find('.dropdown-menu-author li a.is-active', match: :first, text: current_user.to_reference)).to be_visible
+ end
expect(page).to have_content(authored_issue.title)
expect(page).to have_content(authored_issue_on_public_project.title)
diff --git a/spec/features/dashboard/milestone_filter_spec.rb b/spec/features/dashboard/milestone_filter_spec.rb
new file mode 100644
index 00000000000..d60a002a8d7
--- /dev/null
+++ b/spec/features/dashboard/milestone_filter_spec.rb
@@ -0,0 +1,60 @@
+require 'spec_helper'
+
+describe 'Dashboard > milestone filter', :feature, :js do
+ include WaitForAjax
+
+ let(:user) { create(:user) }
+ let(:project) { create(:project, name: 'test', namespace: user.namespace) }
+ let(:milestone) { create(:milestone, title: "v1.0", project: project) }
+ let(:milestone2) { create(:milestone, title: "v2.0", project: project) }
+ let!(:issue) { create :issue, author: user, project: project, milestone: milestone }
+ let!(:issue2) { create :issue, author: user, project: project, milestone: milestone2 }
+
+ before do
+ login_as(user)
+ visit issues_dashboard_path(author_id: user.id)
+ end
+
+ context 'default state' do
+ it 'shows issues with Any Milestone' do
+ page.all('.issue-info').each do |issue_info|
+ expect(issue_info.text).to match(/v\d.0/)
+ end
+ end
+ end
+
+ context 'filtering by milestone' do
+ milestone_select = '.js-milestone-select'
+
+ before do
+ find(milestone_select).click
+ wait_for_ajax
+
+ page.within('.dropdown-content') do
+ click_link 'v1.0'
+ end
+
+ find(milestone_select).click
+ wait_for_ajax
+ end
+
+ it 'shows issues with Milestone v1.0' do
+ expect(find('.issues-list')).to have_selector('.issue', count: 1)
+ expect(find('.dropdown-content')).to have_selector('a.is-active', count: 1)
+ end
+
+ it 'should not change active Milestone unless clicked' do
+ expect(find('.dropdown-content')).to have_selector('a.is-active', count: 1)
+
+ # open & close dropdown
+ find('.dropdown-menu-close').click
+
+ expect(find('.milestone-filter')).not_to have_selector('.dropdown.open')
+
+ find(milestone_select).click
+
+ expect(find('.dropdown-content')).to have_selector('a.is-active', count: 1)
+ expect(find('.dropdown-content a.is-active')).to have_content('v1.0')
+ end
+ end
+end
diff --git a/spec/features/dashboard/project_member_activity_index_spec.rb b/spec/features/dashboard/project_member_activity_index_spec.rb
index 49d93db58a9..16c214ae060 100644
--- a/spec/features/dashboard/project_member_activity_index_spec.rb
+++ b/spec/features/dashboard/project_member_activity_index_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
feature 'Project member activity', feature: true, js: true do
- include WaitForAjax
-
let(:user) { create(:user) }
let(:project) { create(:empty_project, :public, name: 'x', namespace: user.namespace) }
diff --git a/spec/features/dashboard/shortcuts_spec.rb b/spec/features/dashboard/shortcuts_spec.rb
index 4c9adcabe34..349b948eaee 100644
--- a/spec/features/dashboard/shortcuts_spec.rb
+++ b/spec/features/dashboard/shortcuts_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Dashboard shortcuts', feature: true, js: true do
+feature 'Dashboard shortcuts', :feature, :js do
context 'logged in' do
before do
login_as :user
@@ -8,21 +8,21 @@ feature 'Dashboard shortcuts', feature: true, js: true do
end
scenario 'Navigate to tabs' do
- find('body').native.send_keys([:shift, 'P'])
-
- check_page_title('Projects')
-
- find('body').native.send_key([:shift, 'I'])
+ find('body').send_keys([:shift, 'I'])
check_page_title('Issues')
- find('body').native.send_key([:shift, 'M'])
+ find('body').send_keys([:shift, 'M'])
check_page_title('Merge Requests')
- find('body').native.send_keys([:shift, 'T'])
+ find('body').send_keys([:shift, 'T'])
check_page_title('Todos')
+
+ find('body').send_keys([:shift, 'P'])
+
+ check_page_title('Projects')
end
end
@@ -32,17 +32,20 @@ feature 'Dashboard shortcuts', feature: true, js: true do
end
scenario 'Navigate to tabs' do
- find('body').native.send_keys([:shift, 'P'])
-
- expect(page).to have_content('No projects found')
-
- find('body').native.send_keys([:shift, 'G'])
+ find('body').send_keys([:shift, 'G'])
+ find('.nothing-here-block')
expect(page).to have_content('No public groups')
- find('body').native.send_keys([:shift, 'S'])
+ find('body').send_keys([:shift, 'S'])
+ find('.nothing-here-block')
expect(page).to have_selector('.snippets-list-holder')
+
+ find('body').send_keys([:shift, 'P'])
+
+ find('.nothing-here-block')
+ expect(page).to have_content('No projects found')
end
end
diff --git a/spec/features/dashboard/snippets_spec.rb b/spec/features/dashboard/snippets_spec.rb
index 62937688c22..c6ba118220a 100644
--- a/spec/features/dashboard/snippets_spec.rb
+++ b/spec/features/dashboard/snippets_spec.rb
@@ -12,4 +12,51 @@ describe 'Dashboard snippets', feature: true do
it_behaves_like 'paginated snippets'
end
+
+ context 'filtering by visibility' do
+ let(:user) { create(:user) }
+ let!(:snippets) do
+ [
+ create(:personal_snippet, :public, author: user),
+ create(:personal_snippet, :internal, author: user),
+ create(:personal_snippet, :private, author: user),
+ create(:personal_snippet, :public)
+ ]
+ end
+
+ before do
+ login_as(user)
+
+ visit dashboard_snippets_path
+ end
+
+ it 'contains all snippets of logged user' do
+ expect(page).to have_selector('.snippet-row', count: 3)
+
+ expect(page).to have_content(snippets[0].title)
+ expect(page).to have_content(snippets[1].title)
+ expect(page).to have_content(snippets[2].title)
+ end
+
+ it 'contains all private snippets of logged user when clicking on private' do
+ click_link('Private')
+
+ expect(page).to have_selector('.snippet-row', count: 1)
+ expect(page).to have_content(snippets[2].title)
+ end
+
+ it 'contains all internal snippets of logged user when clicking on internal' do
+ click_link('Internal')
+
+ expect(page).to have_selector('.snippet-row', count: 1)
+ expect(page).to have_content(snippets[1].title)
+ end
+
+ it 'contains all public snippets of logged user when clicking on public' do
+ click_link('Public')
+
+ expect(page).to have_selector('.snippet-row', count: 1)
+ expect(page).to have_content(snippets[0].title)
+ end
+ end
end
diff --git a/spec/features/dashboard_issues_spec.rb b/spec/features/dashboard_issues_spec.rb
index 8c61cdebc4b..ad60fb2c74f 100644
--- a/spec/features/dashboard_issues_spec.rb
+++ b/spec/features/dashboard_issues_spec.rb
@@ -2,7 +2,7 @@ require 'spec_helper'
describe "Dashboard Issues filtering", feature: true, js: true do
let(:user) { create(:user) }
- let(:project) { create(:project) }
+ let(:project) { create(:empty_project) }
let(:milestone) { create(:milestone, project: project) }
context 'filtering by milestone' do
@@ -10,8 +10,8 @@ describe "Dashboard Issues filtering", feature: true, js: true do
project.team << [user, :master]
login_as(user)
- create(:issue, project: project, author: user, assignee: user)
- create(:issue, project: project, author: user, assignee: user, milestone: milestone)
+ create(:issue, project: project, author: user, assignees: [user])
+ create(:issue, project: project, author: user, assignees: [user], milestone: milestone)
visit_issues
end
diff --git a/spec/features/expand_collapse_diffs_spec.rb b/spec/features/expand_collapse_diffs_spec.rb
index 8c64b050e19..76c77e0bc5f 100644
--- a/spec/features/expand_collapse_diffs_spec.rb
+++ b/spec/features/expand_collapse_diffs_spec.rb
@@ -1,10 +1,8 @@
require 'spec_helper'
feature 'Expand and collapse diffs', js: true, feature: true do
- include WaitForAjax
-
let(:branch) { 'expand-collapse-diffs' }
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
before do
login_as :admin
diff --git a/spec/features/explore/groups_list_spec.rb b/spec/features/explore/groups_list_spec.rb
index 9daaaa8e555..9828cb179a7 100644
--- a/spec/features/explore/groups_list_spec.rb
+++ b/spec/features/explore/groups_list_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
-describe 'Explore Groups page', js: true, feature: true do
- include WaitForAjax
-
+describe 'Explore Groups page', :js, :feature do
let!(:user) { create :user }
let!(:group) { create(:group) }
let!(:public_group) { create(:group, :public) }
@@ -48,19 +46,39 @@ describe 'Explore Groups page', js: true, feature: true do
it 'shows non-archived projects count' do
# Initially project is not archived
expect(find('.js-groups-list-holder .content-list li:first-child .stats span:first-child')).to have_text("1")
-
+
# Archive project
empty_project.archive!
visit explore_groups_path
# Check project count
expect(find('.js-groups-list-holder .content-list li:first-child .stats span:first-child')).to have_text("0")
-
+
# Unarchive project
empty_project.unarchive!
visit explore_groups_path
# Check project count
- expect(find('.js-groups-list-holder .content-list li:first-child .stats span:first-child')).to have_text("1")
+ expect(find('.js-groups-list-holder .content-list li:first-child .stats span:first-child')).to have_text("1")
+ end
+
+ describe 'landing component' do
+ it 'should show a landing component' do
+ expect(page).to have_content('Below you will find all the groups that are public.')
+ end
+
+ it 'should be dismissable' do
+ find('.dismiss-button').click
+
+ expect(page).not_to have_content('Below you will find all the groups that are public.')
+ end
+
+ it 'should persistently not show once dismissed' do
+ find('.dismiss-button').click
+
+ visit explore_groups_path
+
+ expect(page).not_to have_content('Below you will find all the groups that are public.')
+ end
end
end
diff --git a/spec/features/gitlab_flavored_markdown_spec.rb b/spec/features/gitlab_flavored_markdown_spec.rb
index 876f33dd03e..005a029a393 100644
--- a/spec/features/gitlab_flavored_markdown_spec.rb
+++ b/spec/features/gitlab_flavored_markdown_spec.rb
@@ -1,28 +1,28 @@
require 'spec_helper'
describe "GitLab Flavored Markdown", feature: true do
- let(:project) { create(:project) }
+ let(:project) { create(:empty_project) }
let(:issue) { create(:issue, project: project) }
- let(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
let(:fred) do
- u = create(:user, name: "fred")
- project.team << [u, :master]
- u
+ create(:user, name: 'fred') do |user|
+ project.add_master(user)
+ end
end
before do
- allow_any_instance_of(Commit).to receive(:title).
- and_return("fix #{issue.to_reference}\n\nask #{fred.to_reference} for details")
+ login_as(:user)
+ project.add_developer(@user)
end
- let(:commit) { project.commit }
+ describe "for commits" do
+ let(:project) { create(:project, :repository) }
+ let(:commit) { project.commit }
- before do
- login_as :user
- project.team << [@user, :developer]
- end
+ before do
+ allow_any_instance_of(Commit).to receive(:title).
+ and_return("fix #{issue.to_reference}\n\nask #{fred.to_reference} for details")
+ end
- describe "for commits" do
it "renders title in commits#index" do
visit namespace_project_commits_path(project.namespace, project, 'master', limit: 1)
@@ -54,14 +54,16 @@ describe "GitLab Flavored Markdown", feature: true do
before do
@other_issue = create(:issue,
author: @user,
- assignee: @user,
+ assignees: [@user],
project: project)
@issue = create(:issue,
author: @user,
- assignee: @user,
+ assignees: [@user],
project: project,
title: "fix #{@other_issue.to_reference}",
description: "ask #{fred.to_reference} for details")
+
+ @note = create(:note_on_issue, noteable: @issue, project: @issue.project, note: "Hello world")
end
it "renders subject in issues#index" do
@@ -81,17 +83,11 @@ describe "GitLab Flavored Markdown", feature: true do
expect(page).to have_link(fred.to_reference)
end
-
- it "renders updated subject once edited somewhere else in issues#show" do
- visit namespace_project_issue_path(project.namespace, project, @issue)
- @issue.update(title: "fix #{@other_issue.to_reference} and update")
-
- wait_for_vue_resource
- expect(page).to have_text("fix #{@other_issue.to_reference} and update")
- end
end
describe "for merge requests" do
+ let(:project) { create(:project, :repository) }
+
before do
@merge_request = create(:merge_request, source_project: project, target_project: project, title: "fix #{issue.to_reference}")
end
diff --git a/spec/features/global_search_spec.rb b/spec/features/global_search_spec.rb
index f6409e00f22..4b22b07494d 100644
--- a/spec/features/global_search_spec.rb
+++ b/spec/features/global_search_spec.rb
@@ -2,7 +2,7 @@ require 'spec_helper'
feature 'Global search', feature: true do
let(:user) { create(:user) }
- let(:project) { create(:project, namespace: user.namespace) }
+ let(:project) { create(:empty_project, namespace: user.namespace) }
before do
project.team << [user, :master]
diff --git a/spec/features/groups/group_settings_spec.rb b/spec/features/groups/group_settings_spec.rb
new file mode 100644
index 00000000000..cc25db4ad60
--- /dev/null
+++ b/spec/features/groups/group_settings_spec.rb
@@ -0,0 +1,80 @@
+require 'spec_helper'
+
+feature 'Edit group settings', feature: true do
+ given(:user) { create(:user) }
+ given(:group) { create(:group, path: 'foo') }
+
+ background do
+ group.add_owner(user)
+ login_as(user)
+ end
+
+ describe 'when the group path is changed' do
+ let(:new_group_path) { 'bar' }
+ let(:old_group_full_path) { "/#{group.path}" }
+ let(:new_group_full_path) { "/#{new_group_path}" }
+
+ scenario 'the group is accessible via the new path' do
+ update_path(new_group_path)
+ visit new_group_full_path
+ expect(current_path).to eq(new_group_full_path)
+ expect(find('h1.group-title')).to have_content(new_group_path)
+ end
+
+ scenario 'the old group path redirects to the new path' do
+ update_path(new_group_path)
+ visit old_group_full_path
+ expect(current_path).to eq(new_group_full_path)
+ expect(find('h1.group-title')).to have_content(new_group_path)
+ end
+
+ context 'with a subgroup' do
+ given!(:subgroup) { create(:group, parent: group, path: 'subgroup') }
+ given(:old_subgroup_full_path) { "/#{group.path}/#{subgroup.path}" }
+ given(:new_subgroup_full_path) { "/#{new_group_path}/#{subgroup.path}" }
+
+ scenario 'the subgroup is accessible via the new path' do
+ update_path(new_group_path)
+ visit new_subgroup_full_path
+ expect(current_path).to eq(new_subgroup_full_path)
+ expect(find('h1.group-title')).to have_content(subgroup.path)
+ end
+
+ scenario 'the old subgroup path redirects to the new path' do
+ update_path(new_group_path)
+ visit old_subgroup_full_path
+ expect(current_path).to eq(new_subgroup_full_path)
+ expect(find('h1.group-title')).to have_content(subgroup.path)
+ end
+ end
+
+ context 'with a project' do
+ given!(:project) { create(:project, group: group, path: 'project') }
+ given(:old_project_full_path) { "/#{group.path}/#{project.path}" }
+ given(:new_project_full_path) { "/#{new_group_path}/#{project.path}" }
+
+ before(:context) { TestEnv.clean_test_path }
+ after(:example) { TestEnv.clean_test_path }
+
+ scenario 'the project is accessible via the new path' do
+ update_path(new_group_path)
+ visit new_project_full_path
+ expect(current_path).to eq(new_project_full_path)
+ expect(find('h1.project-title')).to have_content(project.name)
+ end
+
+ scenario 'the old project path redirects to the new path' do
+ update_path(new_group_path)
+ visit old_project_full_path
+ expect(current_path).to eq(new_project_full_path)
+ expect(find('h1.project-title')).to have_content(project.name)
+ end
+ end
+ end
+end
+
+def update_path(new_group_path)
+ visit edit_group_path(group)
+ fill_in 'group_path', with: new_group_path
+ click_button 'Save group'
+end
diff --git a/spec/features/groups/issues_spec.rb b/spec/features/groups/issues_spec.rb
index 1b3747c390b..45f57845c74 100644
--- a/spec/features/groups/issues_spec.rb
+++ b/spec/features/groups/issues_spec.rb
@@ -23,4 +23,20 @@ feature 'Group issues page', feature: true do
it_behaves_like "an autodiscoverable RSS feed without a private token"
end
end
+
+ context 'assignee', :js do
+ let(:access_level) { ProjectFeature::ENABLED }
+ let(:user) { user_in_group }
+ let(:user2) { user_outside_group }
+ let(:path) { issues_group_path(group) }
+
+ it 'filters by only group users' do
+ click_button('Assignee')
+
+ wait_for_ajax
+
+ expect(find('.dropdown-menu-assignee')).to have_link(user.name)
+ expect(find('.dropdown-menu-assignee')).not_to have_link(user2.name)
+ end
+ end
end
diff --git a/spec/features/groups/members/sorting_spec.rb b/spec/features/groups/members/sorting_spec.rb
index 608aedd3471..902d3f789ff 100644
--- a/spec/features/groups/members/sorting_spec.rb
+++ b/spec/features/groups/members/sorting_spec.rb
@@ -68,7 +68,7 @@ feature 'Groups > Members > Sorting', feature: true do
expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Name, descending')
end
- scenario 'sorts by recent sign in' do
+ scenario 'sorts by recent sign in', :redis do
visit_members_list(sort: :recent_sign_in)
expect(first_member).to include(owner.name)
@@ -76,7 +76,7 @@ feature 'Groups > Members > Sorting', feature: true do
expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Recent sign in')
end
- scenario 'sorts by oldest sign in' do
+ scenario 'sorts by oldest sign in', :redis do
visit_members_list(sort: :oldest_sign_in)
expect(first_member).to include(developer.name)
diff --git a/spec/features/issuables/issuable_list_spec.rb b/spec/features/issuables/issuable_list_spec.rb
index 3dc872ae520..414838fa22e 100644
--- a/spec/features/issuables/issuable_list_spec.rb
+++ b/spec/features/issuables/issuable_list_spec.rb
@@ -52,6 +52,9 @@ describe 'issuable list', feature: true do
create(:issue, project: project, author: user)
else
create(:merge_request, source_project: project, source_branch: generate(:branch))
+ source_branch = FFaker::Name.name
+ pipeline = create(:ci_empty_pipeline, project: project, ref: source_branch, status: %w(running failed success).sample, sha: 'any')
+ create(:merge_request, title: FFaker::Lorem.sentence, source_project: project, source_branch: source_branch, head_pipeline: pipeline)
end
2.times do
@@ -68,7 +71,7 @@ describe 'issuable list', feature: true do
source_project: project,
source_branch: generate(:branch))
- MergeRequestsClosingIssues.create!(issue: issue, merge_request: merge_request)
+ create(:merge_requests_closing_issues, issue: issue, merge_request: merge_request)
end
end
end
diff --git a/spec/features/issues/award_emoji_spec.rb b/spec/features/issues/award_emoji_spec.rb
index 8e67ab028d7..853632614c4 100644
--- a/spec/features/issues/award_emoji_spec.rb
+++ b/spec/features/issues/award_emoji_spec.rb
@@ -1,14 +1,13 @@
require 'rails_helper'
describe 'Awards Emoji', feature: true do
- include WaitForAjax
include WaitForVueResource
let!(:project) { create(:project, :public) }
let!(:user) { create(:user) }
let(:issue) do
create(:issue,
- assignee: @user,
+ assignees: [user],
project: project)
end
diff --git a/spec/features/issues/award_spec.rb b/spec/features/issues/award_spec.rb
index 401e1ea2b89..08e3f99e29f 100644
--- a/spec/features/issues/award_spec.rb
+++ b/spec/features/issues/award_spec.rb
@@ -6,9 +6,12 @@ feature 'Issue awards', js: true, feature: true do
let(:issue) { create(:issue, project: project) }
describe 'logged in' do
+ include WaitForVueResource
+
before do
login_as(user)
visit namespace_project_issue_path(project.namespace, project, issue)
+ wait_for_vue_resource
end
it 'adds award to issue' do
@@ -38,8 +41,11 @@ feature 'Issue awards', js: true, feature: true do
end
describe 'logged out' do
+ include WaitForVueResource
+
before do
visit namespace_project_issue_path(project.namespace, project, issue)
+ wait_for_vue_resource
end
it 'does not see award menu button' do
diff --git a/spec/features/issues/bulk_assignment_labels_spec.rb b/spec/features/issues/bulk_assignment_labels_spec.rb
index 2f59630b4fb..1de50d6d77e 100644
--- a/spec/features/issues/bulk_assignment_labels_spec.rb
+++ b/spec/features/issues/bulk_assignment_labels_spec.rb
@@ -1,8 +1,6 @@
require 'rails_helper'
feature 'Issues > Labels bulk assignment', feature: true do
- include WaitForAjax
-
let(:user) { create(:user) }
let!(:project) { create(:project) }
let!(:issue1) { create(:issue, project: project, title: "Issue 1") }
diff --git a/spec/features/issues/create_branch_merge_request_spec.rb b/spec/features/issues/create_branch_merge_request_spec.rb
new file mode 100644
index 00000000000..44c19275ae5
--- /dev/null
+++ b/spec/features/issues/create_branch_merge_request_spec.rb
@@ -0,0 +1,91 @@
+require 'rails_helper'
+
+feature 'Create Branch/Merge Request Dropdown on issue page', feature: true, js: true do
+ let(:user) { create(:user) }
+ let!(:project) { create(:project) }
+ let(:issue) { create(:issue, project: project, title: 'Cherry-Coloured Funk') }
+
+ context 'for team members' do
+ before do
+ project.team << [user, :developer]
+ login_as(user)
+ end
+
+ it 'allows creating a merge request from the issue page' do
+ visit namespace_project_issue_path(project.namespace, project, issue)
+
+ select_dropdown_option('create-mr')
+
+ wait_for_ajax
+
+ expect(page).to have_content("created branch 1-cherry-coloured-funk")
+ expect(page).to have_content("mentioned in merge request !1")
+
+ visit namespace_project_merge_request_path(project.namespace, project, MergeRequest.first)
+
+ expect(page).to have_content('WIP: Resolve "Cherry-Coloured Funk"')
+ expect(current_path).to eq(namespace_project_merge_request_path(project.namespace, project, MergeRequest.first))
+ end
+
+ it 'allows creating a branch from the issue page' do
+ visit namespace_project_issue_path(project.namespace, project, issue)
+
+ select_dropdown_option('create-branch')
+
+ wait_for_ajax
+
+ expect(page).to have_selector('.dropdown-toggle-text ', text: '1-cherry-coloured-funk')
+ expect(current_path).to eq namespace_project_tree_path(project.namespace, project, '1-cherry-coloured-funk')
+ end
+
+ context "when there is a referenced merge request" do
+ let!(:note) do
+ create(:note, :on_issue, :system, project: project, noteable: issue,
+ note: "mentioned in #{referenced_mr.to_reference}")
+ end
+
+ let(:referenced_mr) do
+ create(:merge_request, :simple, source_project: project, target_project: project,
+ description: "Fixes #{issue.to_reference}", author: user)
+ end
+
+ before do
+ referenced_mr.cache_merge_request_closes_issues!(user)
+
+ visit namespace_project_issue_path(project.namespace, project, issue)
+ end
+
+ it 'disables the create branch button' do
+ expect(page).to have_css('.create-mr-dropdown-wrap .unavailable:not(.hide)')
+ expect(page).to have_css('.create-mr-dropdown-wrap .available.hide', visible: false)
+ expect(page).to have_content /1 Related Merge Request/
+ end
+ end
+
+ context 'when issue is confidential' do
+ it 'disables the create branch button' do
+ issue = create(:issue, :confidential, project: project)
+
+ visit namespace_project_issue_path(project.namespace, project, issue)
+
+ expect(page).not_to have_css('.create-mr-dropdown-wrap')
+ end
+ end
+ end
+
+ context 'for visitors' do
+ before do
+ visit namespace_project_issue_path(project.namespace, project, issue)
+ end
+
+ it 'shows no buttons' do
+ expect(page).not_to have_selector('.create-mr-dropdown-wrap')
+ end
+ end
+
+ def select_dropdown_option(option)
+ find('.create-mr-dropdown-wrap .dropdown-toggle').click
+ find("li[data-value='#{option}']").click
+ find('.js-create-merge-request').click
+ end
+end
diff --git a/spec/features/issues/create_issue_for_discussions_in_merge_request_spec.rb b/spec/features/issues/create_issue_for_discussions_in_merge_request_spec.rb
index 58f897cba3e..dc13cab2cd1 100644
--- a/spec/features/issues/create_issue_for_discussions_in_merge_request_spec.rb
+++ b/spec/features/issues/create_issue_for_discussions_in_merge_request_spec.rb
@@ -49,7 +49,7 @@ feature 'Resolving all open discussions in a merge request from an issue', featu
end
it 'does not show a link to create a new issue' do
- expect(page).not_to have_link 'open an issue to resolve them later'
+ expect(page).not_to have_link 'Create an issue to resolve them later'
end
end
@@ -59,18 +59,18 @@ feature 'Resolving all open discussions in a merge request from an issue', featu
end
it 'shows a warning that the merge request contains unresolved discussions' do
- expect(page).to have_content 'This merge request has unresolved discussions'
+ expect(page).to have_content 'There are unresolved discussions.'
end
it 'has a link to resolve all discussions by creating an issue' do
page.within '.mr-widget-body' do
- expect(page).to have_link 'open an issue to resolve them later', href: new_namespace_project_issue_path(project.namespace, project, merge_request_to_resolve_discussions_of: merge_request.iid)
+ expect(page).to have_link 'Create an issue to resolve them later', href: new_namespace_project_issue_path(project.namespace, project, merge_request_to_resolve_discussions_of: merge_request.iid)
end
end
context 'creating an issue for discussions' do
before do
- page.click_link 'open an issue to resolve them later', href: new_namespace_project_issue_path(project.namespace, project, merge_request_to_resolve_discussions_of: merge_request.iid)
+ page.click_link 'Create an issue to resolve them later', href: new_namespace_project_issue_path(project.namespace, project, merge_request_to_resolve_discussions_of: merge_request.iid)
end
it_behaves_like 'creating an issue for a discussion'
diff --git a/spec/features/issues/create_issue_for_single_discussion_in_merge_request.rb b/spec/features/issues/create_issue_for_single_discussion_in_merge_request_spec.rb
index 88e2cc60d79..3a5a79e03f4 100644
--- a/spec/features/issues/create_issue_for_single_discussion_in_merge_request.rb
+++ b/spec/features/issues/create_issue_for_single_discussion_in_merge_request_spec.rb
@@ -4,7 +4,7 @@ feature 'Resolve an open discussion in a merge request by creating an issue', fe
let(:user) { create(:user) }
let(:project) { create(:project, only_allow_merge_if_all_discussions_are_resolved: true) }
let(:merge_request) { create(:merge_request, source_project: project) }
- let!(:discussion) { Discussion.for_diff_notes([create(:diff_note_on_merge_request, noteable: merge_request, project: project)]).first }
+ let!(:discussion) { create(:diff_note_on_merge_request, noteable: merge_request, project: project).to_discussion }
describe 'As a user with access to the project' do
before do
@@ -74,8 +74,8 @@ feature 'Resolve an open discussion in a merge request by creating an issue', fe
it 'Shows a notice to ask someone else to resolve the discussions' do
expect(page).to have_content("The discussion at #{merge_request.to_reference}"\
- "(discussion #{discussion.first_note.id}) will stay unresolved."\
- "Ask someone with permission to resolve it.")
+ " (discussion #{discussion.first_note.id}) will stay unresolved."\
+ " Ask someone with permission to resolve it.")
end
end
end
diff --git a/spec/features/issues/filtered_search/dropdown_assignee_spec.rb b/spec/features/issues/filtered_search/dropdown_assignee_spec.rb
index 3d1a9ed1722..0b573d7cef4 100644
--- a/spec/features/issues/filtered_search/dropdown_assignee_spec.rb
+++ b/spec/features/issues/filtered_search/dropdown_assignee_spec.rb
@@ -2,7 +2,6 @@ require 'rails_helper'
describe 'Dropdown assignee', :feature, :js do
include FilteredSearchHelpers
- include WaitForAjax
let!(:project) { create(:empty_project) }
let!(:user) { create(:user, name: 'administrator', username: 'root') }
diff --git a/spec/features/issues/filtered_search/dropdown_author_spec.rb b/spec/features/issues/filtered_search/dropdown_author_spec.rb
index 990e3b3e60c..0579d6c80ab 100644
--- a/spec/features/issues/filtered_search/dropdown_author_spec.rb
+++ b/spec/features/issues/filtered_search/dropdown_author_spec.rb
@@ -2,7 +2,6 @@ require 'rails_helper'
describe 'Dropdown author', js: true, feature: true do
include FilteredSearchHelpers
- include WaitForAjax
let!(:project) { create(:empty_project) }
let!(:user) { create(:user, name: 'administrator', username: 'root') }
diff --git a/spec/features/issues/filtered_search/dropdown_hint_spec.rb b/spec/features/issues/filtered_search/dropdown_hint_spec.rb
index cae01f37b6b..b9a37cfcc22 100644
--- a/spec/features/issues/filtered_search/dropdown_hint_spec.rb
+++ b/spec/features/issues/filtered_search/dropdown_hint_spec.rb
@@ -2,7 +2,6 @@ require 'rails_helper'
describe 'Dropdown hint', :js, :feature do
include FilteredSearchHelpers
- include WaitForAjax
let!(:project) { create(:empty_project) }
let!(:user) { create(:user) }
diff --git a/spec/features/issues/filtered_search/filter_issues_spec.rb b/spec/features/issues/filtered_search/filter_issues_spec.rb
index 6f00066de4d..a8f4e2d7e10 100644
--- a/spec/features/issues/filtered_search/filter_issues_spec.rb
+++ b/spec/features/issues/filtered_search/filter_issues_spec.rb
@@ -3,7 +3,6 @@ require 'spec_helper'
describe 'Filter issues', js: true, feature: true do
include Devise::Test::IntegrationHelpers
include FilteredSearchHelpers
- include WaitForAjax
let!(:group) { create(:group) }
let!(:project) { create(:project, group: group) }
@@ -13,7 +12,7 @@ describe 'Filter issues', js: true, feature: true do
let!(:wontfix) { create(:label, project: project, title: "Won't fix") }
let!(:bug_label) { create(:label, project: project, title: 'bug') }
- let!(:caps_sensitive_label) { create(:label, project: project, title: 'CAPS_sensitive') }
+ let!(:caps_sensitive_label) { create(:label, project: project, title: 'CaPs') }
let!(:milestone) { create(:milestone, title: "8", project: project, start_date: 2.days.ago) }
let!(:multiple_words_label) { create(:label, project: project, title: "Two words") }
@@ -52,15 +51,15 @@ describe 'Filter issues', js: true, feature: true do
create(:issue, project: project, title: "issue with 'single quotes'")
create(:issue, project: project, title: "issue with \"double quotes\"")
create(:issue, project: project, title: "issue with !@\#{$%^&*()-+")
- create(:issue, project: project, title: "issue by assignee", milestone: milestone, author: user, assignee: user)
- create(:issue, project: project, title: "issue by assignee with searchTerm", milestone: milestone, author: user, assignee: user)
+ create(:issue, project: project, title: "issue by assignee", milestone: milestone, author: user, assignees: [user])
+ create(:issue, project: project, title: "issue by assignee with searchTerm", milestone: milestone, author: user, assignees: [user])
issue = create(:issue,
title: "Bug 2",
project: project,
milestone: milestone,
author: user,
- assignee: user)
+ assignees: [user])
issue.labels << bug_label
issue_with_caps_label = create(:issue,
@@ -68,7 +67,7 @@ describe 'Filter issues', js: true, feature: true do
project: project,
milestone: milestone,
author: user,
- assignee: user)
+ assignees: [user])
issue_with_caps_label.labels << caps_sensitive_label
issue_with_everything = create(:issue,
@@ -76,7 +75,7 @@ describe 'Filter issues', js: true, feature: true do
project: project,
milestone: milestone,
author: user,
- assignee: user)
+ assignees: [user])
issue_with_everything.labels << bug_label
issue_with_everything.labels << caps_sensitive_label
diff --git a/spec/features/issues/filtered_search/recent_searches_spec.rb b/spec/features/issues/filtered_search/recent_searches_spec.rb
index f506065a242..08fe3b4553b 100644
--- a/spec/features/issues/filtered_search/recent_searches_spec.rb
+++ b/spec/features/issues/filtered_search/recent_searches_spec.rb
@@ -2,7 +2,6 @@ require 'spec_helper'
describe 'Recent searches', js: true, feature: true do
include FilteredSearchHelpers
- include WaitForAjax
let!(:group) { create(:group) }
let!(:project) { create(:project, group: group) }
diff --git a/spec/features/issues/filtered_search/search_bar_spec.rb b/spec/features/issues/filtered_search/search_bar_spec.rb
index 28137f11b92..3ea95aed0a6 100644
--- a/spec/features/issues/filtered_search/search_bar_spec.rb
+++ b/spec/features/issues/filtered_search/search_bar_spec.rb
@@ -2,7 +2,6 @@ require 'rails_helper'
describe 'Search bar', js: true, feature: true do
include FilteredSearchHelpers
- include WaitForAjax
let!(:project) { create(:empty_project) }
let!(:user) { create(:user) }
diff --git a/spec/features/issues/form_spec.rb b/spec/features/issues/form_spec.rb
index 755992069ff..095cbb65c16 100644
--- a/spec/features/issues/form_spec.rb
+++ b/spec/features/issues/form_spec.rb
@@ -1,7 +1,9 @@
require 'rails_helper'
-describe 'New/edit issue', feature: true, js: true do
+describe 'New/edit issue', :feature, :js do
include GitlabRoutingHelper
+ include ActionView::Helpers::JavaScriptHelper
+ include WaitForAjax
let!(:project) { create(:project) }
let!(:user) { create(:user)}
@@ -9,7 +11,7 @@ describe 'New/edit issue', feature: true, js: true do
let!(:milestone) { create(:milestone, project: project) }
let!(:label) { create(:label, project: project) }
let!(:label2) { create(:label, project: project) }
- let!(:issue) { create(:issue, project: project, assignee: user, milestone: milestone) }
+ let!(:issue) { create(:issue, project: project, assignees: [user], milestone: milestone) }
before do
project.team << [user, :master]
@@ -22,23 +24,67 @@ describe 'New/edit issue', feature: true, js: true do
visit new_namespace_project_issue_path(project.namespace, project)
end
+ describe 'multiple assignees' do
+ before do
+ click_button 'Unassigned'
+
+ wait_for_ajax
+ end
+
+ it 'unselects other assignees when unassigned is selected' do
+ page.within '.dropdown-menu-user' do
+ click_link user2.name
+ end
+
+ page.within '.dropdown-menu-user' do
+ click_link 'Unassigned'
+ end
+
+ page.within '.js-assignee-search' do
+ expect(page).to have_content 'Unassigned'
+ end
+
+ expect(find('input[name="issue[assignee_ids][]"]', visible: false).value).to match('0')
+ end
+
+ it 'toggles assign to me when current user is selected and unselected' do
+ page.within '.dropdown-menu-user' do
+ click_link user.name
+ end
+
+ expect(find('a', text: 'Assign to me', visible: false)).not_to be_visible
+
+ page.within '.dropdown-menu-user' do
+ click_link user.name
+ end
+
+ expect(find('a', text: 'Assign to me')).to be_visible
+ end
+ end
+
it 'allows user to create new issue' do
fill_in 'issue_title', with: 'title'
fill_in 'issue_description', with: 'title'
expect(find('a', text: 'Assign to me')).to be_visible
- click_button 'Assignee'
+ click_button 'Unassigned'
+
+ wait_for_ajax
+
page.within '.dropdown-menu-user' do
click_link user2.name
end
- expect(find('input[name="issue[assignee_id]"]', visible: false).value).to match(user2.id.to_s)
+ expect(find('input[name="issue[assignee_ids][]"]', visible: false).value).to match(user2.id.to_s)
page.within '.js-assignee-search' do
expect(page).to have_content user2.name
end
expect(find('a', text: 'Assign to me')).to be_visible
click_link 'Assign to me'
- expect(find('input[name="issue[assignee_id]"]', visible: false).value).to match(user.id.to_s)
+ assignee_ids = page.all('input[name="issue[assignee_ids][]"]', visible: false)
+
+ expect(assignee_ids[0].value).to match(user.id.to_s)
+
page.within '.js-assignee-search' do
expect(page).to have_content user.name
end
@@ -68,7 +114,7 @@ describe 'New/edit issue', feature: true, js: true do
page.within '.issuable-sidebar' do
page.within '.assignee' do
- expect(page).to have_content user.name
+ expect(page).to have_content "Assignee"
end
page.within '.milestone' do
@@ -105,6 +151,29 @@ describe 'New/edit issue', feature: true, js: true do
expect(find('.js-label-select')).to have_content('Labels')
end
+
+ it 'correctly updates the selected user when changing assignee' do
+ click_button 'Unassigned'
+
+ wait_for_ajax
+
+ page.within '.dropdown-menu-user' do
+ click_link user.name
+ end
+
+ expect(find('input[name="issue[assignee_ids][]"]', visible: false).value).to match(user.id.to_s)
+ expect(find('.dropdown-menu-user a.is-active').first(:xpath, '..')['data-user-id']).to eq(user.id.to_s)
+ # check the ::before pseudo element to ensure checkmark icon is present
+ expect(before_for_selector('.dropdown-menu-selectable a.is-active')).not_to eq('')
+ expect(before_for_selector('.dropdown-menu-selectable a:not(.is-active)')).to eq('')
+
+ page.within '.dropdown-menu-user' do
+ click_link user2.name
+ end
+
+ expect(find('input[name="issue[assignee_ids][]"]', visible: false).value).to match(user2.id.to_s)
+ expect(find('.dropdown-menu-user a.is-active').first(:xpath, '..')['data-user-id']).to eq(user2.id.to_s)
+ end
end
context 'edit issue' do
@@ -113,7 +182,7 @@ describe 'New/edit issue', feature: true, js: true do
end
it 'allows user to update issue' do
- expect(find('input[name="issue[assignee_id]"]', visible: false).value).to match(user.id.to_s)
+ expect(find('input[name="issue[assignee_ids][]"]', visible: false).value).to match(user.id.to_s)
expect(find('input[name="issue[milestone_id]"]', visible: false).value).to match(milestone.id.to_s)
expect(find('a', text: 'Assign to me', visible: false)).not_to be_visible
@@ -154,4 +223,14 @@ describe 'New/edit issue', feature: true, js: true do
end
end
end
+
+ def before_for_selector(selector)
+ js = <<-JS.strip_heredoc
+ (function(selector) {
+ var el = document.querySelector(selector);
+ return window.getComputedStyle(el, '::before').getPropertyValue('content');
+ })("#{escape_javascript(selector)}")
+ JS
+ page.evaluate_script(js)
+ end
end
diff --git a/spec/features/issues/gfm_autocomplete_spec.rb b/spec/features/issues/gfm_autocomplete_spec.rb
index 7135565294b..ad29911248f 100644
--- a/spec/features/issues/gfm_autocomplete_spec.rb
+++ b/spec/features/issues/gfm_autocomplete_spec.rb
@@ -1,7 +1,6 @@
require 'rails_helper'
feature 'GFM autocomplete', feature: true, js: true do
- include WaitForAjax
let(:user) { create(:user, name: '💃speciąl someone💃', username: 'someone.special') }
let(:project) { create(:project) }
let(:label) { create(:label, project: project, title: 'special+') }
@@ -46,6 +45,33 @@ feature 'GFM autocomplete', feature: true, js: true do
expect(find('#at-view-58')).not_to have_selector('.cur:first-of-type')
end
+ it 'does not open autocomplete menu when ":" is prefixed by a number and letters' do
+ note = find('#note_note')
+
+ # Number.
+ page.within '.timeline-content-form' do
+ note.native.send_keys('7:')
+ end
+
+ expect(page).not_to have_selector('.atwho-view')
+
+ # ASCII letter.
+ page.within '.timeline-content-form' do
+ note.set('')
+ note.native.send_keys('w:')
+ end
+
+ expect(page).not_to have_selector('.atwho-view')
+
+ # Non-ASCII letter.
+ page.within '.timeline-content-form' do
+ note.set('')
+ note.native.send_keys('Ё:')
+ end
+
+ expect(page).not_to have_selector('.atwho-view')
+ end
+
it 'selects the first item for assignee dropdowns' do
page.within '.timeline-content-form' do
find('#note_note').native.send_keys('')
diff --git a/spec/features/issues/issue_sidebar_spec.rb b/spec/features/issues/issue_sidebar_spec.rb
index 85585587fb1..0de0f93089a 100644
--- a/spec/features/issues/issue_sidebar_spec.rb
+++ b/spec/features/issues/issue_sidebar_spec.rb
@@ -1,10 +1,10 @@
require 'rails_helper'
feature 'Issue Sidebar', feature: true do
- include WaitForAjax
include MobileHelpers
- let(:project) { create(:project, :public) }
+ let(:group) { create(:group, :nested) }
+ let(:project) { create(:project, :public, namespace: group) }
let(:issue) { create(:issue, project: project) }
let!(:user) { create(:user)}
let!(:label) { create(:label, project: project, title: 'bug') }
@@ -42,6 +42,21 @@ feature 'Issue Sidebar', feature: true do
expect(page).to have_content(user2.name)
end
end
+
+ it 'assigns yourself' do
+ find('.block.assignee .dropdown-menu-toggle').click
+
+ click_button 'assign yourself'
+
+ wait_for_ajax
+
+ find('.block.assignee .edit-link').click
+
+ page.within '.dropdown-menu-user' do
+ expect(page.find('.dropdown-header')).to be_visible
+ expect(page.find('.dropdown-menu-user-link.is-active')).to have_content(user.name)
+ end
+ end
end
context 'as a allowed user' do
@@ -56,10 +71,12 @@ feature 'Issue Sidebar', feature: true do
# Resize the window
resize_screen_sm
# Make sure the sidebar is collapsed
+ find(sidebar_selector)
expect(page).to have_css(sidebar_selector)
# Once is collapsed let's open the sidebard and reload
open_issue_sidebar
refresh
+ find(sidebar_selector)
expect(page).to have_css(sidebar_selector)
# Restore the window size as it was including the sidebar
restore_window_size
@@ -150,9 +167,7 @@ feature 'Issue Sidebar', feature: true do
end
def open_issue_sidebar
- page.within('aside.right-sidebar.right-sidebar-collapsed') do
- find('.js-sidebar-toggle').click
- sleep 1
- end
+ find('aside.right-sidebar.right-sidebar-collapsed .js-sidebar-toggle').trigger('click')
+ find('aside.right-sidebar.right-sidebar-expanded')
end
end
diff --git a/spec/features/issues/new_branch_button_spec.rb b/spec/features/issues/new_branch_button_spec.rb
deleted file mode 100644
index c0ab42c6822..00000000000
--- a/spec/features/issues/new_branch_button_spec.rb
+++ /dev/null
@@ -1,62 +0,0 @@
-require 'rails_helper'
-
-feature 'Start new branch from an issue', feature: true, js: true do
- let!(:project) { create(:project) }
- let!(:issue) { create(:issue, project: project) }
- let!(:user) { create(:user)}
-
- context "for team members" do
- before do
- project.team << [user, :master]
- login_as(user)
- end
-
- it 'shows the new branch button' do
- visit namespace_project_issue_path(project.namespace, project, issue)
-
- expect(page).to have_css('#new-branch .available')
- end
-
- context "when there is a referenced merge request" do
- let!(:note) do
- create(:note, :on_issue, :system, project: project, noteable: issue,
- note: "mentioned in #{referenced_mr.to_reference}")
- end
-
- let(:referenced_mr) do
- create(:merge_request, :simple, source_project: project, target_project: project,
- description: "Fixes #{issue.to_reference}", author: user)
- end
-
- before do
- referenced_mr.cache_merge_request_closes_issues!(user)
-
- visit namespace_project_issue_path(project.namespace, project, issue)
- end
-
- it "hides the new branch button" do
- expect(page).to have_css('#new-branch .unavailable')
- expect(page).not_to have_css('#new-branch .available')
- expect(page).to have_content /1 Related Merge Request/
- end
- end
-
- context 'when issue is confidential' do
- it 'hides the new branch button' do
- issue = create(:issue, :confidential, project: project)
-
- visit namespace_project_issue_path(project.namespace, project, issue)
-
- expect(page).not_to have_css('#new-branch')
- end
- end
- end
-
- context 'for visitors' do
- it 'shows no buttons' do
- visit namespace_project_issue_path(project.namespace, project, issue)
-
- expect(page).not_to have_css('#new-branch')
- end
- end
-end
diff --git a/spec/features/issues/note_polling_spec.rb b/spec/features/issues/note_polling_spec.rb
index 378f6de1a78..58b3215f14c 100644
--- a/spec/features/issues/note_polling_spec.rb
+++ b/spec/features/issues/note_polling_spec.rb
@@ -4,14 +4,77 @@ feature 'Issue notes polling', :feature, :js do
let(:project) { create(:empty_project, :public) }
let(:issue) { create(:issue, project: project) }
- before do
- visit namespace_project_issue_path(project.namespace, project, issue)
+ describe 'creates' do
+ before do
+ visit namespace_project_issue_path(project.namespace, project, issue)
+ end
+
+ it 'displays the new comment' do
+ note = create(:note, noteable: issue, project: project, note: 'Looks good!')
+ page.execute_script('notes.refresh();')
+
+ expect(page).to have_selector("#note_#{note.id}", text: 'Looks good!')
+ end
end
- it 'should display the new comment' do
- note = create(:note, noteable: issue, project: project, note: 'Looks good!')
- page.execute_script('notes.refresh();')
+ describe 'updates' do
+ let(:user) { create(:user) }
+ let(:note_text) { "Hello World" }
+ let(:updated_text) { "Bye World" }
+ let!(:existing_note) { create(:note, noteable: issue, project: project, author: user, note: note_text) }
+
+ before do
+ login_as(user)
+ visit namespace_project_issue_path(project.namespace, project, issue)
+ end
+
+ it 'displays the updated content' do
+ expect(page).to have_selector("#note_#{existing_note.id}", text: note_text)
+
+ update_note(existing_note, updated_text)
+
+ expect(page).to have_selector("#note_#{existing_note.id}", text: updated_text)
+ end
+
+ it 'when editing but have not changed anything, and an update comes in, show the updated content in the textarea' do
+ find("#note_#{existing_note.id} .js-note-edit").click
+
+ expect(page).to have_field("note[note]", with: note_text)
+
+ update_note(existing_note, updated_text)
+
+ expect(page).to have_field("note[note]", with: updated_text)
+ end
+
+ it 'when editing but you changed some things, and an update comes in, show a warning' do
+ find("#note_#{existing_note.id} .js-note-edit").click
- expect(page).to have_selector("#note_#{note.id}", text: 'Looks good!')
+ expect(page).to have_field("note[note]", with: note_text)
+
+ find("#note_#{existing_note.id} .js-note-text").set('something random')
+
+ update_note(existing_note, updated_text)
+
+ expect(page).to have_selector(".alert")
+ end
+
+ it 'when editing but you changed some things, an update comes in, and you press cancel, show the updated content' do
+ find("#note_#{existing_note.id} .js-note-edit").click
+
+ expect(page).to have_field("note[note]", with: note_text)
+
+ find("#note_#{existing_note.id} .js-note-text").set('something random')
+
+ update_note(existing_note, updated_text)
+
+ find("#note_#{existing_note.id} .note-edit-cancel").click
+
+ expect(page).to have_selector("#note_#{existing_note.id}", text: updated_text)
+ end
+ end
+
+ def update_note(note, new_text)
+ note.update(note: new_text)
+ page.execute_script('notes.refresh();')
end
end
diff --git a/spec/features/issues/notes_on_issues_spec.rb b/spec/features/issues/notes_on_issues_spec.rb
new file mode 100644
index 00000000000..a4035324d2b
--- /dev/null
+++ b/spec/features/issues/notes_on_issues_spec.rb
@@ -0,0 +1,77 @@
+require 'spec_helper'
+
+describe 'Create notes on issues', :js, :feature do
+ let(:user) { create(:user) }
+
+ shared_examples 'notes with reference' do
+ let(:issue) { create(:issue, project: project) }
+ let(:note_text) { "Check #{mention.to_reference}" }
+
+ before do
+ project.team << [user, :developer]
+ login_as(user)
+ visit namespace_project_issue_path(project.namespace, project, issue)
+
+ fill_in 'note[note]', with: note_text
+ click_button 'Comment'
+
+ wait_for_ajax
+ end
+
+ it 'creates a note with reference and cross references the issue' do
+ page.within('div#notes li.note div.note-text') do
+ expect(page).to have_content(note_text)
+ expect(page.find('a')).to have_content(mention.to_reference)
+ end
+
+ find('div#notes li.note div.note-text a').click
+
+ page.within('div#notes li.note .system-note-message') do
+ expect(page).to have_content('mentioned in issue')
+ expect(page.find('a')).to have_content(issue.to_reference)
+ end
+ end
+ end
+
+ context 'mentioning issue on a private project' do
+ it_behaves_like 'notes with reference' do
+ let(:project) { create(:project, :private) }
+ let(:mention) { create(:issue, project: project) }
+ end
+ end
+
+ context 'mentioning issue on an internal project' do
+ it_behaves_like 'notes with reference' do
+ let(:project) { create(:project, :internal) }
+ let(:mention) { create(:issue, project: project) }
+ end
+ end
+
+ context 'mentioning issue on a public project' do
+ it_behaves_like 'notes with reference' do
+ let(:project) { create(:project, :public) }
+ let(:mention) { create(:issue, project: project) }
+ end
+ end
+
+ context 'mentioning merge request on a private project' do
+ it_behaves_like 'notes with reference' do
+ let(:project) { create(:project, :private) }
+ let(:mention) { create(:merge_request, source_project: project) }
+ end
+ end
+
+ context 'mentioning merge request on an internal project' do
+ it_behaves_like 'notes with reference' do
+ let(:project) { create(:project, :internal) }
+ let(:mention) { create(:merge_request, source_project: project) }
+ end
+ end
+
+ context 'mentioning merge request on a public project' do
+ it_behaves_like 'notes with reference' do
+ let(:project) { create(:project, :public) }
+ let(:mention) { create(:merge_request, source_project: project) }
+ end
+ end
+end
diff --git a/spec/features/issues/update_issues_spec.rb b/spec/features/issues/update_issues_spec.rb
index ae5da3877a8..b250fa2ed3c 100644
--- a/spec/features/issues/update_issues_spec.rb
+++ b/spec/features/issues/update_issues_spec.rb
@@ -1,8 +1,6 @@
require 'rails_helper'
feature 'Multiple issue updating from issues#index', feature: true do
- include WaitForAjax
-
let!(:project) { create(:project) }
let!(:issue) { create(:issue, project: project) }
let!(:user) { create(:user)}
@@ -101,7 +99,7 @@ feature 'Multiple issue updating from issues#index', feature: true do
end
def create_assigned
- create(:issue, project: project, assignee: user)
+ create(:issue, project: project, assignees: [user])
end
def create_with_milestone
diff --git a/spec/features/issues/user_uses_slash_commands_spec.rb b/spec/features/issues/user_uses_slash_commands_spec.rb
index 0a9cd11ad6e..4cd6c1171ac 100644
--- a/spec/features/issues/user_uses_slash_commands_spec.rb
+++ b/spec/features/issues/user_uses_slash_commands_spec.rb
@@ -2,7 +2,6 @@ require 'rails_helper'
feature 'Issues > User uses slash commands', feature: true, js: true do
include SlashCommandsHelpers
- include WaitForAjax
it_behaves_like 'issuable record that supports slash commands in its description and notes', :issue do
let(:issuable) { create(:issue, project: project) }
diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb
index 55eca187f6c..fdd78600a1d 100644
--- a/spec/features/issues_spec.rb
+++ b/spec/features/issues_spec.rb
@@ -4,29 +4,21 @@ describe 'Issues', feature: true do
include DropzoneHelper
include IssueHelpers
include SortingHelper
- include WaitForAjax
- let(:project) { create(:project, :public) }
+ let(:project) { create(:empty_project, :public) }
before do
login_as :user
user2 = create(:user)
project.team << [[@user, user2], :developer]
-
- project.repository.create_file(
- @user,
- '.gitlab/issue_templates/bug.md',
- 'this is a test "bug" template',
- message: 'added issue template',
- branch_name: 'master')
end
describe 'Edit issue' do
let!(:issue) do
create(:issue,
author: @user,
- assignee: @user,
+ assignees: [@user],
project: project)
end
@@ -51,7 +43,7 @@ describe 'Issues', feature: true do
let!(:issue) do
create(:issue,
author: @user,
- assignee: @user,
+ assignees: [@user],
project: project)
end
@@ -69,7 +61,7 @@ describe 'Issues', feature: true do
expect(page).to have_content 'No assignee - assign yourself'
end
- expect(issue.reload.assignee).to be_nil
+ expect(issue.reload.assignees).to be_empty
end
end
@@ -146,7 +138,7 @@ describe 'Issues', feature: true do
describe 'Issue info' do
it 'excludes award_emoji from comment count' do
- issue = create(:issue, author: @user, assignee: @user, project: project, title: 'foobar')
+ issue = create(:issue, author: @user, assignees: [@user], project: project, title: 'foobar')
create(:award_emoji, awardable: issue)
visit namespace_project_issues_path(project.namespace, project, assignee_id: @user.id)
@@ -161,14 +153,14 @@ describe 'Issues', feature: true do
%w(foobar barbaz gitlab).each do |title|
create(:issue,
author: @user,
- assignee: @user,
+ assignees: [@user],
project: project,
title: title)
end
@issue = Issue.find_by(title: 'foobar')
@issue.milestone = create(:milestone, project: project)
- @issue.assignee = nil
+ @issue.assignees = []
@issue.save
end
@@ -359,9 +351,9 @@ describe 'Issues', feature: true do
let(:user2) { create(:user) }
before do
- foo.assignee = user2
+ foo.assignees << user2
foo.save
- bar.assignee = user2
+ bar.assignees << user2
bar.save
end
@@ -378,7 +370,7 @@ describe 'Issues', feature: true do
end
describe 'when I want to reset my incoming email token' do
- let(:project1) { create(:project, namespace: @user.namespace) }
+ let(:project1) { create(:empty_project, namespace: @user.namespace) }
let!(:issue) { create(:issue, project: project1) }
before do
@@ -404,7 +396,7 @@ describe 'Issues', feature: true do
end
describe 'update labels from issue#show', js: true do
- let(:issue) { create(:issue, project: project, author: @user, assignee: @user) }
+ let(:issue) { create(:issue, project: project, author: @user, assignees: [@user]) }
let!(:label) { create(:label, project: project) }
before do
@@ -423,7 +415,7 @@ describe 'Issues', feature: true do
end
describe 'update assignee from issue#show' do
- let(:issue) { create(:issue, project: project, author: @user, assignee: @user) }
+ let(:issue) { create(:issue, project: project, author: @user, assignees: [@user]) }
context 'by authorized user' do
it 'allows user to select unassigned', js: true do
@@ -434,10 +426,14 @@ describe 'Issues', feature: true do
click_link 'Edit'
click_link 'Unassigned'
+ first('.title').click
expect(page).to have_content 'No assignee'
end
- expect(issue.reload.assignee).to be_nil
+ # wait_for_ajax does not work with vue-resource at the moment
+ sleep 1
+
+ expect(issue.reload.assignees).to be_empty
end
it 'allows user to select an assignee', js: true do
@@ -469,14 +465,18 @@ describe 'Issues', feature: true do
click_link 'Edit'
click_link @user.name
- page.within '.value' do
+ find('.dropdown-menu-toggle').click
+
+ page.within '.value .author' do
expect(page).to have_content @user.name
end
click_link 'Edit'
click_link @user.name
- page.within '.value' do
+ find('.dropdown-menu-toggle').click
+
+ page.within '.value .assign-yourself' do
expect(page).to have_content "No assignee"
end
end
@@ -495,7 +495,7 @@ describe 'Issues', feature: true do
login_with guest
visit namespace_project_issue_path(project.namespace, project, issue)
- expect(page).to have_content issue.assignee.name
+ expect(page).to have_content issue.assignees.first.name
end
end
end
@@ -566,13 +566,15 @@ describe 'Issues', feature: true do
let(:user2) { create(:user) }
before do
- issue.assignee = user2
+ issue.assignees << user2
issue.save
end
end
end
describe 'new issue' do
+ let!(:issue) { create(:issue, project: project) }
+
context 'by unauthenticated user' do
before do
logout
@@ -610,7 +612,16 @@ describe 'Issues', feature: true do
end
context 'form filled by URL parameters' do
+ let(:project) { create(:project, :public, :repository) }
+
before do
+ project.repository.create_file(
+ @user,
+ '.gitlab/issue_templates/bug.md',
+ 'this is a test "bug" template',
+ message: 'added issue template',
+ branch_name: 'master')
+
visit new_namespace_project_issue_path(project.namespace, project, issuable_template: 'bug')
end
@@ -654,7 +665,7 @@ describe 'Issues', feature: true do
describe 'due date' do
context 'update due on issue#show', js: true do
- let(:issue) { create(:issue, project: project, author: @user, assignee: @user) }
+ let(:issue) { create(:issue, project: project, author: @user, assignees: [@user]) }
before do
visit namespace_project_issue_path(project.namespace, project, issue)
@@ -701,7 +712,7 @@ describe 'Issues', feature: true do
include WaitForVueResource
it 'updates the title', js: true do
- issue = create(:issue, author: @user, assignee: @user, project: project, title: 'new title')
+ issue = create(:issue, author: @user, assignees: [@user], project: project, title: 'new title')
visit namespace_project_issue_path(project.namespace, project, issue)
diff --git a/spec/features/login_spec.rb b/spec/features/login_spec.rb
index 11d417c253d..c82e8c03343 100644
--- a/spec/features/login_spec.rb
+++ b/spec/features/login_spec.rb
@@ -41,7 +41,7 @@ feature 'Login', feature: true do
expect(page).to have_content('Your account has been blocked.')
end
- it 'does not update Devise trackable attributes' do
+ it 'does not update Devise trackable attributes', :redis do
user = create(:user, :blocked)
expect { login_with(user) }.not_to change { user.reload.sign_in_count }
@@ -55,7 +55,7 @@ feature 'Login', feature: true do
expect(page).to have_content('Invalid Login or password.')
end
- it 'does not update Devise trackable attributes' do
+ it 'does not update Devise trackable attributes', :redis do
expect { login_with(User.ghost) }.not_to change { User.ghost.reload.sign_in_count }
end
end
diff --git a/spec/features/markdown_spec.rb b/spec/features/markdown_spec.rb
index 894df13a2dc..ba930de937d 100644
--- a/spec/features/markdown_spec.rb
+++ b/spec/features/markdown_spec.rb
@@ -26,7 +26,7 @@ require 'erb'
describe 'GitLab Markdown', feature: true do
include Capybara::Node::Matchers
- include GitlabMarkdownHelper
+ include MarkupHelper
include MarkdownMatchers
# Sometimes it can be useful to see the parsed output of the Markdown document
diff --git a/spec/features/merge_requests/assign_issues_spec.rb b/spec/features/merge_requests/assign_issues_spec.rb
index 43cc6f2a2a7..b306e2f5f75 100644
--- a/spec/features/merge_requests/assign_issues_spec.rb
+++ b/spec/features/merge_requests/assign_issues_spec.rb
@@ -18,7 +18,7 @@ feature 'Merge request issue assignment', js: true, feature: true do
end
context 'logged in as author' do
- scenario 'updates related issues' do
+ it 'updates related issues' do
visit_merge_request
click_link "Assign yourself to these issues"
@@ -33,7 +33,7 @@ feature 'Merge request issue assignment', js: true, feature: true do
end
it "doesn't display if related issues are already assigned" do
- [issue1, issue2].each { |issue| issue.update!(assignee: user) }
+ [issue1, issue2].each { |issue| issue.update!(assignees: [user]) }
visit_merge_request
diff --git a/spec/features/merge_requests/check_if_mergeable_with_unresolved_discussions_spec.rb b/spec/features/merge_requests/check_if_mergeable_with_unresolved_discussions_spec.rb
index 77b7ba4ac7a..fa306c02a43 100644
--- a/spec/features/merge_requests/check_if_mergeable_with_unresolved_discussions_spec.rb
+++ b/spec/features/merge_requests/check_if_mergeable_with_unresolved_discussions_spec.rb
@@ -19,8 +19,8 @@ feature 'Check if mergeable with unresolved discussions', js: true, feature: tru
it 'does not allow to merge' do
visit_merge_request(merge_request)
- expect(page).not_to have_button 'Accept merge request'
- expect(page).to have_content('This merge request has unresolved discussions')
+ expect(page).not_to have_button 'Merge'
+ expect(page).to have_content('There are unresolved discussions.')
end
end
@@ -32,7 +32,7 @@ feature 'Check if mergeable with unresolved discussions', js: true, feature: tru
it 'allows MR to be merged' do
visit_merge_request(merge_request)
- expect(page).to have_button 'Accept merge request'
+ expect(page).to have_button 'Merge'
end
end
end
@@ -46,7 +46,7 @@ feature 'Check if mergeable with unresolved discussions', js: true, feature: tru
it 'does not allow to merge' do
visit_merge_request(merge_request)
- expect(page).to have_button 'Accept merge request'
+ expect(page).to have_button 'Merge'
end
end
@@ -58,7 +58,7 @@ feature 'Check if mergeable with unresolved discussions', js: true, feature: tru
it 'allows MR to be merged' do
visit_merge_request(merge_request)
- expect(page).to have_button 'Accept merge request'
+ expect(page).to have_button 'Merge'
end
end
end
diff --git a/spec/features/merge_requests/cherry_pick_spec.rb b/spec/features/merge_requests/cherry_pick_spec.rb
index dfe7c910a10..6ba681e36f7 100644
--- a/spec/features/merge_requests/cherry_pick_spec.rb
+++ b/spec/features/merge_requests/cherry_pick_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe 'Cherry-pick Merge Requests' do
+describe 'Cherry-pick Merge Requests', js: true do
let(:user) { create(:user) }
let(:group) { create(:group) }
let(:project) { create(:project, namespace: group) }
diff --git a/spec/features/merge_requests/closes_issues_spec.rb b/spec/features/merge_requests/closes_issues_spec.rb
index eafcab6a0d7..ee0880a1e2f 100644
--- a/spec/features/merge_requests/closes_issues_spec.rb
+++ b/spec/features/merge_requests/closes_issues_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
-feature 'Merge Request closing issues message', feature: true do
+feature 'Merge Request closing issues message', feature: true, js: true do
+ include WaitForAjax
+
let(:user) { create(:user) }
let(:project) { create(:project, :public) }
let(:issue_1) { create(:issue, project: project)}
@@ -23,6 +25,7 @@ feature 'Merge Request closing issues message', feature: true do
login_as user
visit namespace_project_merge_request_path(project.namespace, project, merge_request)
+ wait_for_ajax
end
context 'not closing or mentioning any issue' do
@@ -35,7 +38,7 @@ feature 'Merge Request closing issues message', feature: true do
let(:merge_request_description) { "Description\n\nclosing #{issue_1.to_reference}, #{issue_2.to_reference}" }
it 'does not display closing issue message' do
- expect(page).to have_content("Accepting this merge request will close issues #{issue_1.to_reference} and #{issue_2.to_reference}")
+ expect(page).to have_content("Closes issues #{issue_1.to_reference} and #{issue_2.to_reference}")
end
end
@@ -51,7 +54,8 @@ feature 'Merge Request closing issues message', feature: true do
let(:merge_request_title) { "closes #{issue_1.to_reference}\n\n refers to #{issue_2.to_reference}" }
it 'does not display closing issue message' do
- expect(page).to have_content("Accepting this merge request will close issue #{issue_1.to_reference}. Issue #{issue_2.to_reference} is mentioned but will not be closed.")
+ expect(page).to have_content("Closes issue #{issue_1.to_reference}.")
+ expect(page).to have_content("Issue #{issue_2.to_reference} is mentioned but will not be closed.")
end
end
@@ -59,7 +63,7 @@ feature 'Merge Request closing issues message', feature: true do
let(:merge_request_title) { "closing #{issue_1.to_reference}, #{issue_2.to_reference}" }
it 'does not display closing issue message' do
- expect(page).to have_content("Accepting this merge request will close issues #{issue_1.to_reference} and #{issue_2.to_reference}")
+ expect(page).to have_content("Closes issues #{issue_1.to_reference} and #{issue_2.to_reference}")
end
end
@@ -75,7 +79,8 @@ feature 'Merge Request closing issues message', feature: true do
let(:merge_request_title) { "closes #{issue_1.to_reference}\n\n refers to #{issue_2.to_reference}" }
it 'does not display closing issue message' do
- expect(page).to have_content("Accepting this merge request will close issue #{issue_1.to_reference}. Issue #{issue_2.to_reference} is mentioned but will not be closed.")
+ expect(page).to have_content("Closes issue #{issue_1.to_reference}. Issue #{issue_2.to_reference} is mentioned but will not be closed.")
+ expect(page).to have_content("Issue #{issue_2.to_reference} is mentioned but will not be closed.")
end
end
end
diff --git a/spec/features/merge_requests/conflicts_spec.rb b/spec/features/merge_requests/conflicts_spec.rb
index 18508a44184..04b7593ce68 100644
--- a/spec/features/merge_requests/conflicts_spec.rb
+++ b/spec/features/merge_requests/conflicts_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
feature 'Merge request conflict resolution', js: true, feature: true do
- include WaitForAjax
-
let(:user) { create(:user) }
let(:project) { create(:project) }
@@ -153,7 +151,7 @@ feature 'Merge request conflict resolution', js: true, feature: true do
'conflict-too-large' => 'when the conflicts contain a large file',
'conflict-binary-file' => 'when the conflicts contain a binary file',
'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',
+ 'conflict-non-utf8' => 'when the conflicts contain a non-UTF-8 file'
}.freeze
UNRESOLVABLE_CONFLICTS.each do |source_branch, description|
diff --git a/spec/features/merge_requests/create_new_mr_spec.rb b/spec/features/merge_requests/create_new_mr_spec.rb
index 16b09933bda..f1b3e7f158c 100644
--- a/spec/features/merge_requests/create_new_mr_spec.rb
+++ b/spec/features/merge_requests/create_new_mr_spec.rb
@@ -34,7 +34,7 @@ feature 'Create New Merge Request', feature: true, js: true do
expect(page).to have_content('Target branch')
first('.js-target-branch').click
- first('.dropdown-target-branch .dropdown-content a', text: 'v1.1.0').click
+ find('.dropdown-target-branch .dropdown-content a', text: 'v1.1.0', match: :first).click
expect(page).to have_content "b83d6e3"
end
diff --git a/spec/features/merge_requests/created_from_fork_spec.rb b/spec/features/merge_requests/created_from_fork_spec.rb
index 18833ba7266..bf34c99b92a 100644
--- a/spec/features/merge_requests/created_from_fork_spec.rb
+++ b/spec/features/merge_requests/created_from_fork_spec.rb
@@ -31,7 +31,7 @@ feature 'Merge request created from fork' do
fork_project.destroy!
end
- scenario 'user can access merge request' do
+ scenario 'user can access merge request', js: true do
visit_merge_request(merge_request)
expect(page).to have_content 'Test merge request'
diff --git a/spec/features/merge_requests/deleted_source_branch_spec.rb b/spec/features/merge_requests/deleted_source_branch_spec.rb
index 0952b17b63e..01e5e4f3a05 100644
--- a/spec/features/merge_requests/deleted_source_branch_spec.rb
+++ b/spec/features/merge_requests/deleted_source_branch_spec.rb
@@ -4,8 +4,6 @@ require 'spec_helper'
# message to be shown by JavaScript when the source branch was deleted.
# Please do not remove "js: true".
describe 'Deleted source branch', feature: true, js: true do
- include WaitForAjax
-
let(:user) { create(:user) }
let(:merge_request) { create(:merge_request) }
@@ -22,7 +20,7 @@ describe 'Deleted source branch', feature: true, js: true do
it 'shows a message about missing source branch' do
expect(page).to have_content(
- 'Source branch this-branch-does-not-exist does not exist'
+ 'Source branch does not exist.'
)
end
@@ -37,6 +35,6 @@ describe 'Deleted source branch', feature: true, js: true do
wait_for_ajax
expect(page).to have_selector('.diffs.tab-pane .nothing-here-block')
- expect(page).to have_content('Nothing to merge from this-branch-does-not-exist into feature')
+ expect(page).to have_content('Source branch does not exist.')
end
end
diff --git a/spec/features/merge_requests/diff_notes_avatars_spec.rb b/spec/features/merge_requests/diff_notes_avatars_spec.rb
index 218d95a88b8..b2e170513c4 100644
--- a/spec/features/merge_requests/diff_notes_avatars_spec.rb
+++ b/spec/features/merge_requests/diff_notes_avatars_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
feature 'Diff note avatars', feature: true, js: true do
- include WaitForAjax
-
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") }
diff --git a/spec/features/merge_requests/diff_notes_resolve_spec.rb b/spec/features/merge_requests/diff_notes_resolve_spec.rb
index 0e23c3a8849..4d549f3bdbb 100644
--- a/spec/features/merge_requests/diff_notes_resolve_spec.rb
+++ b/spec/features/merge_requests/diff_notes_resolve_spec.rb
@@ -275,7 +275,7 @@ feature 'Diff notes resolve', feature: true, js: true do
end
page.within '.line-resolve-all-container' do
- page.find('.discussion-next-btn').click
+ page.find('.discussion-next-btn').trigger('click')
end
expect(page.evaluate_script("$('body').scrollTop()")).to be > 0
diff --git a/spec/features/merge_requests/diffs_spec.rb b/spec/features/merge_requests/diffs_spec.rb
index 4a6c76a5caf..7dee3b852ca 100644
--- a/spec/features/merge_requests/diffs_spec.rb
+++ b/spec/features/merge_requests/diffs_spec.rb
@@ -1,11 +1,8 @@
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
+ let(:project) { create(:project, :public) }
+ let(:merge_request) { create(:merge_request, source_project: project) }
context 'when visit with */* as accept header' do
before(:each) do
@@ -13,9 +10,9 @@ feature 'Diffs URL', js: true, feature: true do
end
it 'renders the notes' do
- create :note_on_merge_request, project: @project, noteable: @merge_request, note: 'Rebasing with master'
+ 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)
+ 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')
@@ -25,10 +22,9 @@ feature 'Diffs URL', js: true, feature: true do
context 'when merge request has overflow' do
it 'displays warning' do
- allow_any_instance_of(MergeRequestDiff).to receive(:overflow?).and_return(true)
- allow(Commit).to receive(:max_diff_options).and_return(max_files: 20, max_lines: 20)
+ allow(Commit).to receive(:max_diff_options).and_return(max_files: 3)
- visit diffs_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)
+ visit diffs_namespace_project_merge_request_path(project.namespace, project, merge_request)
page.within('.alert') do
expect(page).to have_text("Too many changes to show. Plain diff Email patch To preserve
@@ -36,4 +32,35 @@ feature 'Diffs URL', js: true, feature: true do
end
end
end
+
+ context 'when editing file' do
+ let(:author_user) { create(:user) }
+ let(:user) { create(:user) }
+ let(:forked_project) { Projects::ForkService.new(project, author_user).execute }
+ let(:merge_request) { create(:merge_request_with_diffs, source_project: forked_project, target_project: project, author: author_user) }
+ let(:changelog_id) { Digest::SHA1.hexdigest("CHANGELOG") }
+
+ context 'as author' do
+ it 'shows direct edit link' do
+ login_as(author_user)
+ visit diffs_namespace_project_merge_request_path(project.namespace, project, merge_request)
+
+ # Throws `Capybara::Poltergeist::InvalidSelector` if we try to use `#hash` syntax
+ expect(page).to have_selector("[id=\"#{changelog_id}\"] a.js-edit-blob")
+ end
+ end
+
+ context 'as user who needs to fork' do
+ it 'shows fork/cancel confirmation' do
+ login_as(user)
+ visit diffs_namespace_project_merge_request_path(project.namespace, project, merge_request)
+
+ # Throws `Capybara::Poltergeist::InvalidSelector` if we try to use `#hash` syntax
+ find("[id=\"#{changelog_id}\"] .js-edit-blob").click
+
+ expect(page).to have_selector('.js-fork-suggestion-button', count: 1)
+ expect(page).to have_selector('.js-cancel-fork-suggestion-button', count: 1)
+ end
+ end
+ end
end
diff --git a/spec/features/merge_requests/edit_mr_spec.rb b/spec/features/merge_requests/edit_mr_spec.rb
index cb3bc392903..ec87a99b3ab 100644
--- a/spec/features/merge_requests/edit_mr_spec.rb
+++ b/spec/features/merge_requests/edit_mr_spec.rb
@@ -29,18 +29,6 @@ feature 'Edit Merge Request', feature: true do
expect(page).to have_content 'Someone edited the merge request the same time you did'
end
- it 'allows to unselect "Remove source branch"' do
- merge_request.update(merge_params: { 'force_remove_source_branch' => '1' })
- expect(merge_request.merge_params['force_remove_source_branch']).to be_truthy
-
- visit edit_namespace_project_merge_request_path(project.namespace, project, merge_request)
- uncheck 'Remove source branch when merge request is accepted'
-
- click_button 'Save changes'
-
- expect(page).to have_content 'Remove source branch'
- end
-
it 'should preserve description textarea height', js: true do
long_description = %q(
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam ac ornare ligula, ut tempus arcu. Etiam ultricies accumsan dolor vitae faucibus. Donec at elit lacus. Mauris orci ante, aliquam quis lorem eget, convallis faucibus arcu. Aenean at pulvinar lacus. Ut viverra quam massa, molestie ornare tortor dignissim a. Suspendisse tristique pellentesque tellus, id lacinia metus elementum id. Nam tristique, arcu rhoncus faucibus viverra, lacus ipsum sagittis ligula, vitae convallis odio lacus a nibh. Ut tincidunt est purus, ac vestibulum augue maximus in. Suspendisse vel erat et mi ultricies semper. Pellentesque volutpat pellentesque consequat.
diff --git a/spec/features/merge_requests/filter_by_labels_spec.rb b/spec/features/merge_requests/filter_by_labels_spec.rb
index 55f3c1863ff..32a9082b9b9 100644
--- a/spec/features/merge_requests/filter_by_labels_spec.rb
+++ b/spec/features/merge_requests/filter_by_labels_spec.rb
@@ -3,7 +3,6 @@ require 'rails_helper'
feature 'Issue filtering by Labels', feature: true, js: true do
include FilteredSearchHelpers
include MergeRequestHelpers
- include WaitForAjax
let(:project) { create(:project, :public) }
let!(:user) { create(:user) }
diff --git a/spec/features/merge_requests/filter_merge_requests_spec.rb b/spec/features/merge_requests/filter_merge_requests_spec.rb
index 70e3997e716..2da60e9f4ad 100644
--- a/spec/features/merge_requests/filter_merge_requests_spec.rb
+++ b/spec/features/merge_requests/filter_merge_requests_spec.rb
@@ -3,7 +3,6 @@ require 'rails_helper'
describe 'Filter merge requests', feature: true do
include FilteredSearchHelpers
include MergeRequestHelpers
- include WaitForAjax
let!(:project) { create(:project) }
let!(:group) { create(:group) }
diff --git a/spec/features/merge_requests/merge_commit_message_toggle_spec.rb b/spec/features/merge_requests/merge_commit_message_toggle_spec.rb
index 1bc2a5548dd..221ddb5873c 100644
--- a/spec/features/merge_requests/merge_commit_message_toggle_spec.rb
+++ b/spec/features/merge_requests/merge_commit_message_toggle_spec.rb
@@ -14,8 +14,6 @@ feature 'Clicking toggle commit message link', feature: true, js: true do
)
end
let(:textbox) { page.find(:css, '.js-commit-message', visible: false) }
- let(:include_link) { page.find(:css, '.js-with-description-link', visible: false) }
- let(:do_not_include_link) { page.find(:css, '.js-without-description-link', visible: false) }
let(:default_message) do
[
"Merge branch 'feature' into 'master'",
@@ -40,7 +38,7 @@ feature 'Clicking toggle commit message link', feature: true, js: true do
visit namespace_project_merge_request_path(project.namespace, project, merge_request)
- expect(textbox).not_to be_visible
+ expect(page).not_to have_selector('.js-commit-message')
click_button "Modify commit message"
expect(textbox).to be_visible
end
@@ -56,19 +54,4 @@ feature 'Clicking toggle commit message link', feature: true, js: true do
expect(textbox.value).to eq(default_message)
end
-
- it "toggles link between 'Include description' and 'Don't include description'" do
- expect(include_link).to be_visible
- expect(do_not_include_link).not_to be_visible
-
- click_link "Include description in commit message"
-
- expect(include_link).not_to be_visible
- expect(do_not_include_link).to be_visible
-
- click_link "Don't include description in commit message"
-
- expect(include_link).to be_visible
- expect(do_not_include_link).not_to be_visible
- end
end
diff --git a/spec/features/merge_requests/merge_immediately_with_pipeline_spec.rb b/spec/features/merge_requests/merge_immediately_with_pipeline_spec.rb
index 497240803d4..5820784f8e7 100644
--- a/spec/features/merge_requests/merge_immediately_with_pipeline_spec.rb
+++ b/spec/features/merge_requests/merge_immediately_with_pipeline_spec.rb
@@ -4,16 +4,18 @@ feature 'Merge immediately', :feature, :js do
let(:user) { create(:user) }
let(:project) { create(:project, :public) }
- let(:merge_request) do
+ let!(:merge_request) do
create(:merge_request_with_diffs, source_project: project,
author: user,
- title: 'Bug NS-04')
+ title: 'Bug NS-04',
+ head_pipeline: pipeline,
+ source_branch: pipeline.ref)
end
let(:pipeline) do
create(:ci_pipeline, project: project,
- sha: merge_request.diff_head_sha,
- ref: merge_request.source_branch)
+ ref: 'master',
+ sha: project.repository.commit('master').id)
end
before { project.team << [user, :master] }
@@ -34,7 +36,7 @@ feature 'Merge immediately', :feature, :js do
click_link 'Merge immediately'
- expect(find('.js-merge-when-pipeline-succeeds-button')).to have_content('Merge in progress')
+ expect(find('.accept-merge-request.btn-info')).to have_content('Merge in progress')
wait_for_ajax
end
diff --git a/spec/features/merge_requests/merge_when_pipeline_succeeds_spec.rb b/spec/features/merge_requests/merge_when_pipeline_succeeds_spec.rb
index cd540ca113a..11b6f0c0a64 100644
--- a/spec/features/merge_requests/merge_when_pipeline_succeeds_spec.rb
+++ b/spec/features/merge_requests/merge_when_pipeline_succeeds_spec.rb
@@ -16,7 +16,10 @@ feature 'Merge When Pipeline Succeeds', :feature, :js do
ref: merge_request.source_branch)
end
- before { project.team << [user, :master] }
+ before do
+ project.add_master(user)
+ merge_request.update(head_pipeline_id: pipeline.id)
+ end
context 'when there is active pipeline for merge request' do
background do
@@ -38,8 +41,8 @@ feature 'Merge When Pipeline Succeeds', :feature, :js do
click_button "Merge when pipeline succeeds"
expect(page).to have_content "Set by #{user.name} to be merged automatically when the pipeline succeeds."
- expect(page).to have_content "The source branch will not be removed."
- expect(page).to have_link "Cancel automatic merge"
+ expect(page).to have_content "The source branch will be removed."
+ expect(page).to have_selector ".js-cancel-auto-merge"
visit_merge_request(merge_request) # Needed to refresh the page
expect(page).to have_content /enabled an automatic merge when the pipeline for \h{8} succeeds/i
end
@@ -93,12 +96,10 @@ feature 'Merge When Pipeline Succeeds', :feature, :js do
describe 'enabling Merge when pipeline succeeds via dropdown' do
it 'activates the Merge when pipeline succeeds feature' do
click_button 'Select merge moment'
- within('.js-merge-dropdown') do
- click_link 'Merge when pipeline succeeds'
- end
+ click_link 'Merge when pipeline succeeds'
expect(page).to have_content "Set by #{user.name} to be merged automatically when the pipeline succeeds."
- expect(page).to have_content "The source branch will not be removed."
+ expect(page).to have_content "The source branch will be removed."
expect(page).to have_link "Cancel automatic merge"
end
end
@@ -131,13 +132,6 @@ feature 'Merge When Pipeline Succeeds', :feature, :js do
expect(page).to have_content "canceled the automatic merge"
end
- it "allows the user to remove the source branch" do
- expect(page).to have_link "Remove source branch when merged"
-
- click_link "Remove source branch when merged"
- expect(page).to have_content "The source branch will be removed"
- end
-
context 'when pipeline succeeds' do
background { build.success }
diff --git a/spec/features/merge_requests/mini_pipeline_graph_spec.rb b/spec/features/merge_requests/mini_pipeline_graph_spec.rb
index 84ad8765d8f..5b2798af32f 100644
--- a/spec/features/merge_requests/mini_pipeline_graph_spec.rb
+++ b/spec/features/merge_requests/mini_pipeline_graph_spec.rb
@@ -1,11 +1,9 @@
require 'rails_helper'
feature 'Mini Pipeline Graph', :js, :feature do
- include WaitForAjax
-
let(:user) { create(:user) }
let(:project) { create(:project, :public) }
- let(:merge_request) { create(:merge_request, source_project: project) }
+ let(:merge_request) { create(:merge_request, source_project: project, head_pipeline: pipeline) }
let(:pipeline) { create(:ci_empty_pipeline, project: project, ref: 'master', status: 'running', sha: project.commit.id) }
let(:build) { create(:ci_build, pipeline: pipeline, stage: 'test', commands: 'test') }
diff --git a/spec/features/merge_requests/only_allow_merge_if_build_succeeds_spec.rb b/spec/features/merge_requests/only_allow_merge_if_build_succeeds_spec.rb
index 4a590e3bf68..cdda0542c51 100644
--- a/spec/features/merge_requests/only_allow_merge_if_build_succeeds_spec.rb
+++ b/spec/features/merge_requests/only_allow_merge_if_build_succeeds_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
-feature 'Only allow merge requests to be merged if the pipeline succeeds', feature: true do
+feature 'Only allow merge requests to be merged if the pipeline succeeds', feature: true, js: true do
+ include WaitForVueResource
+
let(:merge_request) { create(:merge_request_with_diffs) }
let(:project) { merge_request.target_project }
@@ -10,15 +12,17 @@ feature 'Only allow merge requests to be merged if the pipeline succeeds', featu
project.team << [merge_request.author, :master]
end
- context 'project does not have CI enabled' do
+ context 'project does not have CI enabled', js: true do
it 'allows MR to be merged' do
visit_merge_request(merge_request)
- expect(page).to have_button 'Accept merge request'
+ wait_for_vue_resource
+
+ expect(page).to have_button 'Merge'
end
end
- context 'when project has CI enabled' do
+ context 'when project has CI enabled', js: true do
given!(:pipeline) do
create(:ci_empty_pipeline,
project: project,
@@ -27,6 +31,8 @@ feature 'Only allow merge requests to be merged if the pipeline succeeds', featu
status: status)
end
+ before { merge_request.update(head_pipeline: pipeline) }
+
context 'when merge requests can only be merged if the pipeline succeeds' do
before do
project.update_attribute(:only_allow_merge_if_pipeline_succeeds, true)
@@ -38,6 +44,8 @@ feature 'Only allow merge requests to be merged if the pipeline succeeds', featu
it 'does not allow to merge immediately' do
visit_merge_request(merge_request)
+ wait_for_vue_resource
+
expect(page).to have_button 'Merge when pipeline succeeds'
expect(page).not_to have_button 'Select merge moment'
end
@@ -49,7 +57,9 @@ feature 'Only allow merge requests to be merged if the pipeline succeeds', featu
it 'does not allow MR to be merged' do
visit_merge_request(merge_request)
- expect(page).not_to have_button 'Accept merge request'
+ wait_for_vue_resource
+
+ expect(page).to have_css('button[disabled="disabled"]', text: 'Merge')
expect(page).to have_content('Please retry the job or push a new commit to fix the failure.')
end
end
@@ -60,7 +70,9 @@ feature 'Only allow merge requests to be merged if the pipeline succeeds', featu
it 'does not allow MR to be merged' do
visit_merge_request(merge_request)
- expect(page).not_to have_button 'Accept merge request'
+ wait_for_vue_resource
+
+ expect(page).not_to have_button 'Merge'
expect(page).to have_content('Please retry the job or push a new commit to fix the failure.')
end
end
@@ -71,7 +83,9 @@ feature 'Only allow merge requests to be merged if the pipeline succeeds', featu
it 'allows MR to be merged' do
visit_merge_request(merge_request)
- expect(page).to have_button 'Accept merge request'
+ wait_for_vue_resource
+
+ expect(page).to have_button 'Merge'
end
end
@@ -81,7 +95,9 @@ feature 'Only allow merge requests to be merged if the pipeline succeeds', featu
it 'allows MR to be merged' do
visit_merge_request(merge_request)
- expect(page).to have_button 'Accept merge request'
+ wait_for_vue_resource
+
+ expect(page).to have_button 'Merge'
end
end
end
@@ -94,9 +110,11 @@ feature 'Only allow merge requests to be merged if the pipeline succeeds', featu
context 'when CI is running' do
given(:status) { :running }
- it 'allows MR to be merged immediately', js: true do
+ it 'allows MR to be merged immediately' do
visit_merge_request(merge_request)
+ wait_for_vue_resource
+
expect(page).to have_button 'Merge when pipeline succeeds'
click_button 'Select merge moment'
@@ -110,7 +128,9 @@ feature 'Only allow merge requests to be merged if the pipeline succeeds', featu
it 'allows MR to be merged' do
visit_merge_request(merge_request)
- expect(page).to have_button 'Accept merge request'
+ wait_for_vue_resource
+
+ expect(page).to have_button 'Merge'
end
end
@@ -120,7 +140,9 @@ feature 'Only allow merge requests to be merged if the pipeline succeeds', featu
it 'allows MR to be merged' do
visit_merge_request(merge_request)
- expect(page).to have_button 'Accept merge request'
+ wait_for_vue_resource
+
+ expect(page).to have_button 'Merge'
end
end
end
diff --git a/spec/features/merge_requests/pipelines_spec.rb b/spec/features/merge_requests/pipelines_spec.rb
index 9c4c0525267..99e283ac181 100644
--- a/spec/features/merge_requests/pipelines_spec.rb
+++ b/spec/features/merge_requests/pipelines_spec.rb
@@ -1,8 +1,6 @@
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 }
diff --git a/spec/features/merge_requests/reset_filters_spec.rb b/spec/features/merge_requests/reset_filters_spec.rb
index df5943f9136..275f81f50dc 100644
--- a/spec/features/merge_requests/reset_filters_spec.rb
+++ b/spec/features/merge_requests/reset_filters_spec.rb
@@ -3,7 +3,6 @@ require 'rails_helper'
feature 'Merge requests filter clear button', feature: true, js: true do
include FilteredSearchHelpers
include MergeRequestHelpers
- include WaitForAjax
include IssueHelpers
let!(:project) { create(:project, :public) }
diff --git a/spec/features/merge_requests/target_branch_spec.rb b/spec/features/merge_requests/target_branch_spec.rb
index b6134540273..c154cf8ade9 100644
--- a/spec/features/merge_requests/target_branch_spec.rb
+++ b/spec/features/merge_requests/target_branch_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe 'Target branch', feature: true do
+describe 'Target branch', feature: true, js: true do
let(:user) { create(:user) }
let(:merge_request) { create(:merge_request) }
let(:project) { merge_request.project }
@@ -17,11 +17,6 @@ describe 'Target branch', feature: true do
project.team << [user, :master]
end
- it 'shows link to target branch' do
- visit path_to_merge_request
- expect(page).to have_link('feature', href: namespace_project_commits_path(project.namespace, project, merge_request.target_branch))
- end
-
context 'when branch was deleted' do
before do
DeleteBranchService.new(project, user).execute('feature')
@@ -30,12 +25,12 @@ describe 'Target branch', feature: true do
it 'shows a message about missing target branch' do
expect(page).to have_content(
- 'Target branch feature does not exist'
+ 'Target branch does not exist'
)
end
it 'does not show link to target branch' do
- expect(page).not_to have_link('feature')
+ expect(page).not_to have_selector('.mr-widget-body .js-branch-text a')
end
end
end
diff --git a/spec/features/merge_requests/update_merge_requests_spec.rb b/spec/features/merge_requests/update_merge_requests_spec.rb
index b56fdfe5611..9ecc998785b 100644
--- a/spec/features/merge_requests/update_merge_requests_spec.rb
+++ b/spec/features/merge_requests/update_merge_requests_spec.rb
@@ -1,8 +1,6 @@
require 'rails_helper'
feature 'Multiple merge requests updating from merge_requests#index', feature: true do
- include WaitForAjax
-
let!(:user) { create(:user)}
let!(:project) { create(:project) }
let!(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
diff --git a/spec/features/merge_requests/user_posts_notes.rb b/spec/features/merge_requests/user_posts_notes_spec.rb
index c7cc4d6bc72..7fc0e2ce6ec 100644
--- a/spec/features/merge_requests/user_posts_notes.rb
+++ b/spec/features/merge_requests/user_posts_notes_spec.rb
@@ -98,6 +98,7 @@ describe 'Merge requests > User posts notes', :js do
find('.btn-save').click
end
+ wait_for_ajax
find('.note').hover
find('.js-note-edit').click
diff --git a/spec/features/merge_requests/user_uses_slash_commands_spec.rb b/spec/features/merge_requests/user_uses_slash_commands_spec.rb
index a1f4eb2688b..f0ad57eb92f 100644
--- a/spec/features/merge_requests/user_uses_slash_commands_spec.rb
+++ b/spec/features/merge_requests/user_uses_slash_commands_spec.rb
@@ -2,7 +2,6 @@ require 'rails_helper'
feature 'Merge Requests > User uses slash commands', feature: true, js: true do
include SlashCommandsHelpers
- include WaitForAjax
let(:user) { create(:user) }
let(:project) { create(:project, :public) }
@@ -161,6 +160,7 @@ feature 'Merge Requests > User uses slash commands', feature: true, js: true do
it 'changes target branch from a note' do
write_note("message start \n/target_branch merge-test\n message end.")
+ wait_for_ajax
expect(page).not_to have_content('/target_branch')
expect(page).to have_content('message start')
expect(page).to have_content('message end.')
diff --git a/spec/features/merge_requests/versions_spec.rb b/spec/features/merge_requests/versions_spec.rb
index 7a2da623c58..2b5b803946c 100644
--- a/spec/features/merge_requests/versions_spec.rb
+++ b/spec/features/merge_requests/versions_spec.rb
@@ -24,7 +24,12 @@ feature 'Merge Request versions', js: true, feature: true do
before do
page.within '.mr-version-dropdown' do
find('.btn-default').click
- find(:link, 'version 1').trigger('click')
+ click_link 'version 1'
+ end
+
+ # Wait for the page to load
+ page.within '.mr-version-dropdown' do
+ expect(page).to have_content 'version 1'
end
end
@@ -36,8 +41,8 @@ feature 'Merge Request versions', js: true, feature: true do
expect(page).to have_content '5 changed files'
end
- it 'show the message about disabled comment creation' do
- expect(page).to have_content 'comment creation is disabled'
+ it 'show the message about comments' do
+ expect(page).to have_content 'Not all comments are displayed'
end
it 'shows comments that were last relevant at that version' do
@@ -52,15 +57,41 @@ feature 'Merge Request versions', js: true, feature: true do
outdated_diff_note.position = outdated_diff_note.original_position
outdated_diff_note.save!
+ visit current_url
+
expect(page).to have_css(".diffs .notes[data-discussion-id='#{outdated_diff_note.discussion_id}']")
end
+
+ it 'allows commenting' do
+ diff_file_selector = ".diff-file[id='7445606fbf8f3683cd42bdc54b05d7a0bc2dfc44']"
+ line_code = '7445606fbf8f3683cd42bdc54b05d7a0bc2dfc44_2_2'
+
+ page.within(diff_file_selector) do
+ find(".line_holder[id='#{line_code}'] td:nth-of-type(1)").trigger 'mouseover'
+ find(".line_holder[id='#{line_code}'] button").trigger 'click'
+
+ page.within("form[data-line-code='#{line_code}']") do
+ fill_in "note[note]", with: "Typo, please fix"
+ find(".js-comment-button").click
+ end
+
+ wait_for_ajax
+
+ expect(page).to have_content("Typo, please fix")
+ end
+ end
end
describe 'compare with older version' do
before do
page.within '.mr-version-compare-dropdown' do
find('.btn-default').click
- find(:link, 'version 1').trigger('click')
+ click_link 'version 1'
+ end
+
+ # Wait for the page to load
+ page.within '.mr-version-compare-dropdown' do
+ expect(page).to have_content 'version 1'
end
end
@@ -80,8 +111,43 @@ feature 'Merge Request versions', js: true, feature: true do
end
end
- it 'show the message about disabled comments' do
- expect(page).to have_content 'Comments are disabled'
+ it 'show the message about comments' do
+ expect(page).to have_content 'Not all comments are displayed'
+ end
+
+ it 'shows comments that were last relevant at that version' do
+ position = Gitlab::Diff::Position.new(
+ old_path: ".gitmodules",
+ new_path: ".gitmodules",
+ old_line: 4,
+ new_line: 4,
+ diff_refs: merge_request_diff3.compare_with(merge_request_diff1.head_commit_sha).diff_refs
+ )
+ outdated_diff_note = create(:diff_note_on_merge_request, project: project, noteable: merge_request, position: position)
+
+ visit current_url
+ wait_for_ajax
+
+ expect(page).to have_css(".diffs .notes[data-discussion-id='#{outdated_diff_note.discussion_id}']")
+ end
+
+ it 'allows commenting' do
+ diff_file_selector = ".diff-file[id='7445606fbf8f3683cd42bdc54b05d7a0bc2dfc44']"
+ line_code = '7445606fbf8f3683cd42bdc54b05d7a0bc2dfc44_4_4'
+
+ page.within(diff_file_selector) do
+ find(".line_holder[id='#{line_code}'] td:nth-of-type(1)").trigger 'mouseover'
+ find(".line_holder[id='#{line_code}'] button").trigger 'click'
+
+ page.within("form[data-line-code='#{line_code}']") do
+ fill_in "note[note]", with: "Typo, please fix"
+ find(".js-comment-button").click
+ end
+
+ wait_for_ajax
+
+ expect(page).to have_content("Typo, please fix")
+ end
end
it 'show diff between new and old version' do
diff --git a/spec/features/merge_requests/widget_deployments_spec.rb b/spec/features/merge_requests/widget_deployments_spec.rb
index 6676821b807..8370499f6ed 100644
--- a/spec/features/merge_requests/widget_deployments_spec.rb
+++ b/spec/features/merge_requests/widget_deployments_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
feature 'Widget Deployments Header', feature: true, js: true do
- include WaitForAjax
-
describe 'when deployed to an environment' do
given(:user) { create(:user) }
given(:project) { merge_request.target_project }
@@ -23,7 +21,7 @@ feature 'Widget Deployments Header', feature: true, js: true do
wait_for_ajax
expect(page).to have_content("Deployed to #{environment.name}")
- expect(find('.ci_widget > span > span')['data-title']).to eq(deployment.created_at.to_time.in_time_zone.to_s(:medium))
+ expect(find('.js-deploy-time')['data-title']).to eq(deployment.created_at.to_time.in_time_zone.to_s(:medium))
end
context 'with stop action' do
@@ -40,11 +38,11 @@ feature 'Widget Deployments Header', feature: true, js: true do
end
scenario 'does show stop button' do
- expect(page).to have_link('Stop environment')
+ expect(page).to have_button('Stop environment')
end
scenario 'does start build when stop button clicked' do
- click_link('Stop environment')
+ click_button('Stop environment')
expect(page).to have_content('close_app')
end
@@ -53,7 +51,7 @@ feature 'Widget Deployments Header', feature: true, js: true do
given(:role) { :reporter }
scenario 'does not show stop button' do
- expect(page).not_to have_link('Stop environment')
+ expect(page).not_to have_button('Stop environment')
end
end
end
diff --git a/spec/features/merge_requests/widget_spec.rb b/spec/features/merge_requests/widget_spec.rb
index 4e128cd4a7d..ae799584c0f 100644
--- a/spec/features/merge_requests/widget_spec.rb
+++ b/spec/features/merge_requests/widget_spec.rb
@@ -1,8 +1,6 @@
require 'rails_helper'
describe 'Merge request', :feature, :js do
- include WaitForAjax
-
let(:user) { create(:user) }
let(:project) { create(:project) }
let(:merge_request) { create(:merge_request, source_project: project) }
@@ -32,6 +30,7 @@ describe 'Merge request', :feature, :js do
wait_for_ajax
expect(page).to have_selector('.accept-merge-request')
+ expect(find('.accept-merge-request')['disabled']).not_to be(true)
end
end
@@ -53,14 +52,15 @@ describe 'Merge request', :feature, :js do
page.within('.mr-widget-heading') do
expect(page).to have_content("Deployed to #{environment.name}")
- expect(find('.js-environment-link')[:href]).to include(environment.formatted_external_url)
+ expect(find('.js-deploy-url')[:href]).to include(environment.formatted_external_url)
end
end
it 'shows green accept merge request button' do
# Wait for the `ci_status` and `merge_check` requests
wait_for_ajax
- expect(page).to have_selector('.accept-merge-request.btn-create')
+ expect(page).to have_selector('.accept-merge-request')
+ expect(find('.accept-merge-request')['disabled']).not_to be(true)
end
end
@@ -91,6 +91,8 @@ describe 'Merge request', :feature, :js do
statuses: [commit_status])
create(:ci_build, :pending, pipeline: pipeline)
+ merge_request.update(head_pipeline: pipeline)
+
visit namespace_project_merge_request_path(project.namespace, project, merge_request)
end
@@ -103,10 +105,15 @@ describe 'Merge request', :feature, :js do
context 'when merge request is in the blocked pipeline state' do
before do
- create(:ci_pipeline, project: project,
- sha: merge_request.diff_head_sha,
- ref: merge_request.source_branch,
- status: :manual)
+ pipeline = create(
+ :ci_pipeline,
+ project: project,
+ sha: merge_request.diff_head_sha,
+ ref: merge_request.source_branch,
+ status: :manual
+ )
+
+ merge_request.update(head_pipeline: pipeline)
visit namespace_project_merge_request_path(project.namespace,
project,
@@ -131,13 +138,36 @@ describe 'Merge request', :feature, :js do
statuses: [commit_status])
create(:ci_build, :pending, pipeline: pipeline)
+ merge_request.update(head_pipeline: pipeline)
+
visit namespace_project_merge_request_path(project.namespace, project, merge_request)
end
it 'has info button when MWBS button' do
# Wait for the `ci_status` and `merge_check` requests
wait_for_ajax
- expect(page).to have_selector('.merge-when-pipeline-succeeds.btn-info')
+ expect(page).to have_selector('.accept-merge-request.btn-info')
+ end
+ end
+
+ context 'view merge request with MWPS enabled but automatically merge fails' do
+ before do
+ merge_request.update(
+ merge_when_pipeline_succeeds: true,
+ merge_user: merge_request.author,
+ merge_error: 'Something went wrong'
+ )
+
+ visit namespace_project_merge_request_path(project.namespace, project, merge_request)
+ end
+
+ it 'shows information about the merge error' do
+ # Wait for the `ci_status` and `merge_check` requests
+ wait_for_ajax
+
+ page.within('.mr-widget-body') do
+ expect(page).to have_content('Something went wrong')
+ end
end
end
@@ -166,11 +196,11 @@ describe 'Merge request', :feature, :js do
before do
allow_any_instance_of(Repository).to receive(:merge).and_return(false)
visit namespace_project_merge_request_path(project.namespace, project, merge_request)
- click_button 'Accept merge request'
- wait_for_ajax
end
it 'updates the MR widget' do
+ click_button 'Merge'
+
page.within('.mr-widget-body') do
expect(page).to have_content('Conflicts detected during merge')
end
diff --git a/spec/features/milestone_spec.rb b/spec/features/milestone_spec.rb
index c3297de709a..c07de01c594 100644
--- a/spec/features/milestone_spec.rb
+++ b/spec/features/milestone_spec.rb
@@ -1,8 +1,6 @@
require 'rails_helper'
feature 'Milestone', feature: true do
- include WaitForAjax
-
let(:project) { create(:empty_project, :public) }
let(:user) { create(:user) }
diff --git a/spec/features/milestones/milestones_spec.rb b/spec/features/milestones/milestones_spec.rb
index 2fa3e72ab08..9eec3d7f270 100644
--- a/spec/features/milestones/milestones_spec.rb
+++ b/spec/features/milestones/milestones_spec.rb
@@ -1,7 +1,6 @@
require 'rails_helper'
describe 'Milestone draggable', feature: true, js: true do
- include WaitForAjax
include DragTo
let(:milestone) { create(:milestone, project: project, title: 8.14) }
@@ -87,6 +86,9 @@ describe 'Milestone draggable', feature: true, js: true do
visit namespace_project_milestone_path(project.namespace, project, milestone)
page.find("a[href='#tab-merge-requests']").click
+
+ wait_for_ajax
+
scroll_into_view('.milestone-content')
drag_to(selector: '.merge_requests-sortable-list', list_to_index: 1)
diff --git a/spec/features/milestones/show_spec.rb b/spec/features/milestones/show_spec.rb
index 40b4dc63697..227eb04ba72 100644
--- a/spec/features/milestones/show_spec.rb
+++ b/spec/features/milestones/show_spec.rb
@@ -5,7 +5,7 @@ describe 'Milestone show', feature: true do
let(:project) { create(:empty_project) }
let(:milestone) { create(:milestone, project: project) }
let(:labels) { create_list(:label, 2, project: project) }
- let(:issue_params) { { project: project, assignee: user, author: user, milestone: milestone, labels: labels } }
+ let(:issue_params) { { project: project, assignees: [user], author: user, milestone: milestone, labels: labels } }
before do
project.add_user(user, :developer)
diff --git a/spec/features/participants_autocomplete_spec.rb b/spec/features/participants_autocomplete_spec.rb
index decad589c23..449ce80bc71 100644
--- a/spec/features/participants_autocomplete_spec.rb
+++ b/spec/features/participants_autocomplete_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
feature 'Member autocomplete', :js do
- let(:project) { create(:project, :public) }
+ let(:project) { create(:empty_project, :public) }
let(:user) { create(:user) }
let(:author) { create(:user) }
let(:note) { create(:note, noteable: noteable, project: noteable.project) }
@@ -36,6 +36,7 @@ feature 'Member autocomplete', :js do
end
context 'adding a new note on a Merge Request' do
+ let(:project) { create(:project, :public, :repository) }
let(:noteable) do
create(:merge_request, source_project: project,
target_project: project, author: author)
@@ -48,6 +49,7 @@ feature 'Member autocomplete', :js do
end
context 'adding a new note on a Commit' do
+ let(:project) { create(:project, :public, :repository) }
let(:noteable) { project.commit }
let(:note) { create(:note_on_commit, project: project, commit_id: project.commit.id) }
diff --git a/spec/features/profiles/account_spec.rb b/spec/features/profiles/account_spec.rb
new file mode 100644
index 00000000000..05a7587f8d4
--- /dev/null
+++ b/spec/features/profiles/account_spec.rb
@@ -0,0 +1,59 @@
+require 'rails_helper'
+
+feature 'Profile > Account', feature: true do
+ given(:user) { create(:user, username: 'foo') }
+
+ before do
+ login_as(user)
+ end
+
+ describe 'Change username' do
+ given(:new_username) { 'bar' }
+ given(:new_user_path) { "/#{new_username}" }
+ given(:old_user_path) { "/#{user.username}" }
+
+ scenario 'the user is accessible via the new path' do
+ update_username(new_username)
+ visit new_user_path
+ expect(current_path).to eq(new_user_path)
+ expect(find('.user-info')).to have_content(new_username)
+ end
+
+ scenario 'the old user path redirects to the new path' do
+ update_username(new_username)
+ visit old_user_path
+ expect(current_path).to eq(new_user_path)
+ expect(find('.user-info')).to have_content(new_username)
+ end
+
+ context 'with a project' do
+ given!(:project) { create(:project, namespace: user.namespace, path: 'project') }
+ given(:new_project_path) { "/#{new_username}/#{project.path}" }
+ given(:old_project_path) { "/#{user.username}/#{project.path}" }
+
+ before(:context) { TestEnv.clean_test_path }
+ after(:example) { TestEnv.clean_test_path }
+
+ scenario 'the project is accessible via the new path' do
+ update_username(new_username)
+ visit new_project_path
+ expect(current_path).to eq(new_project_path)
+ expect(find('h1.project-title')).to have_content(project.name)
+ end
+
+ scenario 'the old project path redirects to the new path' do
+ update_username(new_username)
+ visit old_project_path
+ expect(current_path).to eq(new_project_path)
+ expect(find('h1.project-title')).to have_content(project.name)
+ end
+ end
+ end
+end
+
+def update_username(new_username)
+ allow(user.namespace).to receive(:move_dir)
+ visit profile_account_path
+ fill_in 'user_username', with: new_username
+ click_button 'Update username'
+end
diff --git a/spec/features/profiles/preferences_spec.rb b/spec/features/profiles/preferences_spec.rb
index 15c8677fcd3..d368bc4d753 100644
--- a/spec/features/profiles/preferences_spec.rb
+++ b/spec/features/profiles/preferences_spec.rb
@@ -44,7 +44,7 @@ describe 'Profile > Preferences', feature: true do
expect(page.current_path).to eq starred_dashboard_projects_path
end
- click_link 'Your projects'
+ find('.shortcuts-activity').trigger('click')
expect(page).not_to have_content("You don't have starred projects yet")
expect(page.current_path).to eq dashboard_projects_path
diff --git a/spec/features/projects/artifacts/file_spec.rb b/spec/features/projects/artifacts/file_spec.rb
new file mode 100644
index 00000000000..74308a7e8dd
--- /dev/null
+++ b/spec/features/projects/artifacts/file_spec.rb
@@ -0,0 +1,59 @@
+require 'spec_helper'
+
+feature 'Artifact file', :js, feature: true do
+ let(:project) { create(:project, :public) }
+ let(:pipeline) { create(:ci_empty_pipeline, project: project, sha: project.commit.sha, ref: 'master') }
+ let(:build) { create(:ci_build, :artifacts, pipeline: pipeline) }
+
+ def visit_file(path)
+ visit file_namespace_project_build_artifacts_path(project.namespace, project, build, path)
+ end
+
+ context 'Text file' do
+ before do
+ visit_file('other_artifacts_0.1.2/doc_sample.txt')
+
+ wait_for_ajax
+ end
+
+ it 'displays an error' do
+ aggregate_failures do
+ # shows an error message
+ expect(page).to have_content('The source could not be displayed because it is stored as a job artifact. You can download it instead.')
+
+ # does not show a viewer switcher
+ expect(page).not_to have_selector('.js-blob-viewer-switcher')
+
+ # does not show a copy button
+ expect(page).not_to have_selector('.js-copy-blob-source-btn')
+
+ # shows a download button
+ expect(page).to have_link('Download')
+ end
+ end
+ end
+
+ context 'JPG file' do
+ before do
+ visit_file('rails_sample.jpg')
+
+ wait_for_ajax
+ end
+
+ it 'displays the blob' do
+ aggregate_failures do
+ # shows rendered image
+ expect(page).to have_selector('.image_file img')
+
+ # does not show a viewer switcher
+ expect(page).not_to have_selector('.js-blob-viewer-switcher')
+
+ # does not show a copy button
+ expect(page).not_to have_selector('.js-copy-blob-source-btn')
+
+ # shows a download button
+ expect(page).to have_link('Download')
+ end
+ end
+ end
+end
diff --git a/spec/features/projects/blobs/blob_show_spec.rb b/spec/features/projects/blobs/blob_show_spec.rb
index 01cd268ffe8..9888624a509 100644
--- a/spec/features/projects/blobs/blob_show_spec.rb
+++ b/spec/features/projects/blobs/blob_show_spec.rb
@@ -1,22 +1,429 @@
require 'spec_helper'
-feature 'File blob', feature: true do
- include WaitForAjax
- include TreeHelper
+feature 'File blob', :js, feature: true do
+ let(:project) { create(:project, :public) }
- let(:project) { create(:project, :public, :test_repo) }
- let(:merge_request) { create(:merge_request, source_project: project, source_branch: 'feature', target_branch: 'master') }
- let(:branch) { 'master' }
- let(:file_path) { project.repository.ls_files(project.repository.root_ref)[1] }
+ def visit_blob(path, fragment = nil)
+ visit namespace_project_blob_path(project.namespace, project, File.join('master', path), anchor: fragment)
- context 'anonymous' do
- context 'from blob file path' do
+ wait_for_ajax
+ end
+
+ context 'Ruby file' do
+ before do
+ visit_blob('files/ruby/popen.rb')
+ end
+
+ it 'displays the blob' do
+ aggregate_failures do
+ # shows highlighted Ruby code
+ expect(page).to have_content("require 'fileutils'")
+
+ # does not show a viewer switcher
+ expect(page).not_to have_selector('.js-blob-viewer-switcher')
+
+ # shows an enabled copy button
+ expect(page).to have_selector('.js-copy-blob-source-btn:not(.disabled)')
+
+ # shows a raw button
+ expect(page).to have_link('Open raw')
+ end
+ end
+ end
+
+ context 'Markdown file' do
+ context 'visiting directly' do
+ before do
+ visit_blob('files/markdown/ruby-style-guide.md')
+ end
+
+ it 'displays the blob using the rich viewer' do
+ aggregate_failures do
+ # hides the simple viewer
+ expect(page).to have_selector('.blob-viewer[data-type="simple"]', visible: false)
+ expect(page).to have_selector('.blob-viewer[data-type="rich"]')
+
+ # shows rendered Markdown
+ expect(page).to have_link("PEP-8")
+
+ # shows a viewer switcher
+ expect(page).to have_selector('.js-blob-viewer-switcher')
+
+ # shows a disabled copy button
+ expect(page).to have_selector('.js-copy-blob-source-btn.disabled')
+
+ # shows a raw button
+ expect(page).to have_link('Open raw')
+ end
+ end
+
+ context 'switching to the simple viewer' do
+ before do
+ find('.js-blob-viewer-switch-btn[data-viewer=simple]').click
+
+ wait_for_ajax
+ end
+
+ it 'displays the blob using the simple viewer' do
+ aggregate_failures do
+ # hides the rich viewer
+ expect(page).to have_selector('.blob-viewer[data-type="simple"]')
+ expect(page).to have_selector('.blob-viewer[data-type="rich"]', visible: false)
+
+ # shows highlighted Markdown code
+ expect(page).to have_content("[PEP-8](http://www.python.org/dev/peps/pep-0008/)")
+
+ # shows an enabled copy button
+ expect(page).to have_selector('.js-copy-blob-source-btn:not(.disabled)')
+ end
+ end
+
+ context 'switching to the rich viewer again' do
+ before do
+ find('.js-blob-viewer-switch-btn[data-viewer=rich]').click
+
+ wait_for_ajax
+ end
+
+ it 'displays the blob using the rich viewer' do
+ aggregate_failures do
+ # hides the simple viewer
+ expect(page).to have_selector('.blob-viewer[data-type="simple"]', visible: false)
+ expect(page).to have_selector('.blob-viewer[data-type="rich"]')
+
+ # shows an enabled copy button
+ expect(page).to have_selector('.js-copy-blob-source-btn:not(.disabled)')
+ end
+ end
+ end
+ end
+ end
+
+ context 'visiting with a line number anchor' do
+ before do
+ visit_blob('files/markdown/ruby-style-guide.md', 'L1')
+ end
+
+ it 'displays the blob using the simple viewer' do
+ aggregate_failures do
+ # hides the rich viewer
+ expect(page).to have_selector('.blob-viewer[data-type="simple"]')
+ expect(page).to have_selector('.blob-viewer[data-type="rich"]', visible: false)
+
+ # highlights the line in question
+ expect(page).to have_selector('#LC1.hll')
+
+ # shows highlighted Markdown code
+ expect(page).to have_content("[PEP-8](http://www.python.org/dev/peps/pep-0008/)")
+
+ # shows an enabled copy button
+ expect(page).to have_selector('.js-copy-blob-source-btn:not(.disabled)')
+ end
+ end
+ end
+ end
+
+ context 'Markdown file (stored in LFS)' do
+ before do
+ project.add_master(project.creator)
+
+ Files::CreateService.new(
+ project,
+ project.creator,
+ start_branch: 'master',
+ branch_name: 'master',
+ commit_message: "Add Markdown in LFS",
+ file_path: 'files/lfs/file.md',
+ file_content: project.repository.blob_at('master', 'files/lfs/lfs_object.iso').data
+ ).execute
+ end
+
+ context 'when LFS is enabled on the project' do
+ before do
+ allow(Gitlab.config.lfs).to receive(:enabled).and_return(true)
+ project.update_attribute(:lfs_enabled, true)
+
+ visit_blob('files/lfs/file.md')
+ end
+
+ it 'displays an error' do
+ aggregate_failures do
+ # hides the simple viewer
+ expect(page).to have_selector('.blob-viewer[data-type="simple"]', visible: false)
+ expect(page).to have_selector('.blob-viewer[data-type="rich"]')
+
+ # shows an error message
+ expect(page).to have_content('The rendered file could not be displayed because it is stored in LFS. You can download it instead.')
+
+ # shows a viewer switcher
+ expect(page).to have_selector('.js-blob-viewer-switcher')
+
+ # does not show a copy button
+ expect(page).not_to have_selector('.js-copy-blob-source-btn')
+
+ # shows a download button
+ expect(page).to have_link('Download')
+ end
+ end
+
+ context 'switching to the simple viewer' do
+ before do
+ find('.js-blob-viewer-switcher .js-blob-viewer-switch-btn[data-viewer=simple]').click
+
+ wait_for_ajax
+ end
+
+ it 'displays an error' do
+ aggregate_failures do
+ # hides the rich viewer
+ expect(page).to have_selector('.blob-viewer[data-type="simple"]')
+ expect(page).to have_selector('.blob-viewer[data-type="rich"]', visible: false)
+
+ # shows an error message
+ expect(page).to have_content('The source could not be displayed because it is stored in LFS. You can download it instead.')
+
+ # does not show a copy button
+ expect(page).not_to have_selector('.js-copy-blob-source-btn')
+ end
+ end
+ end
+ end
+
+ context 'when LFS is disabled on the project' do
+ before do
+ visit_blob('files/lfs/file.md')
+ end
+
+ it 'displays the blob' do
+ aggregate_failures do
+ # shows text
+ expect(page).to have_content('size 1575078')
+
+ # does not show a viewer switcher
+ expect(page).not_to have_selector('.js-blob-viewer-switcher')
+
+ # shows an enabled copy button
+ expect(page).to have_selector('.js-copy-blob-source-btn:not(.disabled)')
+
+ # shows a raw button
+ expect(page).to have_link('Open raw')
+ end
+ end
+ end
+ end
+
+ context 'PDF file' do
+ before do
+ project.add_master(project.creator)
+
+ Files::CreateService.new(
+ project,
+ project.creator,
+ start_branch: 'master',
+ branch_name: 'master',
+ commit_message: "Add PDF",
+ file_path: 'files/test.pdf',
+ file_content: project.repository.blob_at('add-pdf-file', 'files/pdf/test.pdf').data
+ ).execute
+
+ visit_blob('files/test.pdf')
+ end
+
+ it 'displays the blob' do
+ aggregate_failures do
+ # shows rendered PDF
+ expect(page).to have_selector('.js-pdf-viewer')
+
+ # does not show a viewer switcher
+ expect(page).not_to have_selector('.js-blob-viewer-switcher')
+
+ # does not show a copy button
+ expect(page).not_to have_selector('.js-copy-blob-source-btn')
+
+ # shows a download button
+ expect(page).to have_link('Download')
+ end
+ end
+ end
+
+ context 'ISO file (stored in LFS)' do
+ context 'when LFS is enabled on the project' do
before do
- visit namespace_project_blob_path(project.namespace, project, tree_join(branch, file_path))
+ allow(Gitlab.config.lfs).to receive(:enabled).and_return(true)
+ project.update_attribute(:lfs_enabled, true)
+
+ visit_blob('files/lfs/lfs_object.iso')
+ end
+
+ it 'displays the blob' do
+ aggregate_failures do
+ # shows a download link
+ expect(page).to have_link('Download (1.5 MB)')
+
+ # does not show a viewer switcher
+ expect(page).not_to have_selector('.js-blob-viewer-switcher')
+
+ # does not show a copy button
+ expect(page).not_to have_selector('.js-copy-blob-source-btn')
+
+ # shows a download button
+ expect(page).to have_link('Download')
+ end
+ end
+ end
+
+ context 'when LFS is disabled on the project' do
+ before do
+ visit_blob('files/lfs/lfs_object.iso')
+ end
+
+ it 'displays the blob' do
+ aggregate_failures do
+ # shows text
+ expect(page).to have_content('size 1575078')
+
+ # does not show a viewer switcher
+ expect(page).not_to have_selector('.js-blob-viewer-switcher')
+
+ # shows an enabled copy button
+ expect(page).to have_selector('.js-copy-blob-source-btn:not(.disabled)')
+
+ # shows a raw button
+ expect(page).to have_link('Open raw')
+ end
+ end
+ end
+ end
+
+ context 'ZIP file' do
+ before do
+ visit_blob('Gemfile.zip')
+ end
+
+ it 'displays the blob' do
+ aggregate_failures do
+ # shows a download link
+ expect(page).to have_link('Download (2.11 KB)')
+
+ # does not show a viewer switcher
+ expect(page).not_to have_selector('.js-blob-viewer-switcher')
+
+ # does not show a copy button
+ expect(page).not_to have_selector('.js-copy-blob-source-btn')
+
+ # shows a download button
+ expect(page).to have_link('Download')
+ end
+ end
+ end
+
+ context 'empty file' do
+ before do
+ project.add_master(project.creator)
+
+ Files::CreateService.new(
+ project,
+ project.creator,
+ start_branch: 'master',
+ branch_name: 'master',
+ commit_message: "Add empty file",
+ file_path: 'files/empty.md',
+ file_content: ''
+ ).execute
+
+ visit_blob('files/empty.md')
+ end
+
+ it 'displays an error' do
+ aggregate_failures do
+ # shows an error message
+ expect(page).to have_content('Empty file')
+
+ # does not show a viewer switcher
+ expect(page).not_to have_selector('.js-blob-viewer-switcher')
+
+ # does not show a copy button
+ expect(page).not_to have_selector('.js-copy-blob-source-btn')
+
+ # does not show a download or raw button
+ expect(page).not_to have_link('Download')
+ expect(page).not_to have_link('Open raw')
end
+ end
+ end
+
+ context '.gitlab-ci.yml' do
+ before do
+ project.add_master(project.creator)
+
+ Files::CreateService.new(
+ project,
+ project.creator,
+ start_branch: 'master',
+ branch_name: 'master',
+ commit_message: "Add .gitlab-ci.yml",
+ file_path: '.gitlab-ci.yml',
+ file_content: File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml'))
+ ).execute
+
+ visit_blob('.gitlab-ci.yml')
+ end
+
+ it 'displays an auxiliary viewer' do
+ aggregate_failures do
+ # shows that configuration is valid
+ expect(page).to have_content('This GitLab CI configuration is valid.')
+
+ # shows a learn more link
+ expect(page).to have_link('Learn more')
+ end
+ end
+ end
+
+ context '.gitlab/route-map.yml' do
+ before do
+ project.add_master(project.creator)
+
+ Files::CreateService.new(
+ project,
+ project.creator,
+ start_branch: 'master',
+ branch_name: 'master',
+ commit_message: "Add .gitlab/route-map.yml",
+ file_path: '.gitlab/route-map.yml',
+ file_content: <<-MAP.strip_heredoc
+ # Team data
+ - source: 'data/team.yml'
+ public: 'team/'
+ MAP
+ ).execute
+
+ visit_blob('.gitlab/route-map.yml')
+ end
+
+ it 'displays an auxiliary viewer' do
+ aggregate_failures do
+ # shows that map is valid
+ expect(page).to have_content('This Route Map is valid.')
+
+ # shows a learn more link
+ expect(page).to have_link('Learn more')
+ end
+ end
+ end
+
+ context 'LICENSE' do
+ before do
+ visit_blob('LICENSE')
+ end
+
+ it 'displays an auxiliary viewer' do
+ aggregate_failures do
+ # shows license
+ expect(page).to have_content('This project is licensed under the MIT License.')
- it 'updates content' do
- expect(page).to have_link 'Edit'
+ # shows a learn more link
+ expect(page).to have_link('Learn more about this license', 'http://choosealicense.com/licenses/mit/')
end
end
end
diff --git a/spec/features/projects/blobs/edit_spec.rb b/spec/features/projects/blobs/edit_spec.rb
index aab5a72678e..cc5b1a7e734 100644
--- a/spec/features/projects/blobs/edit_spec.rb
+++ b/spec/features/projects/blobs/edit_spec.rb
@@ -1,7 +1,6 @@
require 'spec_helper'
feature 'Editing file blob', feature: true, js: true do
- include WaitForAjax
include TreeHelper
let(:project) { create(:project, :public, :test_repo) }
diff --git a/spec/features/projects/blobs/user_create_spec.rb b/spec/features/projects/blobs/user_create_spec.rb
index 6ea149956fe..d805450e095 100644
--- a/spec/features/projects/blobs/user_create_spec.rb
+++ b/spec/features/projects/blobs/user_create_spec.rb
@@ -1,7 +1,6 @@
require 'spec_helper'
feature 'New blob creation', feature: true, js: true do
- include WaitForAjax
include TargetBranchHelpers
given(:user) { create(:user) }
diff --git a/spec/features/projects/branches/new_branch_ref_dropdown_spec.rb b/spec/features/projects/branches/new_branch_ref_dropdown_spec.rb
new file mode 100644
index 00000000000..c5e0a0f0517
--- /dev/null
+++ b/spec/features/projects/branches/new_branch_ref_dropdown_spec.rb
@@ -0,0 +1,48 @@
+require 'spec_helper'
+
+describe 'New Branch Ref Dropdown', :js, :feature do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :public) }
+ let(:toggle) { find('.create-from .dropdown-menu-toggle') }
+
+ before do
+ project.add_master(user)
+
+ login_as(user)
+ visit new_namespace_project_branch_path(project.namespace, project)
+ end
+
+ it 'filters a list of branches and tags' do
+ toggle.click
+
+ filter_by('v1.0.0')
+
+ expect(items_count).to be(1)
+
+ filter_by('video')
+
+ expect(items_count).to be(1)
+
+ find('.create-from .dropdown-content li').click
+
+ expect(toggle).to have_content 'video'
+ end
+
+ it 'accepts a manually entered commit SHA' do
+ toggle.click
+
+ filter_by('somecommitsha')
+
+ find('.create-from input[type=search]').send_keys(:enter)
+
+ expect(toggle).to have_content 'somecommitsha'
+ end
+
+ def items_count
+ all('.create-from .dropdown-content li').length
+ end
+
+ def filter_by(filter_text)
+ fill_in 'Filter by Git revision', with: filter_text
+ end
+end
diff --git a/spec/features/projects/branches_spec.rb b/spec/features/projects/branches_spec.rb
index 8e0306ce83b..7668ce5f8be 100644
--- a/spec/features/projects/branches_spec.rb
+++ b/spec/features/projects/branches_spec.rb
@@ -4,7 +4,13 @@ describe 'Branches', feature: true do
let(:project) { create(:project, :public) }
let(:repository) { project.repository }
- context 'logged in' do
+ def set_protected_branch_name(branch_name)
+ find(".js-protected-branch-select").click
+ find(".dropdown-input-field").set(branch_name)
+ click_on("Create wildcard #{branch_name}")
+ end
+
+ context 'logged in as developer' do
before do
login_as :user
project.team << [@user, :developer]
@@ -38,6 +44,83 @@ describe 'Branches', feature: true do
expect(find('.all-branches')).to have_selector('li', count: 1)
end
end
+
+ describe 'Delete unprotected branch' do
+ it 'removes branch after confirmation', 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)
+ find('.js-branch-fix .btn-remove').trigger(:click)
+
+ expect(page).not_to have_content('fix')
+ expect(find('.all-branches')).to have_selector('li', count: 0)
+ end
+ end
+
+ describe 'Delete protected branch' do
+ before do
+ project.add_user(@user, :master)
+ visit namespace_project_protected_branches_path(project.namespace, project)
+ set_protected_branch_name('fix')
+ click_on "Protect"
+
+ within(".protected-branches-list") { expect(page).to have_content('fix') }
+ expect(ProtectedBranch.count).to eq(1)
+ project.add_user(@user, :developer)
+ end
+
+ it 'does not allow devleoper to removes protected branch', 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_css('.btn-remove.disabled')
+ end
+ end
+ end
+
+ context 'logged in as master' do
+ before do
+ login_as :user
+ project.team << [@user, :master]
+ end
+
+ describe 'Delete protected branch' do
+ before do
+ visit namespace_project_protected_branches_path(project.namespace, project)
+ set_protected_branch_name('fix')
+ click_on "Protect"
+
+ within(".protected-branches-list") { expect(page).to have_content('fix') }
+ expect(ProtectedBranch.count).to eq(1)
+ end
+
+ it 'removes branch after modal confirmation', 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)
+ page.find('[data-target="#modal-delete-branch"]').trigger(:click)
+
+ expect(page).to have_css('.js-delete-branch[disabled]')
+ fill_in 'delete_branch_input', with: 'fix'
+ click_link 'Delete protected branch'
+
+ fill_in 'branch-search', with: 'fix'
+ find('#branch-search').native.send_keys(:enter)
+
+ expect(page).to have_content('No branches to show')
+ end
+ end
end
context 'logged out' do
diff --git a/spec/features/projects/commit/cherry_pick_spec.rb b/spec/features/projects/commit/cherry_pick_spec.rb
index 0b972d2a439..fa67d390c47 100644
--- a/spec/features/projects/commit/cherry_pick_spec.rb
+++ b/spec/features/projects/commit/cherry_pick_spec.rb
@@ -1,5 +1,4 @@
require 'spec_helper'
-include WaitForAjax
describe 'Cherry-pick Commits' do
let(:group) { create(:group) }
@@ -75,8 +74,10 @@ describe 'Cherry-pick Commits' do
wait_for_ajax
- page.within('#modal-cherry-pick-commit .dropdown-menu .dropdown-content') do
- click_link 'feature'
+ page.within('#modal-cherry-pick-commit .dropdown-menu') do
+ find('.dropdown-input input').set('feature')
+ wait_for_ajax
+ click_link "feature"
end
page.within('#modal-cherry-pick-commit') do
diff --git a/spec/features/projects/commit/mini_pipeline_graph_spec.rb b/spec/features/projects/commit/mini_pipeline_graph_spec.rb
index 30a2b2bcf8c..98c0f2c63b0 100644
--- a/spec/features/projects/commit/mini_pipeline_graph_spec.rb
+++ b/spec/features/projects/commit/mini_pipeline_graph_spec.rb
@@ -1,8 +1,6 @@
require 'rails_helper'
feature 'Mini Pipeline Graph in Commit View', :js, :feature do
- include WaitForAjax
-
let(:user) { create(:user) }
let(:project) { create(:project, :public) }
diff --git a/spec/features/projects/deploy_keys_spec.rb b/spec/features/projects/deploy_keys_spec.rb
index 0b997f130ea..06abfbbc86b 100644
--- a/spec/features/projects/deploy_keys_spec.rb
+++ b/spec/features/projects/deploy_keys_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe 'Project deploy keys', feature: true do
+describe 'Project deploy keys', :js, :feature do
let(:user) { create(:user) }
let(:project) { create(:project_empty_repo) }
@@ -17,9 +17,13 @@ describe 'Project deploy keys', feature: true do
it 'removes association between project and deploy key' do
visit namespace_project_settings_repository_path(project.namespace, project)
- page.within '.deploy-keys' do
- expect { click_on 'Remove' }
- .to change { project.deploy_keys.count }.by(-1)
+ page.within(find('.deploy-keys')) do
+ expect(page).to have_selector('.deploy-keys li', count: 1)
+
+ click_on 'Remove'
+
+ expect(page).not_to have_selector('.fa-spinner', count: 0)
+ expect(page).to have_selector('.deploy-keys li', count: 0)
end
end
end
diff --git a/spec/features/projects/edit_spec.rb b/spec/features/projects/edit_spec.rb
index 7c319af893b..a263781c43c 100644
--- a/spec/features/projects/edit_spec.rb
+++ b/spec/features/projects/edit_spec.rb
@@ -1,8 +1,6 @@
require 'rails_helper'
feature 'Project edit', feature: true, js: true do
- include WaitForAjax
-
let(:user) { create(:user) }
let(:project) { create(:project) }
diff --git a/spec/features/projects/environments/environment_spec.rb b/spec/features/projects/environments/environment_spec.rb
index acc3efe04e6..86ce50c976f 100644
--- a/spec/features/projects/environments/environment_spec.rb
+++ b/spec/features/projects/environments/environment_spec.rb
@@ -62,6 +62,8 @@ feature 'Environment', :feature do
name: 'deploy to production')
end
+ given(:role) { :master }
+
scenario 'does show a play button' do
expect(page).to have_link(action.name.humanize)
end
@@ -132,6 +134,8 @@ feature 'Environment', :feature do
on_stop: 'close_app')
end
+ given(:role) { :master }
+
scenario 'does allow to stop environment' do
click_link('Stop')
@@ -200,7 +204,7 @@ feature 'Environment', :feature do
end
scenario 'user deletes the branch with running environment' do
- visit namespace_project_branches_path(project.namespace, project)
+ visit namespace_project_branches_path(project.namespace, project, search: 'feature')
remove_branch_with_hooks(project, user, 'feature') do
page.within('.js-branch-feature') { find('a.btn-remove').click }
diff --git a/spec/features/projects/features_visibility_spec.rb b/spec/features/projects/features_visibility_spec.rb
index 9079350186d..4533a6fb144 100644
--- a/spec/features/projects/features_visibility_spec.rb
+++ b/spec/features/projects/features_visibility_spec.rb
@@ -1,9 +1,6 @@
require 'spec_helper'
-include WaitForAjax
describe 'Edit Project Settings', feature: true do
- include WaitForAjax
-
let(:member) { create(:user) }
let!(:project) { create(:project, :public, path: 'gitlab', name: 'sample') }
let!(:issue) { create(:issue, project: project) }
@@ -71,20 +68,23 @@ describe 'Edit Project Settings', feature: true do
end
describe 'project features visibility pages' do
- before do
- @tools =
- {
- builds: namespace_project_pipelines_path(project.namespace, project),
- issues: namespace_project_issues_path(project.namespace, project),
- wiki: namespace_project_wiki_path(project.namespace, project, :home),
- snippets: namespace_project_snippets_path(project.namespace, project),
- merge_requests: namespace_project_merge_requests_path(project.namespace, project),
- }
+ let(:tools) do
+ {
+ builds: namespace_project_pipelines_path(project.namespace, project),
+ issues: namespace_project_issues_path(project.namespace, project),
+ wiki: namespace_project_wiki_path(project.namespace, project, :home),
+ snippets: namespace_project_snippets_path(project.namespace, project),
+ merge_requests: namespace_project_merge_requests_path(project.namespace, project)
+ }
end
context 'normal user' do
+ before do
+ login_as(member)
+ end
+
it 'renders 200 if tool is enabled' do
- @tools.each do |method_name, url|
+ tools.each do |method_name, url|
project.project_feature.update_attribute("#{method_name}_access_level", ProjectFeature::ENABLED)
visit url
expect(page.status_code).to eq(200)
@@ -92,7 +92,7 @@ describe 'Edit Project Settings', feature: true do
end
it 'renders 404 if feature is disabled' do
- @tools.each do |method_name, url|
+ tools.each do |method_name, url|
project.project_feature.update_attribute("#{method_name}_access_level", ProjectFeature::DISABLED)
visit url
expect(page.status_code).to eq(404)
@@ -102,21 +102,21 @@ describe 'Edit Project Settings', feature: true do
it 'renders 404 if feature is enabled only for team members' do
project.team.truncate
- @tools.each do |method_name, url|
+ tools.each do |method_name, url|
project.project_feature.update_attribute("#{method_name}_access_level", ProjectFeature::PRIVATE)
visit url
expect(page.status_code).to eq(404)
end
end
- it 'renders 200 if users is member of group' do
+ it 'renders 200 if user is member of group' do
group = create(:group)
project.group = group
project.save
group.add_owner(member)
- @tools.each do |method_name, url|
+ tools.each do |method_name, url|
project.project_feature.update_attribute("#{method_name}_access_level", ProjectFeature::PRIVATE)
visit url
expect(page.status_code).to eq(200)
@@ -131,7 +131,7 @@ describe 'Edit Project Settings', feature: true do
end
it 'renders 404 if feature is disabled' do
- @tools.each do |method_name, url|
+ tools.each do |method_name, url|
project.project_feature.update_attribute("#{method_name}_access_level", ProjectFeature::DISABLED)
visit url
expect(page.status_code).to eq(404)
@@ -141,7 +141,7 @@ describe 'Edit Project Settings', feature: true do
it 'renders 200 if feature is enabled only for team members' do
project.team.truncate
- @tools.each do |method_name, url|
+ tools.each do |method_name, url|
project.project_feature.update_attribute("#{method_name}_access_level", ProjectFeature::PRIVATE)
visit url
expect(page.status_code).to eq(200)
diff --git a/spec/features/projects/files/browse_files_spec.rb b/spec/features/projects/files/browse_files_spec.rb
index d281043caa3..70e96efd557 100644
--- a/spec/features/projects/files/browse_files_spec.rb
+++ b/spec/features/projects/files/browse_files_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'user browses project', feature: true do
+feature 'user browses project', feature: true, js: true do
let(:project) { create(:project) }
let(:user) { create(:user) }
@@ -13,7 +13,7 @@ feature 'user browses project', feature: true do
scenario "can see blame of '.gitignore'" do
click_link ".gitignore"
click_link 'Blame'
-
+
expect(page).to have_content "*.rb"
expect(page).to have_content "Dmitriy Zaporozhets"
expect(page).to have_content "Initial commit"
@@ -24,6 +24,7 @@ feature 'user browses project', feature: true do
click_link 'files'
click_link 'lfs'
click_link 'lfs_object.iso'
+ wait_for_ajax
expect(page).not_to have_content 'Download (1.5 MB)'
expect(page).to have_content 'version https://git-lfs.github.com/spec/v1'
diff --git a/spec/features/projects/files/creating_a_file_spec.rb b/spec/features/projects/files/creating_a_file_spec.rb
index de6905f2b58..69744ac3948 100644
--- a/spec/features/projects/files/creating_a_file_spec.rb
+++ b/spec/features/projects/files/creating_a_file_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
feature 'User wants to create a file', feature: true do
- include WaitForAjax
-
let(:project) { create(:project) }
let(:user) { create(:user) }
diff --git a/spec/features/projects/files/dockerfile_dropdown_spec.rb b/spec/features/projects/files/dockerfile_dropdown_spec.rb
index 32f33a3ca97..548131c7cd4 100644
--- a/spec/features/projects/files/dockerfile_dropdown_spec.rb
+++ b/spec/features/projects/files/dockerfile_dropdown_spec.rb
@@ -1,13 +1,14 @@
require 'spec_helper'
+require 'fileutils'
feature 'User wants to add a Dockerfile file', feature: true do
- include WaitForAjax
-
before do
user = create(:user)
project = create(:project)
project.team << [user, :master]
+
login_as user
+
visit namespace_project_new_blob_path(project.namespace, project, 'master', file_name: 'Dockerfile')
end
@@ -17,11 +18,14 @@ feature 'User wants to add a Dockerfile file', feature: true do
scenario 'user can pick a Dockerfile file from the dropdown', js: true do
find('.js-dockerfile-selector').click
+
wait_for_ajax
+
within '.dockerfile-selector' do
find('.dropdown-input-field').set('HTTPd')
find('.dropdown-content li', text: 'HTTPd').click
end
+
wait_for_ajax
expect(page).to have_css('.dockerfile-selector .dropdown-toggle-text', text: 'HTTPd')
diff --git a/spec/features/projects/files/editing_a_file_spec.rb b/spec/features/projects/files/editing_a_file_spec.rb
index 4da34108b46..7a3afafec29 100644
--- a/spec/features/projects/files/editing_a_file_spec.rb
+++ b/spec/features/projects/files/editing_a_file_spec.rb
@@ -1,8 +1,6 @@
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
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
index 10b91d8990b..5c8105de4cb 100644
--- a/spec/features/projects/files/files_sort_submodules_with_folders_spec.rb
+++ b/spec/features/projects/files/files_sort_submodules_with_folders_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
feature 'User views files page', feature: true do
- include WaitForAjax
-
let(:user) { create(:user) }
let(:project) { create(:forked_project_with_submodules) }
diff --git a/spec/features/projects/files/find_file_keyboard_spec.rb b/spec/features/projects/files/find_file_keyboard_spec.rb
index 582349d8d5b..e7a6749d8ac 100644
--- a/spec/features/projects/files/find_file_keyboard_spec.rb
+++ b/spec/features/projects/files/find_file_keyboard_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
feature 'Find file keyboard shortcuts', feature: true, js: true do
- include WaitForAjax
-
let(:user) { create(:user) }
let(:project) { create(:project) }
diff --git a/spec/features/projects/files/find_files_spec.rb b/spec/features/projects/files/find_files_spec.rb
new file mode 100644
index 00000000000..716b7591b95
--- /dev/null
+++ b/spec/features/projects/files/find_files_spec.rb
@@ -0,0 +1,30 @@
+require 'spec_helper'
+
+feature 'Find files button in the tree header', feature: true do
+ given(:user) { create(:user) }
+ given(:project) { create(:project) }
+
+ background do
+ login_as(user)
+ project.team << [user, :developer]
+ end
+
+ scenario 'project main screen' do
+ visit namespace_project_path(
+ project.namespace,
+ project
+ )
+
+ expect(page).to have_selector('.tree-controls .shortcuts-find-file')
+ end
+
+ scenario 'project tree screen' do
+ visit namespace_project_tree_path(
+ project.namespace,
+ project,
+ project.default_branch
+ )
+
+ expect(page).to have_selector('.tree-controls .shortcuts-find-file')
+ end
+end
diff --git a/spec/features/projects/files/gitignore_dropdown_spec.rb b/spec/features/projects/files/gitignore_dropdown_spec.rb
index 9ebef505b92..e59428f8b24 100644
--- a/spec/features/projects/files/gitignore_dropdown_spec.rb
+++ b/spec/features/projects/files/gitignore_dropdown_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
feature 'User wants to add a .gitignore file', feature: true do
- include WaitForAjax
-
before do
user = create(:user)
project = create(:project)
diff --git a/spec/features/projects/files/gitlab_ci_yml_dropdown_spec.rb b/spec/features/projects/files/gitlab_ci_yml_dropdown_spec.rb
index fca40f68b01..85b66b93fba 100644
--- a/spec/features/projects/files/gitlab_ci_yml_dropdown_spec.rb
+++ b/spec/features/projects/files/gitlab_ci_yml_dropdown_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
feature 'User wants to add a .gitlab-ci.yml file', feature: true do
- include WaitForAjax
-
before do
user = create(:user)
project = create(:project)
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 8ff0f5898ec..249830921ac 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
@@ -1,8 +1,6 @@
require 'spec_helper'
feature 'project owner creates a license file', feature: true, js: true do
- include WaitForAjax
-
let(:project_master) { create(:user) }
let(:project) { create(:project) }
background do
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 1a1910455a1..70a41886985 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
@@ -1,8 +1,6 @@
require 'spec_helper'
feature 'project owner sees a link to create a license file in empty project', feature: true, js: true do
- include WaitForAjax
-
let(:project_master) { create(:user) }
let(:project) { create(:empty_project) }
background do
diff --git a/spec/features/projects/files/undo_template_spec.rb b/spec/features/projects/files/undo_template_spec.rb
index c51851d3f94..cd3af0b7d29 100644
--- a/spec/features/projects/files/undo_template_spec.rb
+++ b/spec/features/projects/files/undo_template_spec.rb
@@ -1,26 +1,25 @@
require 'spec_helper'
-include WaitForAjax
-feature 'Template Undo Button', js: true do
+feature 'Template Undo Button', js: true do
let(:project) { create(:project) }
let(:user) { create(:user) }
before do
project.team << [user, :master]
- login_as user
+ login_as user
end
-
- context 'editing a matching file and applying a template' do
+
+ context 'editing a matching file and applying a template' do
before do
- visit namespace_project_edit_blob_path(project.namespace, project, File.join(project.default_branch, "LICENSE"))
+ visit namespace_project_edit_blob_path(project.namespace, project, File.join(project.default_branch, "LICENSE"))
select_file_template('.js-license-selector', 'Apache License 2.0')
end
-
+
scenario 'reverts template application' do
try_template_undo('http://www.apache.org/licenses/', 'Apply a license template')
end
end
-
+
context 'creating a non-matching file' do
before do
visit namespace_project_new_blob_path(project.namespace, project, 'master')
diff --git a/spec/features/projects/import_export/test_project_export.tar.gz b/spec/features/projects/import_export/test_project_export.tar.gz
index 399c1d478c5..4efd5a26a82 100644
--- a/spec/features/projects/import_export/test_project_export.tar.gz
+++ b/spec/features/projects/import_export/test_project_export.tar.gz
Binary files differ
diff --git a/spec/features/projects/issuable_templates_spec.rb b/spec/features/projects/issuable_templates_spec.rb
index 6cdca0f114b..fa5e30075e3 100644
--- a/spec/features/projects/issuable_templates_spec.rb
+++ b/spec/features/projects/issuable_templates_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
feature 'issuable templates', feature: true, js: true do
- include WaitForAjax
-
let(:user) { create(:user) }
let(:project) { create(:project, :public) }
@@ -14,7 +12,7 @@ feature 'issuable templates', feature: true, js: true do
context 'user creates an issue using templates' do
let(:template_content) { 'this is a test "bug" template' }
let(:longtemplate_content) { %Q(this\n\n\n\n\nis\n\n\n\n\na\n\n\n\n\nbug\n\n\n\n\ntemplate) }
- let(:issue) { create(:issue, author: user, assignee: user, project: project) }
+ let(:issue) { create(:issue, author: user, assignees: [user], project: project) }
let(:description_addition) { ' appending to description' }
background do
@@ -74,7 +72,7 @@ feature 'issuable templates', feature: true, js: true do
context 'user creates an issue using templates, with a prior description' do
let(:prior_description) { 'test issue description' }
let(:template_content) { 'this is a test "bug" template' }
- let(:issue) { create(:issue, author: user, assignee: user, project: project) }
+ let(:issue) { create(:issue, author: user, assignees: [user], project: project) }
background do
project.repository.create_file(
diff --git a/spec/features/projects/labels/update_prioritization_spec.rb b/spec/features/projects/labels/update_prioritization_spec.rb
index 1e900d7e660..836f81fb16d 100644
--- a/spec/features/projects/labels/update_prioritization_spec.rb
+++ b/spec/features/projects/labels/update_prioritization_spec.rb
@@ -1,7 +1,6 @@
require 'spec_helper'
feature 'Prioritize labels', feature: true do
- include WaitForAjax
include DragTo
let(:user) { create(:user) }
diff --git a/spec/features/projects/members/group_links_spec.rb b/spec/features/projects/members/group_links_spec.rb
index cffb935ad5a..ab2b089db2e 100644
--- a/spec/features/projects/members/group_links_spec.rb
+++ b/spec/features/projects/members/group_links_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
feature 'Projects > Members > Anonymous user sees members', feature: true, js: true do
- include WaitForAjax
-
let(:user) { create(:user) }
let(:group) { create(:group, :public) }
let(:project) { create(:empty_project, :public) }
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
index c3f45be6e4b..19d14ad9af4 100644
--- 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
@@ -1,7 +1,6 @@
require 'spec_helper'
feature 'Projects > Members > Master adds member with expiration date', feature: true, js: true do
- include WaitForAjax
include Select2Helper
include ActiveSupport::Testing::TimeHelpers
diff --git a/spec/features/projects/members/sorting_spec.rb b/spec/features/projects/members/sorting_spec.rb
index c7a32a65e49..b7ae5f0b925 100644
--- a/spec/features/projects/members/sorting_spec.rb
+++ b/spec/features/projects/members/sorting_spec.rb
@@ -68,7 +68,7 @@ feature 'Projects > Members > Sorting', feature: true do
expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Name, descending')
end
- scenario 'sorts by recent sign in' do
+ scenario 'sorts by recent sign in', :redis do
visit_members_list(sort: :recent_sign_in)
expect(first_member).to include(master.name)
@@ -76,7 +76,7 @@ feature 'Projects > Members > Sorting', feature: true do
expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Recent sign in')
end
- scenario 'sorts by oldest sign in' do
+ scenario 'sorts by oldest sign in', :redis do
visit_members_list(sort: :oldest_sign_in)
expect(first_member).to include(developer.name)
diff --git a/spec/features/projects/members/user_requests_access_spec.rb b/spec/features/projects/members/user_requests_access_spec.rb
index de25d45f447..1bf8f710b9f 100644
--- a/spec/features/projects/members/user_requests_access_spec.rb
+++ b/spec/features/projects/members/user_requests_access_spec.rb
@@ -31,6 +31,17 @@ feature 'Projects > Members > User requests access', feature: true do
expect(page).not_to have_content 'Leave Project'
end
+ context 'code access is restricted' do
+ scenario 'user can request access' do
+ project.project_feature.update!(repository_access_level: ProjectFeature::PRIVATE,
+ builds_access_level: ProjectFeature::PRIVATE,
+ merge_requests_access_level: ProjectFeature::PRIVATE)
+ visit namespace_project_path(project.namespace, project)
+
+ expect(page).to have_content 'Request Access'
+ end
+ end
+
scenario 'user is not listed in the project members page' do
click_link 'Request Access'
diff --git a/spec/features/projects/merge_request_button_spec.rb b/spec/features/projects/merge_request_button_spec.rb
index 05f3162f13c..1370ab1c521 100644
--- a/spec/features/projects/merge_request_button_spec.rb
+++ b/spec/features/projects/merge_request_button_spec.rb
@@ -85,8 +85,8 @@ feature 'Merge Request button', feature: true do
context 'on branches page' do
it_behaves_like 'Merge request button only shown when allowed' do
let(:label) { 'Merge request' }
- let(:url) { namespace_project_branches_path(project.namespace, project) }
- let(:fork_url) { namespace_project_branches_path(forked_project.namespace, forked_project) }
+ let(:url) { namespace_project_branches_path(project.namespace, project, search: 'feature') }
+ let(:fork_url) { namespace_project_branches_path(forked_project.namespace, forked_project, search: 'feature') }
end
end
diff --git a/spec/features/projects/milestones/milestone_spec.rb b/spec/features/projects/milestones/milestone_spec.rb
index dab78fd3571..b4fc0edbde8 100644
--- a/spec/features/projects/milestones/milestone_spec.rb
+++ b/spec/features/projects/milestones/milestone_spec.rb
@@ -63,4 +63,27 @@ feature 'Project milestone', :feature do
expect(page).not_to have_content('Assign some issues to this milestone.')
end
end
+
+ context 'when project has an issue' do
+ before do
+ create(:issue, project: project, milestone: milestone)
+
+ visit namespace_project_milestone_path(project.namespace, project, milestone)
+ end
+
+ describe 'the collapsed sidebar' do
+ before do
+ find('.milestone-sidebar .gutter-toggle').click
+ end
+
+ it 'shows the total MR and issue counts' do
+ find('.milestone-sidebar .block', match: :first)
+
+ aggregate_failures 'MR and issue blocks' do
+ expect(find('.milestone-sidebar .block.issues')).to have_content 1
+ expect(find('.milestone-sidebar .block.merge-requests')).to have_content 0
+ end
+ end
+ end
+ end
end
diff --git a/spec/features/projects/pipeline_schedules_spec.rb b/spec/features/projects/pipeline_schedules_spec.rb
new file mode 100644
index 00000000000..fe9f94db574
--- /dev/null
+++ b/spec/features/projects/pipeline_schedules_spec.rb
@@ -0,0 +1,141 @@
+require 'spec_helper'
+
+feature 'Pipeline Schedules', :feature do
+ include PipelineSchedulesHelper
+ include WaitForAjax
+
+ let!(:project) { create(:project) }
+ let!(:pipeline_schedule) { create(:ci_pipeline_schedule, project: project) }
+ let!(:pipeline) { create(:ci_pipeline, pipeline_schedule: pipeline_schedule) }
+ let(:scope) { nil }
+ let!(:user) { create(:user) }
+
+ before do
+ project.add_master(user)
+
+ login_as(user)
+ visit_page
+ end
+
+ describe 'GET /projects/pipeline_schedules' do
+ let(:visit_page) { visit_pipelines_schedules }
+
+ it 'avoids N + 1 queries' do
+ control_count = ActiveRecord::QueryRecorder.new { visit_pipelines_schedules }.count
+
+ create_list(:ci_pipeline_schedule, 2, project: project)
+
+ expect { visit_pipelines_schedules }.not_to exceed_query_limit(control_count)
+ end
+
+ describe 'The view' do
+ it 'displays the required information description' do
+ page.within('.pipeline-schedule-table-row') do
+ expect(page).to have_content('pipeline schedule')
+ expect(page).to have_link('master')
+ expect(page).to have_link("##{pipeline.id}")
+ end
+ end
+
+ it 'creates a new scheduled pipeline' do
+ click_link 'New Schedule'
+
+ expect(page).to have_content('Schedule a new pipeline')
+ end
+
+ it 'changes ownership of the pipeline' do
+ click_link 'Take ownership'
+ page.within('.pipeline-schedule-table-row') do
+ expect(page).not_to have_content('No owner')
+ expect(page).to have_link('John Doe')
+ end
+ end
+
+ it 'edits the pipeline' do
+ page.within('.pipeline-schedule-table-row') do
+ click_link 'Edit'
+ end
+
+ expect(page).to have_content('Edit Pipeline Schedule')
+ end
+
+ it 'deletes the pipeline' do
+ click_link 'Delete'
+
+ expect(page).not_to have_content('pipeline schedule')
+ end
+ end
+ end
+
+ describe 'POST /projects/pipeline_schedules/new', js: true do
+ let(:visit_page) { visit_new_pipeline_schedule }
+
+ it 'it creates a new scheduled pipeline' do
+ fill_in_schedule_form
+ save_pipeline_schedule
+
+ expect(page).to have_content('my fancy description')
+ end
+
+ it 'it prevents an invalid form from being submitted' do
+ save_pipeline_schedule
+
+ expect(page).to have_content('This field is required')
+ end
+ end
+
+ describe 'PATCH /projects/pipelines_schedules/:id/edit', js: true do
+ let(:visit_page) do
+ edit_pipeline_schedule
+ end
+
+ it 'it displays existing properties' do
+ description = find_field('schedule_description').value
+ expect(description).to eq('pipeline schedule')
+ expect(page).to have_button('master')
+ expect(page).to have_button('UTC')
+ end
+
+ it 'edits the scheduled pipeline' do
+ fill_in 'schedule_description', with: 'my brand new description'
+
+ save_pipeline_schedule
+
+ expect(page).to have_content('my brand new description')
+ end
+ end
+
+ def visit_new_pipeline_schedule
+ visit new_namespace_project_pipeline_schedule_path(project.namespace, project, pipeline_schedule)
+ end
+
+ def edit_pipeline_schedule
+ visit edit_namespace_project_pipeline_schedule_path(project.namespace, project, pipeline_schedule)
+ end
+
+ def visit_pipelines_schedules
+ visit namespace_project_pipeline_schedules_path(project.namespace, project, scope: scope)
+ end
+
+ def select_timezone
+ click_button 'Select a timezone'
+ click_link 'American Samoa'
+ end
+
+ def select_target_branch
+ click_button 'Select target branch'
+ click_link 'master'
+ end
+
+ def save_pipeline_schedule
+ click_button 'Save pipeline schedule'
+ end
+
+ def fill_in_schedule_form
+ fill_in 'schedule_description', with: 'my fancy description'
+ fill_in 'schedule_cron', with: '* 1 2 3 4'
+
+ select_timezone
+ select_target_branch
+ end
+end
diff --git a/spec/features/projects/pipelines/pipeline_spec.rb b/spec/features/projects/pipelines/pipeline_spec.rb
index 5a53e48f5f8..cfac54ef259 100644
--- a/spec/features/projects/pipelines/pipeline_spec.rb
+++ b/spec/features/projects/pipelines/pipeline_spec.rb
@@ -254,4 +254,57 @@ describe 'Pipeline', :feature, :js do
it { expect(build_manual.reload).to be_pending }
end
end
+
+ describe 'GET /:project/pipelines/:id/failures' do
+ let(:project) { create(:project) }
+ let(:pipeline) { create(:ci_pipeline, project: project, ref: 'master', sha: project.commit.id) }
+ let(:pipeline_failures_page) { failures_namespace_project_pipeline_path(project.namespace, project, pipeline) }
+ let!(:failed_build) { create(:ci_build, :failed, pipeline: pipeline) }
+
+ context 'with failed build' do
+ before do
+ failed_build.trace.set('4 examples, 1 failure')
+
+ visit pipeline_failures_page
+ end
+
+ it 'shows jobs tab pane as active' do
+ expect(page).to have_content('Failed Jobs')
+ expect(page).to have_css('#js-tab-failures.active')
+ end
+
+ it 'lists failed builds' do
+ expect(page).to have_content(failed_build.name)
+ expect(page).to have_content(failed_build.stage)
+ end
+
+ it 'shows build failure logs' do
+ expect(page).to have_content('4 examples, 1 failure')
+ end
+ end
+
+ context 'when missing build logs' do
+ before do
+ visit pipeline_failures_page
+ end
+
+ it 'includes failed jobs' do
+ expect(page).to have_content('No job trace')
+ end
+ end
+
+ context 'without failures' do
+ before do
+ failed_build.update!(status: :success)
+
+ visit pipeline_failures_page
+ end
+
+ it 'displays the pipeline graph' do
+ expect(current_path).to eq(pipeline_path(pipeline))
+ expect(page).not_to have_content('Failed Jobs')
+ expect(page).to have_selector('.pipeline-visualization')
+ end
+ end
+ end
end
diff --git a/spec/features/projects/pipelines/pipelines_spec.rb b/spec/features/projects/pipelines/pipelines_spec.rb
index 2272b19bc8f..5f82cf2f5e5 100644
--- a/spec/features/projects/pipelines/pipelines_spec.rb
+++ b/spec/features/projects/pipelines/pipelines_spec.rb
@@ -22,7 +22,7 @@ describe 'Pipelines', :feature, :js do
project: project,
ref: 'master',
status: 'running',
- sha: project.commit.id,
+ sha: project.commit.id
)
end
@@ -370,6 +370,58 @@ describe 'Pipelines', :feature, :js do
end
end
+ describe 'GET /:project/pipelines/show' do
+ let(:project) { create(:project) }
+
+ let(:pipeline) do
+ create(:ci_empty_pipeline,
+ project: project,
+ sha: project.commit.id,
+ user: user)
+ end
+
+ before do
+ create_build('build', 0, 'build', :success)
+ create_build('test', 1, 'rspec 0:2', :pending)
+ create_build('test', 1, 'rspec 1:2', :running)
+ create_build('test', 1, 'spinach 0:2', :created)
+ create_build('test', 1, 'spinach 1:2', :created)
+ create_build('test', 1, 'audit', :created)
+ create_build('deploy', 2, 'production', :created)
+
+ create(:generic_commit_status, pipeline: pipeline, stage: 'external', name: 'jenkins', stage_idx: 3)
+
+ visit namespace_project_pipeline_path(project.namespace, project, pipeline)
+ wait_for_vue_resource
+ end
+
+ it 'shows a graph with grouped stages' do
+ expect(page).to have_css('.js-pipeline-graph')
+
+ # header
+ expect(page).to have_text("##{pipeline.id}")
+ expect(page).to have_selector(%Q(img[alt$="#{pipeline.user.name}'s avatar"]))
+ expect(page).to have_link(pipeline.user.name, href: user_path(pipeline.user))
+
+ # stages
+ expect(page).to have_text('Build')
+ expect(page).to have_text('Test')
+ expect(page).to have_text('Deploy')
+ expect(page).to have_text('External')
+
+ # builds
+ expect(page).to have_text('rspec')
+ expect(page).to have_text('spinach')
+ expect(page).to have_text('rspec')
+ expect(page).to have_text('production')
+ expect(page).to have_text('jenkins')
+ end
+
+ def create_build(stage, stage_idx, name, status)
+ create(:ci_build, pipeline: pipeline, stage: stage, stage_idx: stage_idx, name: name, status: status)
+ end
+ end
+
describe 'POST /:project/pipelines' do
let(:project) { create(:project) }
diff --git a/spec/features/projects/project_settings_spec.rb b/spec/features/projects/project_settings_spec.rb
index 5d0314d5c09..11dcab4d737 100644
--- a/spec/features/projects/project_settings_spec.rb
+++ b/spec/features/projects/project_settings_spec.rb
@@ -1,64 +1,158 @@
require 'spec_helper'
describe 'Edit Project Settings', feature: true do
+ include Select2Helper
+
let(:user) { create(:user) }
- let(:project) { create(:empty_project, path: 'gitlab', name: 'sample') }
+ let(:project) { create(:empty_project, namespace: user.namespace, path: 'gitlab', name: 'sample') }
before do
login_as(user)
- project.team << [user, :master]
end
- describe 'Project settings', js: true do
+ describe 'Project settings section', 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, emojis, '_', '.', dash, space. It must start with letter, digit, emoji or '_'."
expect(page).to have_button 'Save changes'
end
- scenario 'shows a successful notice when the project is updated' do
+ it 'shows a successful notice when the project is updated' do
visit edit_namespace_project_path(project.namespace, project)
-
fill_in 'project_name_edit', with: 'hello world'
-
click_button 'Save changes'
-
expect(page).to have_content "Project 'hello world' was successfully updated."
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'
+ describe 'Rename repository section' do
+ context 'with invalid characters' do
+ it 'shows errors for invalid project path/name' do
+ rename_project(project, name: 'foo&bar', path: 'foo&bar')
+ 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, emojis, '_', '.', dash, space. It must start with letter, digit, emoji 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
- click_button 'Rename project'
+ context 'when changing project name' do
+ it 'renames the repository' do
+ rename_project(project, name: 'bar')
+ expect(find('h1.title')).to have_content(project.name)
+ end
+
+ context 'with emojis' do
+ it 'shows error for invalid project name' do
+ rename_project(project, name: '🚀 foo bar ☁️')
+ expect(page).to have_field 'Project name', with: '🚀 foo bar ☁️'
+ expect(page).not_to have_content "Name can contain only letters, digits, emojis '_', '.', dash and space. It must start with letter, digit, emoji or '_'."
+ end
+ end
+ end
- 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, emojis, '_', '.', dash, space. It must start with letter, digit, emoji or '_'."
- expect(page).to have_content "Path can contain only letters, digits, '_', '-' and '.'. Cannot start with '-', end in '.git' or end in '.atom'"
+ context 'when changing project path' do
+ # Not using empty project because we need a repo to exist
+ let(:project) { create(:project, namespace: user.namespace, name: 'gitlabhq') }
+
+ before(:context) { TestEnv.clean_test_path }
+ after(:example) { TestEnv.clean_test_path }
+
+ specify 'the project is accessible via the new path' do
+ rename_project(project, path: 'bar')
+ new_path = namespace_project_path(project.namespace, 'bar')
+ visit new_path
+ expect(current_path).to eq(new_path)
+ expect(find('h1.title')).to have_content(project.name)
+ end
+
+ specify 'the project is accessible via a redirect from the old path' do
+ old_path = namespace_project_path(project.namespace, project)
+ rename_project(project, path: 'bar')
+ new_path = namespace_project_path(project.namespace, 'bar')
+ visit old_path
+ expect(current_path).to eq(new_path)
+ expect(find('h1.title')).to have_content(project.name)
+ end
+
+ context 'and a new project is added with the same path' do
+ it 'overrides the redirect' do
+ old_path = namespace_project_path(project.namespace, project)
+ rename_project(project, path: 'bar')
+ new_project = create(:empty_project, namespace: user.namespace, path: 'gitlabhq', name: 'quz')
+ visit old_path
+ expect(current_path).to eq(old_path)
+ expect(find('h1.title')).to have_content(new_project.name)
+ end
+ end
end
end
- describe 'Rename repository name with emojis' do
- it 'shows error for invalid project name' do
- visit edit_namespace_project_path(project.namespace, project)
-
- fill_in 'project_name', with: '🚀 foo bar ☁️'
+ describe 'Transfer project section', js: true do
+ # Not using empty project because we need a repo to exist
+ let!(:project) { create(:project, namespace: user.namespace, name: 'gitlabhq') }
+ let!(:group) { create(:group) }
+
+ before(:context) { TestEnv.clean_test_path }
+ before(:example) { group.add_owner(user) }
+ after(:example) { TestEnv.clean_test_path }
+
+ specify 'the project is accessible via the new path' do
+ transfer_project(project, group)
+ new_path = namespace_project_path(group, project)
+ visit new_path
+ expect(current_path).to eq(new_path)
+ expect(find('h1.title')).to have_content(project.name)
+ end
- click_button 'Rename project'
+ specify 'the project is accessible via a redirect from the old path' do
+ old_path = namespace_project_path(project.namespace, project)
+ transfer_project(project, group)
+ new_path = namespace_project_path(group, project)
+ visit old_path
+ expect(current_path).to eq(new_path)
+ expect(find('h1.title')).to have_content(project.name)
+ end
- expect(page).to have_field 'Project name', with: '🚀 foo bar ☁️'
- expect(page).not_to have_content "Name can contain only letters, digits, emojis '_', '.', dash and space. It must start with letter, digit, emoji or '_'."
+ context 'and a new project is added with the same path' do
+ it 'overrides the redirect' do
+ old_path = namespace_project_path(project.namespace, project)
+ transfer_project(project, group)
+ new_project = create(:empty_project, namespace: user.namespace, path: 'gitlabhq', name: 'quz')
+ visit old_path
+ expect(current_path).to eq(old_path)
+ expect(find('h1.title')).to have_content(new_project.name)
+ end
end
end
end
+
+def rename_project(project, name: nil, path: nil)
+ visit edit_namespace_project_path(project.namespace, project)
+ fill_in('project_name', with: name) if name
+ fill_in('Path', with: path) if path
+ click_button('Rename project')
+ wait_for_edit_project_page_reload
+ project.reload
+end
+
+def transfer_project(project, namespace)
+ visit edit_namespace_project_path(project.namespace, project)
+ select2(namespace.id, from: '#new_namespace_id')
+ click_button('Transfer project')
+ confirm_transfer_modal
+ wait_for_edit_project_page_reload
+ project.reload
+end
+
+def confirm_transfer_modal
+ fill_in('confirm_name_input', with: project.path)
+ click_button 'Confirm'
+end
+
+def wait_for_edit_project_page_reload
+ expect(find('.project-edit-container')).to have_content('Rename repository')
+end
diff --git a/spec/features/projects/ref_switcher_spec.rb b/spec/features/projects/ref_switcher_spec.rb
index 3b8f0b2d3f8..881ad7910dd 100644
--- a/spec/features/projects/ref_switcher_spec.rb
+++ b/spec/features/projects/ref_switcher_spec.rb
@@ -1,7 +1,6 @@
require 'rails_helper'
feature 'Ref switcher', feature: true, js: true do
- include WaitForAjax
let(:user) { create(:user) }
let(:project) { create(:project, :public) }
diff --git a/spec/features/projects/settings/integration_settings_spec.rb b/spec/features/projects/settings/integration_settings_spec.rb
new file mode 100644
index 00000000000..7909234556e
--- /dev/null
+++ b/spec/features/projects/settings/integration_settings_spec.rb
@@ -0,0 +1,94 @@
+require 'spec_helper'
+
+feature 'Integration settings', feature: true do
+ let(:project) { create(:empty_project) }
+ let(:user) { create(:user) }
+ let(:role) { :developer }
+ let(:integrations_path) { namespace_project_settings_integrations_path(project.namespace, project) }
+
+ background do
+ login_as(user)
+ project.team << [user, role]
+ end
+
+ context 'for developer' do
+ given(:role) { :developer }
+
+ scenario 'to be disallowed to view' do
+ visit integrations_path
+
+ expect(page.status_code).to eq(404)
+ end
+ end
+
+ context 'for master' do
+ given(:role) { :master }
+
+ context 'Webhooks' do
+ let(:hook) { create(:project_hook, :all_events_enabled, enable_ssl_verification: true, project: project) }
+ let(:url) { generate(:url) }
+
+ scenario 'show list of webhooks' do
+ hook
+
+ visit integrations_path
+
+ expect(page.status_code).to eq(200)
+ expect(page).to have_content(hook.url)
+ expect(page).to have_content('SSL Verification: enabled')
+ expect(page).to have_content('Push Events')
+ expect(page).to have_content('Tag Push Events')
+ expect(page).to have_content('Issues Events')
+ expect(page).to have_content('Confidential Issues Events')
+ expect(page).to have_content('Note Events')
+ expect(page).to have_content('Merge Requests Events')
+ expect(page).to have_content('Pipeline Events')
+ expect(page).to have_content('Wiki Page Events')
+ end
+
+ scenario 'create webhook' do
+ visit integrations_path
+
+ fill_in 'hook_url', with: url
+ check 'Tag push events'
+ check 'Enable SSL verification'
+
+ click_button 'Add webhook'
+
+ expect(page).to have_content(url)
+ expect(page).to have_content('SSL Verification: enabled')
+ expect(page).to have_content('Push Events')
+ expect(page).to have_content('Tag Push Events')
+ end
+
+ scenario 'edit existing webhook' do
+ hook
+ visit integrations_path
+
+ click_link 'Edit'
+ fill_in 'hook_url', with: url
+ check 'Enable SSL verification'
+ click_button 'Save changes'
+
+ expect(page).to have_content 'SSL Verification: enabled'
+ expect(page).to have_content(url)
+ end
+
+ scenario 'test existing webhook' do
+ WebMock.stub_request(:post, hook.url)
+ visit integrations_path
+
+ click_link 'Test'
+
+ expect(current_path).to eq(integrations_path)
+ end
+
+ scenario 'remove existing webhook' do
+ hook
+ visit integrations_path
+
+ expect { click_link 'Remove' }.to change(ProjectHook, :count).by(-1)
+ end
+ end
+ end
+end
diff --git a/spec/features/projects/snippets/show_spec.rb b/spec/features/projects/snippets/show_spec.rb
new file mode 100644
index 00000000000..cedf3778c7e
--- /dev/null
+++ b/spec/features/projects/snippets/show_spec.rb
@@ -0,0 +1,144 @@
+require 'spec_helper'
+
+feature 'Project snippet', :js, feature: true do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :repository) }
+ let(:snippet) { create(:project_snippet, project: project, file_name: file_name, content: content) }
+
+ before do
+ project.team << [user, :master]
+ login_as(user)
+ end
+
+ context 'Ruby file' do
+ let(:file_name) { 'popen.rb' }
+ let(:content) { project.repository.blob_at('master', 'files/ruby/popen.rb').data }
+
+ before do
+ visit namespace_project_snippet_path(project.namespace, project, snippet)
+
+ wait_for_ajax
+ end
+
+ it 'displays the blob' do
+ aggregate_failures do
+ # shows highlighted Ruby code
+ expect(page).to have_content("require 'fileutils'")
+
+ # does not show a viewer switcher
+ expect(page).not_to have_selector('.js-blob-viewer-switcher')
+
+ # shows an enabled copy button
+ expect(page).to have_selector('.js-copy-blob-source-btn:not(.disabled)')
+
+ # shows a raw button
+ expect(page).to have_link('Open raw')
+
+ # shows a download button
+ expect(page).to have_link('Download')
+ end
+ end
+ end
+
+ context 'Markdown file' do
+ let(:file_name) { 'ruby-style-guide.md' }
+ let(:content) { project.repository.blob_at('master', 'files/markdown/ruby-style-guide.md').data }
+
+ context 'visiting directly' do
+ before do
+ visit namespace_project_snippet_path(project.namespace, project, snippet)
+
+ wait_for_ajax
+ end
+
+ it 'displays the blob using the rich viewer' do
+ aggregate_failures do
+ # hides the simple viewer
+ expect(page).to have_selector('.blob-viewer[data-type="simple"]', visible: false)
+ expect(page).to have_selector('.blob-viewer[data-type="rich"]')
+
+ # shows rendered Markdown
+ expect(page).to have_link("PEP-8")
+
+ # shows a viewer switcher
+ expect(page).to have_selector('.js-blob-viewer-switcher')
+
+ # shows a disabled copy button
+ expect(page).to have_selector('.js-copy-blob-source-btn.disabled')
+
+ # shows a raw button
+ expect(page).to have_link('Open raw')
+
+ # shows a download button
+ expect(page).to have_link('Download')
+ end
+ end
+
+ context 'switching to the simple viewer' do
+ before do
+ find('.js-blob-viewer-switch-btn[data-viewer=simple]').click
+
+ wait_for_ajax
+ end
+
+ it 'displays the blob using the simple viewer' do
+ aggregate_failures do
+ # hides the rich viewer
+ expect(page).to have_selector('.blob-viewer[data-type="simple"]')
+ expect(page).to have_selector('.blob-viewer[data-type="rich"]', visible: false)
+
+ # shows highlighted Markdown code
+ expect(page).to have_content("[PEP-8](http://www.python.org/dev/peps/pep-0008/)")
+
+ # shows an enabled copy button
+ expect(page).to have_selector('.js-copy-blob-source-btn:not(.disabled)')
+ end
+ end
+
+ context 'switching to the rich viewer again' do
+ before do
+ find('.js-blob-viewer-switch-btn[data-viewer=rich]').click
+
+ wait_for_ajax
+ end
+
+ it 'displays the blob using the rich viewer' do
+ aggregate_failures do
+ # hides the simple viewer
+ expect(page).to have_selector('.blob-viewer[data-type="simple"]', visible: false)
+ expect(page).to have_selector('.blob-viewer[data-type="rich"]')
+
+ # shows an enabled copy button
+ expect(page).to have_selector('.js-copy-blob-source-btn:not(.disabled)')
+ end
+ end
+ end
+ end
+ end
+
+ context 'visiting with a line number anchor' do
+ before do
+ visit namespace_project_snippet_path(project.namespace, project, snippet, anchor: 'L1')
+
+ wait_for_ajax
+ end
+
+ it 'displays the blob using the simple viewer' do
+ aggregate_failures do
+ # hides the rich viewer
+ expect(page).to have_selector('.blob-viewer[data-type="simple"]')
+ expect(page).to have_selector('.blob-viewer[data-type="rich"]', visible: false)
+
+ # highlights the line in question
+ expect(page).to have_selector('#LC1.hll')
+
+ # shows highlighted Markdown code
+ expect(page).to have_content("[PEP-8](http://www.python.org/dev/peps/pep-0008/)")
+
+ # shows an enabled copy button
+ expect(page).to have_selector('.js-copy-blob-source-btn:not(.disabled)')
+ end
+ end
+ end
+ end
+end
diff --git a/spec/features/projects/snippets_spec.rb b/spec/features/projects/snippets_spec.rb
index d37e8ed4699..18689c17fe9 100644
--- a/spec/features/projects/snippets_spec.rb
+++ b/spec/features/projects/snippets_spec.rb
@@ -4,11 +4,27 @@ describe 'Project snippets', feature: true do
context 'when the project has snippets' do
let(:project) { create(:empty_project, :public) }
let!(:snippets) { create_list(:project_snippet, 2, :public, author: project.owner, project: project) }
- before do
- allow(Snippet).to receive(:default_per_page).and_return(1)
- visit namespace_project_snippets_path(project.namespace, project)
+ let!(:other_snippet) { create(:project_snippet) }
+
+ context 'pagination' do
+ before do
+ allow(Snippet).to receive(:default_per_page).and_return(1)
+
+ visit namespace_project_snippets_path(project.namespace, project)
+ end
+
+ it_behaves_like 'paginated snippets'
end
- it_behaves_like 'paginated snippets'
+ context 'list content' do
+ it 'contains all project snippets' do
+ visit namespace_project_snippets_path(project.namespace, project)
+
+ expect(page).to have_selector('.snippet-row', count: 2)
+
+ expect(page).to have_content(snippets[0].title)
+ expect(page).to have_content(snippets[1].title)
+ end
+ end
end
end
diff --git a/spec/features/projects/user_create_dir_spec.rb b/spec/features/projects/user_create_dir_spec.rb
index 2065abfb248..5dfdc465d7d 100644
--- a/spec/features/projects/user_create_dir_spec.rb
+++ b/spec/features/projects/user_create_dir_spec.rb
@@ -1,7 +1,6 @@
require 'spec_helper'
feature 'New directory creation', feature: true, js: true do
- include WaitForAjax
include TargetBranchHelpers
given(:user) { create(:user) }
diff --git a/spec/features/projects/view_on_env_spec.rb b/spec/features/projects/view_on_env_spec.rb
index 34c6a10950f..b7a41ca54e6 100644
--- a/spec/features/projects/view_on_env_spec.rb
+++ b/spec/features/projects/view_on_env_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
describe 'View on environment', js: true do
- include WaitForAjax
-
let(:branch_name) { 'feature' }
let(:file_path) { 'files/ruby/feature.rb' }
let(:project) { create(:project, :repository) }
diff --git a/spec/features/projects/wiki/markdown_preview_spec.rb b/spec/features/projects/wiki/markdown_preview_spec.rb
index 43d8b45669e..49d7ef09e64 100644
--- a/spec/features/projects/wiki/markdown_preview_spec.rb
+++ b/spec/features/projects/wiki/markdown_preview_spec.rb
@@ -17,14 +17,14 @@ feature 'Projects > Wiki > User previews markdown changes', feature: true, js: t
login_as(user)
visit namespace_project_path(project.namespace, project)
- click_link 'Wiki'
+ find('.shortcuts-wiki').trigger('click')
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'
+ find('.add-new-wiki').trigger('click')
page.within '#modal-new-wiki' do
fill_in :new_wiki_path, with: 'a/b/c/d'
click_button 'Create page'
@@ -73,7 +73,7 @@ feature 'Projects > Wiki > User previews markdown changes', feature: true, js: t
fill_in :new_wiki_path, with: 'a-page/b-page/c-page/d-page'
click_button 'Create page'
end
-
+
page.within '.wiki-form' do
fill_in :wiki_content, with: wiki_content
click_on "Preview"
@@ -91,7 +91,7 @@ feature 'Projects > Wiki > User previews markdown changes', feature: true, js: t
context "while editing a wiki page" do
def create_wiki_page(path)
- click_link 'New page'
+ find('.add-new-wiki').trigger('click')
page.within '#modal-new-wiki' do
fill_in :new_wiki_path, with: path
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 1ffac8cd542..5c502ce4fb5 100644
--- a/spec/features/projects/wiki/user_creates_wiki_page_spec.rb
+++ b/spec/features/projects/wiki/user_creates_wiki_page_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Projects > Wiki > User creates wiki page', feature: true do
+feature 'Projects > Wiki > User creates wiki page', js: true, feature: true do
let(:user) { create(:user) }
background do
@@ -8,7 +8,7 @@ feature 'Projects > Wiki > User creates wiki page', feature: true do
login_as(user)
visit namespace_project_path(project.namespace, project)
- click_link 'Wiki'
+ find('.shortcuts-wiki').trigger('click')
end
context 'in the user namespace' do
diff --git a/spec/features/projects_spec.rb b/spec/features/projects_spec.rb
index ba56030e28d..060e19596ae 100644
--- a/spec/features/projects_spec.rb
+++ b/spec/features/projects_spec.rb
@@ -2,7 +2,7 @@ require 'spec_helper'
feature 'Project', feature: true do
describe 'description' do
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
let(:path) { namespace_project_path(project.namespace, project) }
before do
@@ -36,7 +36,7 @@ feature 'Project', feature: true do
describe 'remove forked relationship', js: true do
let(:user) { create(:user) }
- let(:project) { create(:project, namespace: user.namespace) }
+ let(:project) { create(:empty_project, namespace: user.namespace) }
before do
login_with user
@@ -57,7 +57,7 @@ feature 'Project', feature: true do
describe 'removal', js: true do
let(:user) { create(:user, username: 'test', name: 'test') }
- let(:project) { create(:project, namespace: user.namespace, name: 'project1') }
+ let(:project) { create(:empty_project, namespace: user.namespace, name: 'project1') }
before do
login_with(user)
@@ -75,10 +75,8 @@ feature 'Project', feature: true do
end
describe 'project title' do
- include WaitForAjax
-
let(:user) { create(:user) }
- let(:project) { create(:project, namespace: user.namespace) }
+ let(:project) { create(:empty_project, namespace: user.namespace) }
before do
login_with(user)
@@ -94,8 +92,8 @@ feature 'Project', feature: true do
describe 'project title' do
let(:user) { create(:user) }
- let(:project) { create(:project, namespace: user.namespace) }
- let(:project2) { create(:project, namespace: user.namespace, path: 'test') }
+ let(:project) { create(:empty_project, namespace: user.namespace) }
+ let(:project2) { create(:empty_project, namespace: user.namespace, path: 'test') }
let(:issue) { create(:issue, project: project) }
context 'on issues page', js: true do
diff --git a/spec/features/protected_branches/access_control_ce_spec.rb b/spec/features/protected_branches/access_control_ce_spec.rb
index eb3cea775da..7fda4ade665 100644
--- a/spec/features/protected_branches/access_control_ce_spec.rb
+++ b/spec/features/protected_branches/access_control_ce_spec.rb
@@ -9,7 +9,7 @@ RSpec.shared_examples "protected branches > access control > CE" do
allowed_to_push_button = find(".js-allowed-to-push")
unless allowed_to_push_button.text == access_type_name
- allowed_to_push_button.click
+ allowed_to_push_button.trigger('click')
within(".dropdown.open .dropdown-menu") { click_on access_type_name }
end
end
@@ -31,7 +31,7 @@ RSpec.shared_examples "protected branches > access control > CE" do
within(".protected-branches-list") do
find(".js-allowed-to-push").click
-
+
within('.js-allowed-to-push-container') do
expect(first("li")).to have_content("Roles")
click_on access_type_name
diff --git a/spec/features/protected_branches_spec.rb b/spec/features/protected_branches_spec.rb
index 1a3f7b970f6..fc9b293c393 100644
--- a/spec/features/protected_branches_spec.rb
+++ b/spec/features/protected_branches_spec.rb
@@ -2,15 +2,13 @@ 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) }
+ let(:project) { create(:project, :repository) }
before { login_as(user) }
def set_protected_branch_name(branch_name)
- find(".js-protected-branch-select").click
+ find(".js-protected-branch-select").trigger('click')
find(".dropdown-input-field").set(branch_name)
click_on("Create wildcard #{branch_name}")
end
diff --git a/spec/features/protected_tags/access_control_ce_spec.rb b/spec/features/protected_tags/access_control_ce_spec.rb
index 5b2baf8616c..12622cd548a 100644
--- a/spec/features/protected_tags/access_control_ce_spec.rb
+++ b/spec/features/protected_tags/access_control_ce_spec.rb
@@ -9,8 +9,9 @@ RSpec.shared_examples "protected tags > access control > CE" do
allowed_to_create_button = find(".js-allowed-to-create")
unless allowed_to_create_button.text == access_type_name
- allowed_to_create_button.click
- within(".dropdown.open .dropdown-menu") { click_on access_type_name }
+ allowed_to_create_button.trigger('click')
+ find('.create_access_levels-container .dropdown-menu li', match: :first)
+ within('.create_access_levels-container .dropdown-menu') { click_on access_type_name }
end
end
diff --git a/spec/features/protected_tags_spec.rb b/spec/features/protected_tags_spec.rb
index 09e8c850de3..e68448467b0 100644
--- a/spec/features/protected_tags_spec.rb
+++ b/spec/features/protected_tags_spec.rb
@@ -2,10 +2,8 @@ require 'spec_helper'
Dir["./spec/features/protected_tags/*.rb"].sort.each { |f| require f }
feature 'Projected Tags', feature: true, js: true do
- include WaitForAjax
-
let(:user) { create(:user, :admin) }
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
before { login_as(user) }
@@ -13,6 +11,7 @@ feature 'Projected Tags', feature: true, js: true do
find(".js-protected-tag-select").click
find(".dropdown-input-field").set(tag_name)
click_on("Create wildcard #{tag_name}")
+ find('.protected-tags-dropdown .dropdown-menu', visible: false)
end
describe "explicit protected tags" do
diff --git a/spec/features/raven_js_spec.rb b/spec/features/raven_js_spec.rb
new file mode 100644
index 00000000000..e8fa49c18cb
--- /dev/null
+++ b/spec/features/raven_js_spec.rb
@@ -0,0 +1,23 @@
+require 'spec_helper'
+
+feature 'RavenJS', :feature, :js do
+ let(:raven_path) { '/raven.bundle.js' }
+
+ it 'should not load raven if sentry is disabled' do
+ visit new_user_session_path
+
+ expect(has_requested_raven).to eq(false)
+ end
+
+ it 'should load raven if sentry is enabled' do
+ stub_application_setting(clientside_sentry_dsn: 'https://key@domain.com/id', clientside_sentry_enabled: true)
+
+ visit new_user_session_path
+
+ expect(has_requested_raven).to eq(true)
+ end
+
+ def has_requested_raven
+ page.driver.network_traffic.one? {|request| request.url.end_with?(raven_path)}
+ end
+end
diff --git a/spec/features/search_spec.rb b/spec/features/search_spec.rb
index e8ad28a00f0..2fda7758407 100644
--- a/spec/features/search_spec.rb
+++ b/spec/features/search_spec.rb
@@ -2,11 +2,10 @@ require 'spec_helper'
describe "Search", feature: true do
include FilteredSearchHelpers
- include WaitForAjax
let(:user) { create(:user) }
- let(:project) { create(:project, namespace: user.namespace) }
- let!(:issue) { create(:issue, project: project, assignee: user) }
+ let(:project) { create(:empty_project, namespace: user.namespace) }
+ let!(:issue) { create(:issue, project: project, assignees: [user]) }
let!(:issue2) { create(:issue, project: project, author: user) }
before do
@@ -21,13 +20,14 @@ describe "Search", feature: true do
context 'search filters', js: true do
let(:group) { create(:group) }
+ let!(:group_project) { create(:empty_project, group: group) }
before do
group.add_owner(user)
end
it 'shows group name after filtering' do
- find('.js-search-group-dropdown').click
+ find('.js-search-group-dropdown').trigger('click')
wait_for_ajax
page.within '.search-holder' do
@@ -37,9 +37,27 @@ describe "Search", feature: true do
expect(find('.js-search-group-dropdown')).to have_content(group.name)
end
+ it 'filters by group projects after filtering by group' do
+ find('.js-search-group-dropdown').trigger('click')
+ wait_for_ajax
+
+ page.within '.search-holder' do
+ click_link group.name
+ end
+
+ expect(find('.js-search-group-dropdown')).to have_content(group.name)
+
+ page.within('.project-filter') do
+ find('.js-search-project-dropdown').trigger('click')
+ wait_for_ajax
+
+ expect(page).to have_link(group_project.name_with_namespace)
+ end
+ end
+
it 'shows project name after filtering' do
page.within('.project-filter') do
- find('.js-search-project-dropdown').click
+ find('.js-search-project-dropdown').trigger('click')
wait_for_ajax
click_link project.name_with_namespace
@@ -62,6 +80,7 @@ describe "Search", feature: true do
context 'search for comments' do
context 'when comment belongs to a invalid commit' do
+ let(:project) { create(:project, :repository) }
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) }
@@ -103,6 +122,7 @@ describe "Search", feature: true do
end
it 'finds a commit' do
+ project = create(:project, :repository) { |p| p.add_reporter(user) }
visit namespace_project_path(project.namespace, project)
page.within '.search' do
@@ -116,6 +136,7 @@ describe "Search", feature: true do
end
it 'finds a code' do
+ project = create(:project, :repository) { |p| p.add_reporter(user) }
visit namespace_project_path(project.namespace, project)
page.within '.search' do
@@ -222,6 +243,8 @@ describe "Search", feature: true do
end
describe 'search for commits' do
+ let(:project) { create(:project, :repository) }
+
before do
visit search_path(project_id: project.id)
end
diff --git a/spec/features/security/project/internal_access_spec.rb b/spec/features/security/project/internal_access_spec.rb
index 6ecdc8cbb71..78a76d9c112 100644
--- a/spec/features/security/project/internal_access_spec.rb
+++ b/spec/features/security/project/internal_access_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
describe "Internal Project Access", feature: true do
include AccessMatchers
- let(:project) { create(:project, :internal) }
+ set(:project) { create(:project, :internal) }
describe "Project should be internal" do
describe '#internal?' do
@@ -399,6 +399,58 @@ describe "Internal Project Access", feature: true do
end
end
+ describe 'GET /:project_path/builds/:id/trace' do
+ let(:pipeline) { create(:ci_pipeline, project: project) }
+ let(:build) { create(:ci_build, pipeline: pipeline) }
+ subject { trace_namespace_project_build_path(project.namespace, project, build.id) }
+
+ context 'when allowed for public and internal' do
+ before do
+ project.update(public_builds: true)
+ end
+
+ it { is_expected.to be_allowed_for(:admin) }
+ it { is_expected.to be_allowed_for(:owner).of(project) }
+ it { is_expected.to be_allowed_for(:master).of(project) }
+ it { is_expected.to be_allowed_for(:developer).of(project) }
+ it { is_expected.to be_allowed_for(:reporter).of(project) }
+ it { is_expected.to be_allowed_for(:guest).of(project) }
+ 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
+
+ context 'when disallowed for public and internal' do
+ before do
+ project.update(public_builds: false)
+ end
+
+ it { is_expected.to be_allowed_for(:admin) }
+ it { is_expected.to be_allowed_for(:owner).of(project) }
+ it { is_expected.to be_allowed_for(:master).of(project) }
+ it { is_expected.to be_allowed_for(:developer).of(project) }
+ it { is_expected.to be_allowed_for(:reporter).of(project) }
+ it { is_expected.to be_denied_for(:guest).of(project) }
+ 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
+
+ describe "GET /:project_path/pipeline_schedules" do
+ subject { namespace_project_pipeline_schedules_path(project.namespace, project) }
+
+ it { is_expected.to be_allowed_for(:admin) }
+ it { is_expected.to be_allowed_for(:owner).of(project) }
+ it { is_expected.to be_allowed_for(:master).of(project) }
+ it { is_expected.to be_allowed_for(:developer).of(project) }
+ it { is_expected.to be_allowed_for(:reporter).of(project) }
+ it { is_expected.to be_allowed_for(:guest).of(project) }
+ 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
+
describe "GET /:project_path/environments" do
subject { namespace_project_environments_path(project.namespace, project) }
@@ -428,6 +480,21 @@ describe "Internal Project Access", feature: true do
it { is_expected.to be_denied_for(:visitor) }
end
+ describe "GET /:project_path/environments/:id/deployments" do
+ let(:environment) { create(:environment, project: project) }
+ subject { namespace_project_environment_deployments_path(project.namespace, project, environment) }
+
+ it { is_expected.to be_allowed_for(:admin) }
+ it { is_expected.to be_allowed_for(:owner).of(project) }
+ it { is_expected.to be_allowed_for(:master).of(project) }
+ it { is_expected.to be_allowed_for(:developer).of(project) }
+ it { is_expected.to be_allowed_for(:reporter).of(project) }
+ it { is_expected.to be_denied_for(:guest).of(project) }
+ 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
+
describe "GET /:project_path/environments/new" do
subject { new_namespace_project_environment_path(project.namespace, project) }
diff --git a/spec/features/security/project/private_access_spec.rb b/spec/features/security/project/private_access_spec.rb
index a8fc0624588..a66f6e09055 100644
--- a/spec/features/security/project/private_access_spec.rb
+++ b/spec/features/security/project/private_access_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
describe "Private Project Access", feature: true do
include AccessMatchers
- let(:project) { create(:project, :private, public_builds: false) }
+ set(:project) { create(:project, :private, public_builds: false) }
describe "Project should be private" do
describe '#private?' do
@@ -388,6 +388,38 @@ describe "Private Project Access", feature: true do
end
end
+ describe 'GET /:project_path/builds/:id/trace' do
+ let(:pipeline) { create(:ci_pipeline, project: project) }
+ let(:build) { create(:ci_build, pipeline: pipeline) }
+ subject { trace_namespace_project_build_path(project.namespace, project, build.id) }
+
+ it { is_expected.to be_allowed_for(:admin) }
+ it { is_expected.to be_allowed_for(:owner).of(project) }
+ it { is_expected.to be_allowed_for(:master).of(project) }
+ it { is_expected.to be_allowed_for(:developer).of(project) }
+ it { is_expected.to be_allowed_for(:reporter).of(project) }
+ it { is_expected.to be_denied_for(:guest).of(project) }
+ it { is_expected.to be_denied_for(:user) }
+ it { is_expected.to be_denied_for(:external) }
+ it { is_expected.to be_denied_for(:visitor) }
+
+ context 'when public builds is enabled' do
+ before do
+ project.update(public_builds: true)
+ end
+
+ it { is_expected.to be_allowed_for(:guest).of(project) }
+ end
+
+ context 'when public builds is disabled' do
+ before do
+ project.update(public_builds: false)
+ end
+
+ it { is_expected.to be_denied_for(:guest).of(project) }
+ end
+ end
+
describe "GET /:project_path/environments" do
subject { namespace_project_environments_path(project.namespace, project) }
@@ -417,6 +449,21 @@ describe "Private Project Access", feature: true do
it { is_expected.to be_denied_for(:visitor) }
end
+ describe "GET /:project_path/environments/:id/deployments" do
+ let(:environment) { create(:environment, project: project) }
+ subject { namespace_project_environment_deployments_path(project.namespace, project, environment) }
+
+ it { is_expected.to be_allowed_for(:admin) }
+ it { is_expected.to be_allowed_for(:owner).of(project) }
+ it { is_expected.to be_allowed_for(:master).of(project) }
+ it { is_expected.to be_allowed_for(:developer).of(project) }
+ it { is_expected.to be_allowed_for(:reporter).of(project) }
+ it { is_expected.to be_denied_for(:guest).of(project) }
+ 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
+
describe "GET /:project_path/environments/new" do
subject { new_namespace_project_environment_path(project.namespace, project) }
@@ -431,6 +478,48 @@ describe "Private Project Access", feature: true do
it { is_expected.to be_denied_for(:visitor) }
end
+ describe "GET /:project_path/pipeline_schedules" do
+ subject { namespace_project_pipeline_schedules_path(project.namespace, project) }
+
+ it { is_expected.to be_allowed_for(:admin) }
+ it { is_expected.to be_allowed_for(:owner).of(project) }
+ it { is_expected.to be_allowed_for(:master).of(project) }
+ it { is_expected.to be_allowed_for(:developer).of(project) }
+ it { is_expected.to be_allowed_for(:reporter).of(project) }
+ it { is_expected.to be_denied_for(:guest).of(project) }
+ 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
+
+ describe "GET /:project_path/pipeline_schedules/new" do
+ subject { new_namespace_project_pipeline_schedule_path(project.namespace, project) }
+
+ it { is_expected.to be_allowed_for(:admin) }
+ it { is_expected.to be_allowed_for(:owner).of(project) }
+ it { is_expected.to be_allowed_for(:master).of(project) }
+ it { is_expected.to be_allowed_for(:developer).of(project) }
+ it { is_expected.to be_denied_for(:reporter).of(project) }
+ it { is_expected.to be_denied_for(:guest).of(project) }
+ 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
+
+ describe "GET /:project_path/environments/new" do
+ subject { new_namespace_project_pipeline_schedule_path(project.namespace, project) }
+
+ it { is_expected.to be_allowed_for(:admin) }
+ it { is_expected.to be_allowed_for(:owner).of(project) }
+ it { is_expected.to be_allowed_for(:master).of(project) }
+ it { is_expected.to be_allowed_for(:developer).of(project) }
+ it { is_expected.to be_denied_for(:reporter).of(project) }
+ it { is_expected.to be_denied_for(:guest).of(project) }
+ 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
+
describe "GET /:project_path/container_registry" do
let(:container_repository) { create(:container_repository) }
diff --git a/spec/features/security/project/public_access_spec.rb b/spec/features/security/project/public_access_spec.rb
index c4d2f50ca14..5cd575500c3 100644
--- a/spec/features/security/project/public_access_spec.rb
+++ b/spec/features/security/project/public_access_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
describe "Public Project Access", feature: true do
include AccessMatchers
- let(:project) { create(:project, :public) }
+ set(:project) { create(:project, :public) }
describe "Project should be public" do
describe '#public?' do
@@ -219,6 +219,58 @@ describe "Public Project Access", feature: true do
end
end
+ describe 'GET /:project_path/builds/:id/trace' do
+ let(:pipeline) { create(:ci_pipeline, project: project) }
+ let(:build) { create(:ci_build, pipeline: pipeline) }
+ subject { trace_namespace_project_build_path(project.namespace, project, build.id) }
+
+ context 'when allowed for public' do
+ before do
+ project.update(public_builds: true)
+ end
+
+ it { is_expected.to be_allowed_for(:admin) }
+ it { is_expected.to be_allowed_for(:owner).of(project) }
+ it { is_expected.to be_allowed_for(:master).of(project) }
+ it { is_expected.to be_allowed_for(:developer).of(project) }
+ it { is_expected.to be_allowed_for(:reporter).of(project) }
+ it { is_expected.to be_allowed_for(:guest).of(project) }
+ 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
+
+ context 'when disallowed for public' do
+ before do
+ project.update(public_builds: false)
+ end
+
+ it { is_expected.to be_allowed_for(:admin) }
+ it { is_expected.to be_allowed_for(:owner).of(project) }
+ it { is_expected.to be_allowed_for(:master).of(project) }
+ it { is_expected.to be_allowed_for(:developer).of(project) }
+ it { is_expected.to be_allowed_for(:reporter).of(project) }
+ it { is_expected.to be_denied_for(:guest).of(project) }
+ 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
+
+ describe "GET /:project_path/pipeline_schedules" do
+ subject { namespace_project_pipeline_schedules_path(project.namespace, project) }
+
+ it { is_expected.to be_allowed_for(:admin) }
+ it { is_expected.to be_allowed_for(:owner).of(project) }
+ it { is_expected.to be_allowed_for(:master).of(project) }
+ it { is_expected.to be_allowed_for(:developer).of(project) }
+ it { is_expected.to be_allowed_for(:reporter).of(project) }
+ it { is_expected.to be_allowed_for(:guest).of(project) }
+ 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
+
describe "GET /:project_path/environments" do
subject { namespace_project_environments_path(project.namespace, project) }
@@ -248,6 +300,21 @@ describe "Public Project Access", feature: true do
it { is_expected.to be_denied_for(:visitor) }
end
+ describe "GET /:project_path/environments/:id/deployments" do
+ let(:environment) { create(:environment, project: project) }
+ subject { namespace_project_environment_deployments_path(project.namespace, project, environment) }
+
+ it { is_expected.to be_allowed_for(:admin) }
+ it { is_expected.to be_allowed_for(:owner).of(project) }
+ it { is_expected.to be_allowed_for(:master).of(project) }
+ it { is_expected.to be_allowed_for(:developer).of(project) }
+ it { is_expected.to be_allowed_for(:reporter).of(project) }
+ it { is_expected.to be_denied_for(:guest).of(project) }
+ 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
+
describe "GET /:project_path/environments/new" do
subject { new_namespace_project_environment_path(project.namespace, project) }
diff --git a/spec/features/signup_spec.rb b/spec/features/signup_spec.rb
index 9fde8d6e5cf..d7b6dda4946 100644
--- a/spec/features/signup_spec.rb
+++ b/spec/features/signup_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
feature 'Signup', feature: true do
describe 'signup with no errors' do
context "when sending confirmation email" do
- before { allow_any_instance_of(ApplicationSetting).to receive(:send_user_confirmation_email).and_return(true) }
+ before { stub_application_setting(send_user_confirmation_email: true) }
it 'creates the user account and sends a confirmation email' do
user = build(:user)
@@ -23,7 +23,7 @@ feature 'Signup', feature: true do
end
context "when not sending confirmation email" do
- before { allow_any_instance_of(ApplicationSetting).to receive(:send_user_confirmation_email).and_return(false) }
+ before { stub_application_setting(send_user_confirmation_email: false) }
it 'creates the user account and goes to dashboard' do
user = build(:user)
diff --git a/spec/features/snippets/create_snippet_spec.rb b/spec/features/snippets/create_snippet_spec.rb
index 5470276bf06..9409c323288 100644
--- a/spec/features/snippets/create_snippet_spec.rb
+++ b/spec/features/snippets/create_snippet_spec.rb
@@ -1,6 +1,6 @@
require 'rails_helper'
-feature 'Create Snippet', feature: true do
+feature 'Create Snippet', :js, feature: true do
before do
login_as :user
visit new_snippet_path
@@ -9,10 +9,11 @@ feature 'Create Snippet', feature: true do
scenario 'Authenticated user creates a snippet' do
fill_in 'personal_snippet_title', with: 'My Snippet Title'
page.within('.file-editor') do
- find(:xpath, "//input[@id='personal_snippet_content']").set 'Hello World!'
+ find('.ace_editor').native.send_keys 'Hello World!'
end
click_button 'Create snippet'
+ wait_for_ajax
expect(page).to have_content('My Snippet Title')
expect(page).to have_content('Hello World!')
@@ -22,10 +23,11 @@ feature 'Create Snippet', feature: true do
fill_in 'personal_snippet_title', with: 'My Snippet Title'
page.within('.file-editor') do
find(:xpath, "//input[@id='personal_snippet_file_name']").set 'snippet+file+name'
- find(:xpath, "//input[@id='personal_snippet_content']").set 'Hello World!'
+ find('.ace_editor').native.send_keys 'Hello World!'
end
click_button 'Create snippet'
+ wait_for_ajax
expect(page).to have_content('My Snippet Title')
expect(page).to have_content('snippet+file+name')
diff --git a/spec/features/snippets/explore_spec.rb b/spec/features/snippets/explore_spec.rb
index 10a4597e467..fd097fe2e74 100644
--- a/spec/features/snippets/explore_spec.rb
+++ b/spec/features/snippets/explore_spec.rb
@@ -1,11 +1,11 @@
require 'rails_helper'
feature 'Explore Snippets', feature: true do
- scenario 'User should see snippets that are not private' do
- public_snippet = create(:personal_snippet, :public)
- internal_snippet = create(:personal_snippet, :internal)
- private_snippet = create(:personal_snippet, :private)
+ let!(:public_snippet) { create(:personal_snippet, :public) }
+ let!(:internal_snippet) { create(:personal_snippet, :internal) }
+ let!(:private_snippet) { create(:personal_snippet, :private) }
+ scenario 'User should see snippets that are not private' do
login_as create(:user)
visit explore_snippets_path
@@ -13,4 +13,21 @@ feature 'Explore Snippets', feature: true do
expect(page).to have_content(internal_snippet.title)
expect(page).not_to have_content(private_snippet.title)
end
+
+ scenario 'External user should see only public snippets' do
+ login_as create(:user, :external)
+ visit explore_snippets_path
+
+ expect(page).to have_content(public_snippet.title)
+ expect(page).not_to have_content(internal_snippet.title)
+ expect(page).not_to have_content(private_snippet.title)
+ end
+
+ scenario 'Not authenticated user should see only public snippets' do
+ visit explore_snippets_path
+
+ expect(page).to have_content(public_snippet.title)
+ expect(page).not_to have_content(internal_snippet.title)
+ expect(page).not_to have_content(private_snippet.title)
+ end
end
diff --git a/spec/features/snippets/internal_snippet_spec.rb b/spec/features/snippets/internal_snippet_spec.rb
new file mode 100644
index 00000000000..93382f4c359
--- /dev/null
+++ b/spec/features/snippets/internal_snippet_spec.rb
@@ -0,0 +1,23 @@
+require 'rails_helper'
+
+feature 'Internal Snippets', feature: true, js: true do
+ let(:internal_snippet) { create(:personal_snippet, :internal) }
+
+ describe 'normal user' do
+ before do
+ login_as :user
+ end
+
+ scenario 'sees internal snippets' do
+ visit snippet_path(internal_snippet)
+
+ expect(page).to have_content(internal_snippet.content)
+ end
+
+ scenario 'sees raw internal snippets' do
+ visit raw_snippet_path(internal_snippet)
+
+ expect(page).to have_content(internal_snippet.content)
+ end
+ end
+end
diff --git a/spec/features/snippets/notes_on_personal_snippets_spec.rb b/spec/features/snippets/notes_on_personal_snippets_spec.rb
new file mode 100644
index 00000000000..698eb46573f
--- /dev/null
+++ b/spec/features/snippets/notes_on_personal_snippets_spec.rb
@@ -0,0 +1,101 @@
+require 'spec_helper'
+
+describe 'Comments on personal snippets', :js, feature: true do
+ let!(:user) { create(:user) }
+ let!(:snippet) { create(:personal_snippet, :public) }
+ let!(:snippet_notes) do
+ [
+ create(:note_on_personal_snippet, noteable: snippet, author: user),
+ create(:note_on_personal_snippet, noteable: snippet)
+ ]
+ end
+ let!(:other_note) { create(:note_on_personal_snippet) }
+
+ before do
+ login_as user
+ visit snippet_path(snippet)
+ end
+
+ subject { page }
+
+ context 'when viewing the snippet detail page' do
+ it 'contains notes for a snippet with correct action icons' do
+ expect(page).to have_selector('#notes-list li', count: 2)
+
+ # comment authored by current user
+ page.within("#notes-list li#note_#{snippet_notes[0].id}") do
+ expect(page).to have_content(snippet_notes[0].note)
+ expect(page).to have_selector('.js-note-delete')
+ expect(page).to have_selector('.note-emoji-button')
+ end
+
+ page.within("#notes-list li#note_#{snippet_notes[1].id}") do
+ expect(page).to have_content(snippet_notes[1].note)
+ expect(page).not_to have_selector('.js-note-delete')
+ expect(page).to have_selector('.note-emoji-button')
+ end
+ end
+ end
+
+ context 'when submitting a note' do
+ it 'shows a valid form' do
+ is_expected.to have_css('.js-main-target-form', visible: true, count: 1)
+ expect(find('.js-main-target-form .js-comment-button').value).
+ to eq('Comment')
+
+ page.within('.js-main-target-form') do
+ expect(page).not_to have_link('Cancel')
+ end
+ end
+
+ it 'previews a note' do
+ fill_in 'note[note]', with: 'This is **awesome**!'
+ find('.js-md-preview-button').click
+
+ page.within('.new-note .md-preview') do
+ expect(page).to have_content('This is awesome!')
+ expect(page).to have_selector('strong')
+ end
+ end
+
+ it 'creates a note' do
+ fill_in 'note[note]', with: 'This is **awesome**!'
+ click_button 'Comment'
+
+ expect(find('div#notes')).to have_content('This is awesome!')
+ end
+ end
+
+ context 'when editing a note' do
+ it 'changes the text' do
+ page.within("#notes-list li#note_#{snippet_notes[0].id}") do
+ click_on 'Edit comment'
+ end
+
+ page.within('.current-note-edit-form') do
+ fill_in 'note[note]', with: 'new content'
+ find('.btn-save').click
+ end
+
+ page.within("#notes-list li#note_#{snippet_notes[0].id}") do
+ edited_text = find('.edited-text')
+
+ expect(page).to have_css('.note_edited_ago')
+ expect(page).to have_content('new content')
+ expect(edited_text).to have_selector('.note_edited_ago')
+ end
+ end
+ end
+
+ context 'when deleting a note' do
+ it 'removes the note from the snippet detail page' do
+ page.within("#notes-list li#note_#{snippet_notes[0].id}") do
+ click_on 'Remove comment'
+ end
+
+ wait_for_ajax
+
+ expect(page).not_to have_selector("#notes-list li#note_#{snippet_notes[0].id}")
+ end
+ end
+end
diff --git a/spec/features/snippets/public_snippets_spec.rb b/spec/features/snippets/public_snippets_spec.rb
index 34300ccb940..2df483818c3 100644
--- a/spec/features/snippets/public_snippets_spec.rb
+++ b/spec/features/snippets/public_snippets_spec.rb
@@ -1,10 +1,11 @@
require 'rails_helper'
-feature 'Public Snippets', feature: true do
+feature 'Public Snippets', :js, feature: true do
scenario 'Unauthenticated user should see public snippets' do
public_snippet = create(:personal_snippet, :public)
visit snippet_path(public_snippet)
+ wait_for_ajax
expect(page).to have_content(public_snippet.content)
end
diff --git a/spec/features/snippets/show_spec.rb b/spec/features/snippets/show_spec.rb
new file mode 100644
index 00000000000..e36cf547f80
--- /dev/null
+++ b/spec/features/snippets/show_spec.rb
@@ -0,0 +1,138 @@
+require 'spec_helper'
+
+feature 'Snippet', :js, feature: true do
+ let(:project) { create(:project, :repository) }
+ let(:snippet) { create(:personal_snippet, :public, file_name: file_name, content: content) }
+
+ context 'Ruby file' do
+ let(:file_name) { 'popen.rb' }
+ let(:content) { project.repository.blob_at('master', 'files/ruby/popen.rb').data }
+
+ before do
+ visit snippet_path(snippet)
+
+ wait_for_ajax
+ end
+
+ it 'displays the blob' do
+ aggregate_failures do
+ # shows highlighted Ruby code
+ expect(page).to have_content("require 'fileutils'")
+
+ # does not show a viewer switcher
+ expect(page).not_to have_selector('.js-blob-viewer-switcher')
+
+ # shows an enabled copy button
+ expect(page).to have_selector('.js-copy-blob-source-btn:not(.disabled)')
+
+ # shows a raw button
+ expect(page).to have_link('Open raw')
+
+ # shows a download button
+ expect(page).to have_link('Download')
+ end
+ end
+ end
+
+ context 'Markdown file' do
+ let(:file_name) { 'ruby-style-guide.md' }
+ let(:content) { project.repository.blob_at('master', 'files/markdown/ruby-style-guide.md').data }
+
+ context 'visiting directly' do
+ before do
+ visit snippet_path(snippet)
+
+ wait_for_ajax
+ end
+
+ it 'displays the blob using the rich viewer' do
+ aggregate_failures do
+ # hides the simple viewer
+ expect(page).to have_selector('.blob-viewer[data-type="simple"]', visible: false)
+ expect(page).to have_selector('.blob-viewer[data-type="rich"]')
+
+ # shows rendered Markdown
+ expect(page).to have_link("PEP-8")
+
+ # shows a viewer switcher
+ expect(page).to have_selector('.js-blob-viewer-switcher')
+
+ # shows a disabled copy button
+ expect(page).to have_selector('.js-copy-blob-source-btn.disabled')
+
+ # shows a raw button
+ expect(page).to have_link('Open raw')
+
+ # shows a download button
+ expect(page).to have_link('Download')
+ end
+ end
+
+ context 'switching to the simple viewer' do
+ before do
+ find('.js-blob-viewer-switch-btn[data-viewer=simple]').click
+
+ wait_for_ajax
+ end
+
+ it 'displays the blob using the simple viewer' do
+ aggregate_failures do
+ # hides the rich viewer
+ expect(page).to have_selector('.blob-viewer[data-type="simple"]')
+ expect(page).to have_selector('.blob-viewer[data-type="rich"]', visible: false)
+
+ # shows highlighted Markdown code
+ expect(page).to have_content("[PEP-8](http://www.python.org/dev/peps/pep-0008/)")
+
+ # shows an enabled copy button
+ expect(page).to have_selector('.js-copy-blob-source-btn:not(.disabled)')
+ end
+ end
+
+ context 'switching to the rich viewer again' do
+ before do
+ find('.js-blob-viewer-switch-btn[data-viewer=rich]').click
+
+ wait_for_ajax
+ end
+
+ it 'displays the blob using the rich viewer' do
+ aggregate_failures do
+ # hides the simple viewer
+ expect(page).to have_selector('.blob-viewer[data-type="simple"]', visible: false)
+ expect(page).to have_selector('.blob-viewer[data-type="rich"]')
+
+ # shows an enabled copy button
+ expect(page).to have_selector('.js-copy-blob-source-btn:not(.disabled)')
+ end
+ end
+ end
+ end
+ end
+
+ context 'visiting with a line number anchor' do
+ before do
+ visit snippet_path(snippet, anchor: 'L1')
+
+ wait_for_ajax
+ end
+
+ it 'displays the blob using the simple viewer' do
+ aggregate_failures do
+ # hides the rich viewer
+ expect(page).to have_selector('.blob-viewer[data-type="simple"]')
+ expect(page).to have_selector('.blob-viewer[data-type="rich"]', visible: false)
+
+ # highlights the line in question
+ expect(page).to have_selector('#LC1.hll')
+
+ # shows highlighted Markdown code
+ expect(page).to have_content("[PEP-8](http://www.python.org/dev/peps/pep-0008/)")
+
+ # shows an enabled copy button
+ expect(page).to have_selector('.js-copy-blob-source-btn:not(.disabled)')
+ end
+ end
+ end
+ end
+end
diff --git a/spec/features/task_lists_spec.rb b/spec/features/task_lists_spec.rb
index a5d14aa19f1..8bd13caf2b0 100644
--- a/spec/features/task_lists_spec.rb
+++ b/spec/features/task_lists_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
feature 'Task Lists', feature: true do
include Warden::Test::Helpers
- let(:project) { create(:project) }
+ let(:project) { create(:empty_project) }
let(:user) { create(:user) }
let(:user2) { create(:user) }
@@ -62,12 +62,15 @@ feature 'Task Lists', feature: true do
visit namespace_project_issue_path(project.namespace, project, issue)
end
- describe 'for Issues' do
- describe 'multiple tasks' do
+ describe 'for Issues', feature: true do
+ describe 'multiple tasks', js: true do
+ include WaitForVueResource
+
let!(:issue) { create(:issue, description: markdown, author: user, project: project) }
it 'renders' do
visit_issue(project, issue)
+ wait_for_vue_resource
expect(page).to have_selector('ul.task-list', count: 1)
expect(page).to have_selector('li.task-list-item', count: 6)
@@ -76,25 +79,24 @@ feature 'Task Lists', feature: true do
it 'contains the required selectors' do
visit_issue(project, issue)
+ wait_for_vue_resource
- container = '.detail-page-description .description.js-task-list-container'
-
- expect(page).to have_selector(container)
- expect(page).to have_selector("#{container} .wiki .task-list .task-list-item .task-list-item-checkbox")
- expect(page).to have_selector("#{container} .js-task-list-field")
- expect(page).to have_selector('form.js-issuable-update')
+ expect(page).to have_selector(".wiki .task-list .task-list-item .task-list-item-checkbox")
expect(page).to have_selector('a.btn-close')
end
it 'is only editable by author' do
visit_issue(project, issue)
- expect(page).to have_selector('.js-task-list-container')
+ wait_for_vue_resource
- logout(:user)
+ expect(page).to have_selector(".wiki .task-list .task-list-item .task-list-item-checkbox")
+ logout(:user)
login_as(user2)
visit current_path
- expect(page).not_to have_selector('.js-task-list-container')
+ wait_for_vue_resource
+
+ expect(page).to have_selector(".wiki .task-list .task-list-item .task-list-item-checkbox")
end
it 'provides a summary on Issues#index' do
@@ -103,11 +105,14 @@ feature 'Task Lists', feature: true do
end
end
- describe 'single incomplete task' do
+ describe 'single incomplete task', js: true do
+ include WaitForVueResource
+
let!(:issue) { create(:issue, description: singleIncompleteMarkdown, author: user, project: project) }
it 'renders' do
visit_issue(project, issue)
+ wait_for_vue_resource
expect(page).to have_selector('ul.task-list', count: 1)
expect(page).to have_selector('li.task-list-item', count: 1)
@@ -116,15 +121,18 @@ feature 'Task Lists', feature: true do
it 'provides a summary on Issues#index' do
visit namespace_project_issues_path(project.namespace, project)
+
expect(page).to have_content("0 of 1 task completed")
end
end
- describe 'single complete task' do
+ describe 'single complete task', js: true do
+ include WaitForVueResource
let!(:issue) { create(:issue, description: singleCompleteMarkdown, author: user, project: project) }
it 'renders' do
visit_issue(project, issue)
+ wait_for_vue_resource
expect(page).to have_selector('ul.task-list', count: 1)
expect(page).to have_selector('li.task-list-item', count: 1)
@@ -133,6 +141,7 @@ feature 'Task Lists', feature: true do
it 'provides a summary on Issues#index' do
visit namespace_project_issues_path(project.namespace, project)
+
expect(page).to have_content("1 of 1 task completed")
end
end
@@ -240,6 +249,7 @@ feature 'Task Lists', feature: true do
end
describe 'multiple tasks' do
+ let(:project) { create(:project, :repository) }
let!(:merge) { create(:merge_request, :simple, description: markdown, author: user, source_project: project) }
it 'renders for description' do
diff --git a/spec/features/todos/todos_filtering_spec.rb b/spec/features/todos/todos_filtering_spec.rb
index e8f06916d53..f32e70c2c3f 100644
--- a/spec/features/todos/todos_filtering_spec.rb
+++ b/spec/features/todos/todos_filtering_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
describe 'Dashboard > User filters todos', feature: true, js: true do
- include WaitForAjax
-
let(:user_1) { create(:user, username: 'user_1', name: 'user_1') }
let(:user_2) { create(:user, username: 'user_2', name: 'user_2') }
@@ -47,8 +45,8 @@ describe 'Dashboard > User filters todos', feature: true, js: true do
wait_for_ajax
- expect(find('.todos-list')).to have_content user_1.name
- expect(find('.todos-list')).not_to have_content user_2.name
+ expect(find('.todos-list')).to have_content 'merge request'
+ expect(find('.todos-list')).not_to have_content 'issue'
end
it "shows only authors of existing todos" do
diff --git a/spec/features/todos/todos_spec.rb b/spec/features/todos/todos_spec.rb
index c270511c903..55b3e3d9424 100644
--- a/spec/features/todos/todos_spec.rb
+++ b/spec/features/todos/todos_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
describe 'Dashboard Todos', feature: true do
- include WaitForAjax
-
let(:user) { create(:user) }
let(:author) { create(:user) }
let(:project) { create(:project, visibility_level: Gitlab::VisibilityLevel::PUBLIC) }
@@ -101,6 +99,83 @@ describe 'Dashboard Todos', feature: true do
end
end
+ context 'User created todos for themself' do
+ before do
+ login_as(user)
+ end
+
+ context 'issue assigned todo' do
+ before do
+ create(:todo, :assigned, user: user, project: project, target: issue, author: user)
+ visit dashboard_todos_path
+ end
+
+ it 'shows issue assigned to yourself message' do
+ page.within('.js-todos-all') do
+ expect(page).to have_content("You assigned issue #{issue.to_reference(full: true)} to yourself")
+ end
+ end
+ end
+
+ context 'marked todo' do
+ before do
+ create(:todo, :marked, user: user, project: project, target: issue, author: user)
+ visit dashboard_todos_path
+ end
+
+ it 'shows you added a todo message' do
+ page.within('.js-todos-all') do
+ expect(page).to have_content("You added a todo for issue #{issue.to_reference(full: true)}")
+ expect(page).not_to have_content('to yourself')
+ end
+ end
+ end
+
+ context 'mentioned todo' do
+ before do
+ create(:todo, :mentioned, user: user, project: project, target: issue, author: user)
+ visit dashboard_todos_path
+ end
+
+ it 'shows you mentioned yourself message' do
+ page.within('.js-todos-all') do
+ expect(page).to have_content("You mentioned yourself on issue #{issue.to_reference(full: true)}")
+ expect(page).not_to have_content('to yourself')
+ end
+ end
+ end
+
+ context 'directly_addressed todo' do
+ before do
+ create(:todo, :directly_addressed, user: user, project: project, target: issue, author: user)
+ visit dashboard_todos_path
+ end
+
+ it 'shows you directly addressed yourself message' do
+ page.within('.js-todos-all') do
+ expect(page).to have_content("You directly addressed yourself on issue #{issue.to_reference(full: true)}")
+ expect(page).not_to have_content('to yourself')
+ end
+ end
+ end
+
+ context 'approval todo' do
+ let(:merge_request) { create(:merge_request) }
+
+ before do
+ create(:todo, :approval_required, user: user, project: project, target: merge_request, author: user)
+ visit dashboard_todos_path
+ end
+
+ it 'shows you set yourself as an approver message' do
+ page.within('.js-todos-all') do
+ expect(page).to have_content("You set yourself as an approver for merge request #{merge_request.to_reference(full: true)}")
+ expect(page).not_to have_content('to yourself')
+ end
+ end
+ end
+ end
+
context 'User has done todos', js: true do
before do
create(:todo, :mentioned, :done, user: user, project: project, target: issue, author: author)
@@ -176,7 +251,7 @@ describe 'Dashboard Todos', feature: true do
describe 'mark all as done', js: true do
before do
visit dashboard_todos_path
- click_link 'Mark all as done'
+ find('.js-todos-mark-all').trigger('click')
end
it 'shows "All done" message!' do
@@ -233,9 +308,9 @@ describe 'Dashboard Todos', feature: true do
end
def mark_all_and_undo
- click_link 'Mark all as done'
+ find('.js-todos-mark-all').trigger('click')
wait_for_ajax
- click_link 'Undo mark all as done'
+ find('.js-todos-undo-all').trigger('click')
wait_for_ajax
end
end
diff --git a/spec/features/triggers_spec.rb b/spec/features/triggers_spec.rb
index 81fa2de1cc3..c1ae6db00c6 100644
--- a/spec/features/triggers_spec.rb
+++ b/spec/features/triggers_spec.rb
@@ -77,59 +77,6 @@ feature 'Triggers', feature: true, js: true do
expect(page.find('.flash-notice')).to have_content 'Trigger was successfully updated.'
expect(page.find('.triggers-list')).to have_content new_trigger_title
end
-
- context 'scheduled triggers' do
- let!(:trigger) do
- create(:ci_trigger, owner: user, project: @project, description: trigger_title)
- end
-
- context 'enabling schedule' do
- before do
- visit edit_namespace_project_trigger_path(@project.namespace, @project, trigger)
- end
-
- scenario 'do fill form with valid data and save' do
- find('#trigger_trigger_schedule_attributes_active').click
- fill_in 'trigger_trigger_schedule_attributes_cron', with: '1 * * * *'
- fill_in 'trigger_trigger_schedule_attributes_cron_timezone', with: 'UTC'
- fill_in 'trigger_trigger_schedule_attributes_ref', with: 'master'
- click_button 'Save trigger'
-
- expect(page.find('.flash-notice')).to have_content 'Trigger was successfully updated.'
- end
-
- scenario 'do not fill form with valid data and save' do
- find('#trigger_trigger_schedule_attributes_active').click
- click_button 'Save trigger'
-
- expect(page).to have_content 'The form contains the following errors'
- end
- end
-
- context 'disabling schedule' do
- before do
- trigger.create_trigger_schedule(
- project: trigger.project,
- active: true,
- ref: 'master',
- cron: '1 * * * *',
- cron_timezone: 'UTC')
-
- visit edit_namespace_project_trigger_path(@project.namespace, @project, trigger)
- end
-
- scenario 'disable and save form' do
- find('#trigger_trigger_schedule_attributes_active').click
- click_button 'Save trigger'
- expect(page.find('.flash-notice')).to have_content 'Trigger was successfully updated.'
-
- visit edit_namespace_project_trigger_path(@project.namespace, @project, trigger)
- checkbox = find_field('trigger_trigger_schedule_attributes_active')
-
- expect(checkbox).not_to be_checked
- end
- end
- end
end
describe 'trigger "Take ownership" workflow' do
diff --git a/spec/features/u2f_spec.rb b/spec/features/u2f_spec.rb
index c877cfdd978..544d2dcb87f 100644
--- a/spec/features/u2f_spec.rb
+++ b/spec/features/u2f_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', :js do
- include WaitForAjax
-
before { allow_any_instance_of(U2fHelper).to receive(:inject_u2f_api?).and_return(true) }
def manage_two_factor_authentication
diff --git a/spec/features/unsubscribe_links_spec.rb b/spec/features/unsubscribe_links_spec.rb
index e2d9cfdd0b0..a23c4ca2b92 100644
--- a/spec/features/unsubscribe_links_spec.rb
+++ b/spec/features/unsubscribe_links_spec.rb
@@ -6,7 +6,7 @@ describe 'Unsubscribe links', feature: true do
let(:recipient) { create(:user) }
let(:author) { create(:user) }
let(:project) { create(:empty_project, :public) }
- let(:params) { { title: 'A bug!', description: 'Fix it!', assignee: recipient } }
+ let(:params) { { title: 'A bug!', description: 'Fix it!', assignees: [recipient] } }
let(:issue) { Issues::CreateService.new(project, author, params).execute }
let(:mail) { ActionMailer::Base.deliveries.last }
diff --git a/spec/features/user_callout_spec.rb b/spec/features/user_callout_spec.rb
index 848af5e3a4d..b84f834ff1e 100644
--- a/spec/features/user_callout_spec.rb
+++ b/spec/features/user_callout_spec.rb
@@ -20,7 +20,7 @@ describe 'User Callouts', js: true do
visit dashboard_projects_path
within('.user-callout') do
- find('.close').click
+ find('.close').trigger('click')
end
visit dashboard_projects_path
diff --git a/spec/features/users/projects_spec.rb b/spec/features/users/projects_spec.rb
index 1d75fe434b0..373b64808f8 100644
--- a/spec/features/users/projects_spec.rb
+++ b/spec/features/users/projects_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
describe 'Projects tab on a user profile', :feature, :js do
- include WaitForAjax
-
let(:user) { create(:user) }
let!(:project) { create(:empty_project, namespace: user.namespace) }
let!(:project2) { create(:empty_project, namespace: user.namespace) }
diff --git a/spec/features/users/snippets_spec.rb b/spec/features/users/snippets_spec.rb
index ce7e809ec76..4efbd672322 100644
--- a/spec/features/users/snippets_spec.rb
+++ b/spec/features/users/snippets_spec.rb
@@ -1,18 +1,48 @@
require 'spec_helper'
describe 'Snippets tab on a user profile', feature: true, js: true do
- include WaitForAjax
-
context 'when the user has snippets' do
let(:user) { create(:user) }
- let!(:snippets) { create_list(:snippet, 2, :public, author: user) }
- before do
- allow(Snippet).to receive(:default_per_page).and_return(1)
- visit user_path(user)
- page.within('.user-profile-nav') { click_link 'Snippets' }
- wait_for_ajax
+
+ context 'pagination' do
+ let!(:snippets) { create_list(:snippet, 2, :public, author: user) }
+
+ before do
+ allow(Snippet).to receive(:default_per_page).and_return(1)
+ visit user_path(user)
+ page.within('.user-profile-nav') { click_link 'Snippets' }
+ wait_for_ajax
+ end
+
+ it_behaves_like 'paginated snippets', remote: true
end
- it_behaves_like 'paginated snippets', remote: true
+ context 'list content' do
+ let!(:public_snippet) { create(:snippet, :public, author: user) }
+ let!(:internal_snippet) { create(:snippet, :internal, author: user) }
+ let!(:private_snippet) { create(:snippet, :private, author: user) }
+ let!(:other_snippet) { create(:snippet, :public) }
+
+ it 'contains only internal and public snippets of a user when a user is logged in' do
+ login_as(:user)
+ visit user_path(user)
+ page.within('.user-profile-nav') { click_link 'Snippets' }
+ wait_for_ajax
+
+ expect(page).to have_selector('.snippet-row', count: 2)
+
+ expect(page).to have_content(public_snippet.title)
+ expect(page).to have_content(internal_snippet.title)
+ end
+
+ it 'contains only public snippets of a user when a user is not logged in' do
+ visit user_path(user)
+ page.within('.user-profile-nav') { click_link 'Snippets' }
+ wait_for_ajax
+
+ expect(page).to have_selector('.snippet-row', count: 1)
+ expect(page).to have_content(public_snippet.title)
+ end
+ end
end
end
diff --git a/spec/features/users_spec.rb b/spec/features/users_spec.rb
index 2de0fbe7ab2..c43feadc808 100644
--- a/spec/features/users_spec.rb
+++ b/spec/features/users_spec.rb
@@ -68,7 +68,6 @@ feature 'Users', feature: true, js: true do
end
feature 'username validation' do
- include WaitForAjax
let(:loading_icon) { '.fa.fa-spinner' }
let(:username_input) { 'new_user_username' }
diff --git a/spec/features/variables_spec.rb b/spec/features/variables_spec.rb
index a362d6fd3b6..b83a230c1f8 100644
--- a/spec/features/variables_spec.rb
+++ b/spec/features/variables_spec.rb
@@ -2,7 +2,7 @@ require 'spec_helper'
describe 'Project variables', js: true do
let(:user) { create(:user) }
- let(:project) { create(:project) }
+ let(:project) { create(:empty_project) }
let(:variable) { create(:ci_variable, key: 'test_key', value: 'test value') }
before do
diff --git a/spec/finders/groups_finder_spec.rb b/spec/finders/groups_finder_spec.rb
index d5d111e8d15..5b3591550c1 100644
--- a/spec/finders/groups_finder_spec.rb
+++ b/spec/finders/groups_finder_spec.rb
@@ -3,29 +3,64 @@ require 'spec_helper'
describe GroupsFinder do
describe '#execute' do
let(:user) { create(:user) }
- let!(:private_group) { create(:group, :private) }
- let!(:internal_group) { create(:group, :internal) }
- let!(:public_group) { create(:group, :public) }
- let(:finder) { described_class.new }
- describe 'execute' do
- describe 'without a user' do
- subject { finder.execute }
+ context 'root level groups' do
+ let!(:private_group) { create(:group, :private) }
+ let!(:internal_group) { create(:group, :internal) }
+ let!(:public_group) { create(:group, :public) }
+
+ context 'without a user' do
+ subject { described_class.new.execute }
it { is_expected.to eq([public_group]) }
end
- describe 'with a user' do
- subject { finder.execute(user) }
+ context 'with a user' do
+ subject { described_class.new(user).execute }
context 'normal user' do
- it { is_expected.to eq([public_group, internal_group]) }
+ it { is_expected.to contain_exactly(public_group, internal_group) }
end
context 'external user' do
let(:user) { create(:user, external: true) }
- it { is_expected.to eq([public_group]) }
+ it { is_expected.to contain_exactly(public_group) }
+ end
+
+ context 'user is member of the private group' do
+ before do
+ private_group.add_guest(user)
+ end
+
+ it { is_expected.to contain_exactly(public_group, internal_group, private_group) }
+ end
+ end
+ end
+
+ context 'subgroups' do
+ let!(:parent_group) { create(:group, :public) }
+ let!(:public_subgroup) { create(:group, :public, parent: parent_group) }
+ let!(:internal_subgroup) { create(:group, :internal, parent: parent_group) }
+ let!(:private_subgroup) { create(:group, :private, parent: parent_group) }
+
+ context 'without a user' do
+ it 'only returns public subgroups' do
+ expect(described_class.new(nil, parent: parent_group).execute).to contain_exactly(public_subgroup)
+ end
+ end
+
+ context 'with a user' do
+ it 'returns public and internal subgroups' do
+ expect(described_class.new(user, parent: parent_group).execute).to contain_exactly(public_subgroup, internal_subgroup)
+ end
+
+ context 'being member' do
+ it 'returns public subgroups, internal subgroups, and private subgroups user is member of' do
+ private_subgroup.add_guest(user)
+
+ expect(described_class.new(user, parent: parent_group).execute).to contain_exactly(public_subgroup, internal_subgroup, private_subgroup)
+ end
end
end
end
diff --git a/spec/finders/issues_finder_spec.rb b/spec/finders/issues_finder_spec.rb
index 231fd85c464..96151689359 100644
--- a/spec/finders/issues_finder_spec.rb
+++ b/spec/finders/issues_finder_spec.rb
@@ -1,24 +1,24 @@
require 'spec_helper'
describe IssuesFinder do
- let(:user) { create(:user) }
- let(:user2) { create(:user) }
- let(:project1) { create(:empty_project) }
- let(:project2) { create(:empty_project) }
- let(:milestone) { create(:milestone, project: project1) }
- let(:label) { create(:label, project: project2) }
- let(:issue1) { create(:issue, author: user, assignee: user, project: project1, milestone: milestone, title: 'gitlab') }
- let(:issue2) { create(:issue, author: user, assignee: user, project: project2, description: 'gitlab') }
- let(:issue3) { create(:issue, author: user2, assignee: user2, project: project2, title: 'tanuki', description: 'tanuki') }
+ set(:user) { create(:user) }
+ set(:user2) { create(:user) }
+ set(:project1) { create(:empty_project) }
+ set(:project2) { create(:empty_project) }
+ set(:milestone) { create(:milestone, project: project1) }
+ set(:label) { create(:label, project: project2) }
+ set(:issue1) { create(:issue, author: user, assignees: [user], project: project1, milestone: milestone, title: 'gitlab') }
+ set(:issue2) { create(:issue, author: user, assignees: [user], project: project2, description: 'gitlab') }
+ set(:issue3) { create(:issue, author: user2, assignees: [user2], project: project2, title: 'tanuki', description: 'tanuki') }
describe '#execute' do
- let(:closed_issue) { create(:issue, author: user2, assignee: user2, project: project2, state: 'closed') }
- let!(:label_link) { create(:label_link, label: label, target: issue2) }
+ set(:closed_issue) { create(:issue, author: user2, assignees: [user2], project: project2, state: 'closed') }
+ set(:label_link) { create(:label_link, label: label, target: issue2) }
let(:search_user) { user }
let(:params) { {} }
- let(:issues) { IssuesFinder.new(search_user, params.reverse_merge(scope: scope, state: 'opened')).execute }
+ let(:issues) { described_class.new(search_user, params.reverse_merge(scope: scope, state: 'opened')).execute }
- before do
+ before(:context) do
project1.team << [user, :master]
project2.team << [user, :developer]
project2.team << [user2, :developer]
@@ -91,7 +91,7 @@ describe IssuesFinder do
before do
milestones.each do |milestone|
- create(:issue, project: milestone.project, milestone: milestone, author: user, assignee: user)
+ create(:issue, project: milestone.project, milestone: milestone, author: user, assignees: [user])
end
end
@@ -126,7 +126,7 @@ describe IssuesFinder do
before do
milestones.each do |milestone|
- create(:issue, project: milestone.project, milestone: milestone, author: user, assignee: user)
+ create(:issue, project: milestone.project, milestone: milestone, author: user, assignees: [user])
end
end
@@ -282,15 +282,15 @@ describe IssuesFinder do
let!(:confidential_issue) { create(:issue, project: project, confidential: true) }
it 'returns non confidential issues for nil user' do
- expect(IssuesFinder.send(:not_restricted_by_confidentiality, nil)).to include(public_issue)
+ expect(described_class.send(:not_restricted_by_confidentiality, nil)).to include(public_issue)
end
it 'returns non confidential issues for user not authorized for the issues projects' do
- expect(IssuesFinder.send(:not_restricted_by_confidentiality, user)).to include(public_issue)
+ expect(described_class.send(:not_restricted_by_confidentiality, user)).to include(public_issue)
end
it 'returns all issues for user authorized for the issues projects' do
- expect(IssuesFinder.send(:not_restricted_by_confidentiality, authorized_user)).to include(public_issue, confidential_issue)
+ expect(described_class.send(:not_restricted_by_confidentiality, authorized_user)).to include(public_issue, confidential_issue)
end
end
end
diff --git a/spec/finders/merge_requests_finder_spec.rb b/spec/finders/merge_requests_finder_spec.rb
index 21ef94ac5d1..58b7cd5e098 100644
--- a/spec/finders/merge_requests_finder_spec.rb
+++ b/spec/finders/merge_requests_finder_spec.rb
@@ -23,26 +23,26 @@ describe MergeRequestsFinder do
describe "#execute" do
it 'filters by scope' do
params = { scope: 'authored', state: 'opened' }
- merge_requests = MergeRequestsFinder.new(user, params).execute
+ merge_requests = described_class.new(user, params).execute
expect(merge_requests.size).to eq(3)
end
it 'filters by project' do
params = { project_id: project1.id, scope: 'authored', state: 'opened' }
- merge_requests = MergeRequestsFinder.new(user, params).execute
+ merge_requests = described_class.new(user, params).execute
expect(merge_requests.size).to eq(1)
end
it 'filters by non_archived' do
params = { non_archived: true }
- merge_requests = MergeRequestsFinder.new(user, params).execute
+ merge_requests = described_class.new(user, params).execute
expect(merge_requests.size).to eq(3)
end
it 'filters by iid' do
params = { project_id: project1.id, iids: merge_request1.iid }
- merge_requests = MergeRequestsFinder.new(user, params).execute
+ merge_requests = described_class.new(user, params).execute
expect(merge_requests).to contain_exactly(merge_request1)
end
diff --git a/spec/finders/notes_finder_spec.rb b/spec/finders/notes_finder_spec.rb
index 765bf44d863..ba6bbb3bce0 100644
--- a/spec/finders/notes_finder_spec.rb
+++ b/spec/finders/notes_finder_spec.rb
@@ -110,6 +110,15 @@ describe NotesFinder do
expect(notes.count).to eq(1)
end
+ it 'finds notes on personal snippets' do
+ note = create(:note_on_personal_snippet)
+ params = { target_type: 'personal_snippet', target_id: note.noteable_id }
+
+ notes = described_class.new(project, user, params).execute
+
+ expect(notes.count).to eq(1)
+ end
+
it 'raises an exception for an invalid target_type' do
params[:target_type] = 'invalid'
expect { described_class.new(project, user, params).execute }.to raise_error('invalid target_type')
diff --git a/spec/finders/pipeline_schedules_finder_spec.rb b/spec/finders/pipeline_schedules_finder_spec.rb
new file mode 100644
index 00000000000..e184a87c9c7
--- /dev/null
+++ b/spec/finders/pipeline_schedules_finder_spec.rb
@@ -0,0 +1,41 @@
+require 'spec_helper'
+
+describe PipelineSchedulesFinder do
+ let(:project) { create(:empty_project) }
+
+ let!(:active_schedule) { create(:ci_pipeline_schedule, project: project) }
+ let!(:inactive_schedule) { create(:ci_pipeline_schedule, :inactive, project: project) }
+
+ subject { described_class.new(project).execute(params) }
+
+ describe "#execute" do
+ context 'when the scope is nil' do
+ let(:params) { { scope: nil } }
+
+ it 'selects all pipeline pipeline schedules' do
+ expect(subject.count).to be(2)
+ expect(subject).to include(active_schedule, inactive_schedule)
+ end
+ end
+
+ context 'when the scope is active' do
+ let(:params) { { scope: 'active' } }
+
+ it 'selects only active pipelines' do
+ expect(subject.count).to be(1)
+ expect(subject).to include(active_schedule)
+ expect(subject).not_to include(inactive_schedule)
+ end
+ end
+
+ context 'when the scope is inactve' do
+ let(:params) { { scope: 'inactive' } }
+
+ it 'selects only inactive pipelines' do
+ expect(subject.count).to be(1)
+ expect(subject).not_to include(active_schedule)
+ expect(subject).to include(inactive_schedule)
+ end
+ end
+ end
+end
diff --git a/spec/finders/pipelines_finder_spec.rb b/spec/finders/pipelines_finder_spec.rb
index 6bada7b3eb9..f2aeda241c1 100644
--- a/spec/finders/pipelines_finder_spec.rb
+++ b/spec/finders/pipelines_finder_spec.rb
@@ -3,50 +3,205 @@ require 'spec_helper'
describe PipelinesFinder do
let(:project) { create(:project, :repository) }
- let!(:tag_pipeline) { create(:ci_pipeline, project: project, ref: 'v1.0.0') }
- let!(:branch_pipeline) { create(:ci_pipeline, project: project) }
-
- subject { described_class.new(project).execute(params) }
+ subject { described_class.new(project, params).execute }
describe "#execute" do
- context 'when a scope is passed' do
- context 'when scope is nil' do
- let(:params) { { scope: nil } }
+ context 'when params is empty' do
+ let(:params) { {} }
+ let!(:pipelines) { create_list(:ci_pipeline, 2, project: project) }
+
+ it 'returns all pipelines' do
+ is_expected.to match_array(pipelines)
+ end
+ end
+
+ %w[running pending].each do |target|
+ context "when scope is #{target}" do
+ let(:params) { { scope: target } }
+ let!(:pipeline) { create(:ci_pipeline, project: project, status: target) }
- it 'selects all pipelines' do
- expect(subject.count).to be 2
- expect(subject).to include tag_pipeline
- expect(subject).to include branch_pipeline
+ it 'returns matched pipelines' do
+ is_expected.to eq([pipeline])
end
end
+ end
+
+ context 'when scope is finished' do
+ let(:params) { { scope: 'finished' } }
+ let!(:pipelines) do
+ [create(:ci_pipeline, project: project, status: 'success'),
+ create(:ci_pipeline, project: project, status: 'failed'),
+ create(:ci_pipeline, project: project, status: 'canceled')]
+ end
- context 'when selecting branches' do
+ it 'returns matched pipelines' do
+ is_expected.to match_array(pipelines)
+ end
+ end
+
+ context 'when scope is branches or tags' do
+ let!(:pipeline_branch) { create(:ci_pipeline, project: project) }
+ let!(:pipeline_tag) { create(:ci_pipeline, project: project, ref: 'v1.0.0', tag: true) }
+
+ context 'when scope is branches' do
let(:params) { { scope: 'branches' } }
- it 'excludes tags' do
- expect(subject).not_to include tag_pipeline
- expect(subject).to include branch_pipeline
+ it 'returns matched pipelines' do
+ is_expected.to eq([pipeline_branch])
end
end
- context 'when selecting tags' do
+ context 'when scope is tags' do
let(:params) { { scope: 'tags' } }
- it 'excludes branches' do
- expect(subject).to include tag_pipeline
- expect(subject).not_to include branch_pipeline
+ it 'returns matched pipelines' do
+ is_expected.to eq([pipeline_tag])
+ end
+ end
+ end
+
+ HasStatus::AVAILABLE_STATUSES.each do |target|
+ context "when status is #{target}" do
+ let(:params) { { status: target } }
+ let!(:pipeline) { create(:ci_pipeline, project: project, status: target) }
+
+ before do
+ exception_status = HasStatus::AVAILABLE_STATUSES - [target]
+ create(:ci_pipeline, project: project, status: exception_status.first)
+ end
+
+ it 'returns matched pipelines' do
+ is_expected.to eq([pipeline])
end
end
end
- # Scoping to pending will speed up the test as it doesn't hit the FS
- let(:params) { { scope: 'pending' } }
+ context 'when ref is specified' do
+ let!(:pipeline) { create(:ci_pipeline, project: project) }
+
+ context 'when ref exists' do
+ let(:params) { { ref: 'master' } }
+
+ it 'returns matched pipelines' do
+ is_expected.to eq([pipeline])
+ end
+ end
+
+ context 'when ref does not exist' do
+ let(:params) { { ref: 'invalid-ref' } }
+
+ it 'returns empty' do
+ is_expected.to be_empty
+ end
+ end
+ end
+
+ context 'when name is specified' do
+ let(:user) { create(:user) }
+ let!(:pipeline) { create(:ci_pipeline, project: project, user: user) }
+
+ context 'when name exists' do
+ let(:params) { { name: user.name } }
+
+ it 'returns matched pipelines' do
+ is_expected.to eq([pipeline])
+ end
+ end
+
+ context 'when name does not exist' do
+ let(:params) { { name: 'invalid-name' } }
+
+ it 'returns empty' do
+ is_expected.to be_empty
+ end
+ end
+ end
- it 'orders in descending order on ID' do
- feature_pipeline = create(:ci_pipeline, project: project, ref: 'feature')
+ context 'when username is specified' do
+ let(:user) { create(:user) }
+ let!(:pipeline) { create(:ci_pipeline, project: project, user: user) }
- expected_ids = [feature_pipeline.id, branch_pipeline.id, tag_pipeline.id].sort.reverse
- expect(subject.map(&:id)).to eq expected_ids
+ context 'when username exists' do
+ let(:params) { { username: user.username } }
+
+ it 'returns matched pipelines' do
+ is_expected.to eq([pipeline])
+ end
+ end
+
+ context 'when username does not exist' do
+ let(:params) { { username: 'invalid-username' } }
+
+ it 'returns empty' do
+ is_expected.to be_empty
+ end
+ end
+ end
+
+ context 'when yaml_errors is specified' do
+ let!(:pipeline1) { create(:ci_pipeline, project: project, yaml_errors: 'Syntax error') }
+ let!(:pipeline2) { create(:ci_pipeline, project: project) }
+
+ context 'when yaml_errors is true' do
+ let(:params) { { yaml_errors: true } }
+
+ it 'returns matched pipelines' do
+ is_expected.to eq([pipeline1])
+ end
+ end
+
+ context 'when yaml_errors is false' do
+ let(:params) { { yaml_errors: false } }
+
+ it 'returns matched pipelines' do
+ is_expected.to eq([pipeline2])
+ end
+ end
+
+ context 'when yaml_errors is invalid' do
+ let(:params) { { yaml_errors: "invalid-yaml_errors" } }
+
+ it 'returns all pipelines' do
+ is_expected.to match_array([pipeline1, pipeline2])
+ end
+ end
+ end
+
+ context 'when order_by and sort are specified' do
+ context 'when order_by user_id' do
+ let(:params) { { order_by: 'user_id', sort: 'asc' } }
+ let!(:pipelines) { create_list(:ci_pipeline, 2, project: project, user: create(:user)) }
+
+ it 'sorts as user_id: :asc' do
+ is_expected.to match_array(pipelines)
+ end
+
+ context 'when sort is invalid' do
+ let(:params) { { order_by: 'user_id', sort: 'invalid_sort' } }
+
+ it 'sorts as user_id: :desc' do
+ is_expected.to eq(pipelines.sort_by { |p| -p.user.id })
+ end
+ end
+ end
+
+ context 'when order_by is invalid' do
+ let(:params) { { order_by: 'invalid_column', sort: 'asc' } }
+ let!(:pipelines) { create_list(:ci_pipeline, 2, project: project) }
+
+ it 'sorts as id: :asc' do
+ is_expected.to eq(pipelines.sort_by { |p| p.id })
+ end
+ end
+
+ context 'when both are nil' do
+ let(:params) { { order_by: nil, sort: nil } }
+ let!(:pipelines) { create_list(:ci_pipeline, 2, project: project) }
+
+ it 'sorts as id: :desc' do
+ is_expected.to eq(pipelines.sort_by { |p| -p.id })
+ end
+ end
end
end
end
diff --git a/spec/finders/snippets_finder_spec.rb b/spec/finders/snippets_finder_spec.rb
index 975e99c5807..35f1683eef9 100644
--- a/spec/finders/snippets_finder_spec.rb
+++ b/spec/finders/snippets_finder_spec.rb
@@ -8,79 +8,145 @@ describe SnippetsFinder do
let(:project1) { create(:empty_project, :public, group: group) }
let(:project2) { create(:empty_project, :private, group: group) }
- context ':all filter' do
+ context 'all snippets visible to a user' do
let!(:snippet1) { create(:personal_snippet, :private) }
let!(:snippet2) { create(:personal_snippet, :internal) }
let!(:snippet3) { create(:personal_snippet, :public) }
+ let!(:project_snippet1) { create(:project_snippet, :private) }
+ let!(:project_snippet2) { create(:project_snippet, :internal) }
+ let!(:project_snippet3) { create(:project_snippet, :public) }
it "returns all private and internal snippets" do
- snippets = SnippetsFinder.new.execute(user, filter: :all)
- expect(snippets).to include(snippet2, snippet3)
- expect(snippets).not_to include(snippet1)
+ snippets = described_class.new(user, scope: :all).execute
+ expect(snippets).to include(snippet2, snippet3, project_snippet2, project_snippet3)
+ expect(snippets).not_to include(snippet1, project_snippet1)
end
it "returns all public snippets" do
- snippets = SnippetsFinder.new.execute(nil, filter: :all)
- expect(snippets).to include(snippet3)
- expect(snippets).not_to include(snippet1, snippet2)
+ snippets = described_class.new(nil, scope: :all).execute
+ expect(snippets).to include(snippet3, project_snippet3)
+ expect(snippets).not_to include(snippet1, snippet2, project_snippet1, project_snippet2)
+ end
+
+ it "returns all public and internal snippets for normal user" do
+ snippets = described_class.new(user).execute
+
+ expect(snippets).to include(snippet2, snippet3, project_snippet2, project_snippet3)
+ expect(snippets).not_to include(snippet1, project_snippet1)
+ end
+
+ it "returns all public snippets for non authorized user" do
+ snippets = described_class.new(nil).execute
+
+ expect(snippets).to include(snippet3, project_snippet3)
+ expect(snippets).not_to include(snippet1, snippet2, project_snippet1, project_snippet2)
+ end
+
+ it "returns all public and authored snippets for external user" do
+ external_user = create(:user, :external)
+ authored_snippet = create(:personal_snippet, :internal, author: external_user)
+
+ snippets = described_class.new(external_user).execute
+
+ expect(snippets).to include(snippet3, project_snippet3, authored_snippet)
+ expect(snippets).not_to include(snippet1, snippet2, project_snippet1, project_snippet2)
end
end
- context ':public filter' do
+ context 'filter by visibility' do
let!(:snippet1) { create(:personal_snippet, :private) }
let!(:snippet2) { create(:personal_snippet, :internal) }
let!(:snippet3) { create(:personal_snippet, :public) }
- it "returns public public snippets" do
- snippets = SnippetsFinder.new.execute(nil, filter: :public)
+ it "returns public snippets when visibility is PUBLIC" do
+ snippets = described_class.new(nil, visibility: Snippet::PUBLIC).execute
expect(snippets).to include(snippet3)
expect(snippets).not_to include(snippet1, snippet2)
end
end
- context ':by_user filter' do
+ context 'filter by scope' do
+ let!(:snippet1) { create(:personal_snippet, :private, author: user) }
+ let!(:snippet2) { create(:personal_snippet, :internal, author: user) }
+ let!(:snippet3) { create(:personal_snippet, :public, author: user) }
+
+ it "returns all snippets for 'all' scope" do
+ snippets = described_class.new(user, scope: :all).execute
+
+ expect(snippets).to include(snippet1, snippet2, snippet3)
+ end
+
+ it "returns all snippets for 'are_private' scope" do
+ snippets = described_class.new(user, scope: :are_private).execute
+
+ expect(snippets).to include(snippet1)
+ expect(snippets).not_to include(snippet2, snippet3)
+ end
+
+ it "returns all snippets for 'are_interna;' scope" do
+ snippets = described_class.new(user, scope: :are_internal).execute
+
+ expect(snippets).to include(snippet2)
+ expect(snippets).not_to include(snippet1, snippet3)
+ end
+
+ it "returns all snippets for 'are_private' scope" do
+ snippets = described_class.new(user, scope: :are_public).execute
+
+ expect(snippets).to include(snippet3)
+ expect(snippets).not_to include(snippet1, snippet2)
+ end
+ end
+
+ context 'filter by author' do
let!(:snippet1) { create(:personal_snippet, :private, author: user) }
let!(:snippet2) { create(:personal_snippet, :internal, author: user) }
let!(:snippet3) { create(:personal_snippet, :public, author: user) }
it "returns all public and internal snippets" do
- snippets = SnippetsFinder.new.execute(user1, filter: :by_user, user: user)
+ snippets = described_class.new(user1, author: user).execute
+
expect(snippets).to include(snippet2, snippet3)
expect(snippets).not_to include(snippet1)
end
it "returns internal snippets" do
- snippets = SnippetsFinder.new.execute(user, filter: :by_user, user: user, scope: "are_internal")
+ snippets = described_class.new(user, author: user, visibility: Snippet::INTERNAL).execute
+
expect(snippets).to include(snippet2)
expect(snippets).not_to include(snippet1, snippet3)
end
it "returns private snippets" do
- snippets = SnippetsFinder.new.execute(user, filter: :by_user, user: user, scope: "are_private")
+ snippets = described_class.new(user, author: user, visibility: Snippet::PRIVATE).execute
+
expect(snippets).to include(snippet1)
expect(snippets).not_to include(snippet2, snippet3)
end
it "returns public snippets" do
- snippets = SnippetsFinder.new.execute(user, filter: :by_user, user: user, scope: "are_public")
+ snippets = described_class.new(user, author: user, visibility: Snippet::PUBLIC).execute
+
expect(snippets).to include(snippet3)
expect(snippets).not_to include(snippet1, snippet2)
end
it "returns all snippets" do
- snippets = SnippetsFinder.new.execute(user, filter: :by_user, user: user)
+ snippets = described_class.new(user, author: user).execute
+
expect(snippets).to include(snippet1, snippet2, snippet3)
end
it "returns only public snippets if unauthenticated user" do
- snippets = SnippetsFinder.new.execute(nil, filter: :by_user, user: user)
+ snippets = described_class.new(nil, author: user).execute
+
expect(snippets).to include(snippet3)
expect(snippets).not_to include(snippet2, snippet1)
end
end
- context 'by_project filter' do
+ context 'filter by project' do
before do
@snippet1 = create(:project_snippet, :private, project: project1)
@snippet2 = create(:project_snippet, :internal, project: project1)
@@ -88,43 +154,52 @@ describe SnippetsFinder do
end
it "returns public snippets for unauthorized user" do
- snippets = SnippetsFinder.new.execute(nil, filter: :by_project, project: project1)
+ snippets = described_class.new(nil, project: project1).execute
+
expect(snippets).to include(@snippet3)
expect(snippets).not_to include(@snippet1, @snippet2)
end
it "returns public and internal snippets for non project members" do
- snippets = SnippetsFinder.new.execute(user, filter: :by_project, project: project1)
+ snippets = described_class.new(user, project: project1).execute
+
expect(snippets).to include(@snippet2, @snippet3)
expect(snippets).not_to include(@snippet1)
end
it "returns public snippets for non project members" do
- snippets = SnippetsFinder.new.execute(user, filter: :by_project, project: project1, scope: "are_public")
+ snippets = described_class.new(user, project: project1, visibility: Snippet::PUBLIC).execute
+
expect(snippets).to include(@snippet3)
expect(snippets).not_to include(@snippet1, @snippet2)
end
it "returns internal snippets for non project members" do
- snippets = SnippetsFinder.new.execute(user, filter: :by_project, project: project1, scope: "are_internal")
+ snippets = described_class.new(user, project: project1, visibility: Snippet::INTERNAL).execute
+
expect(snippets).to include(@snippet2)
expect(snippets).not_to include(@snippet1, @snippet3)
end
it "does not return private snippets for non project members" do
- snippets = SnippetsFinder.new.execute(user, filter: :by_project, project: project1, scope: "are_private")
+ snippets = described_class.new(user, project: project1, visibility: Snippet::PRIVATE).execute
+
expect(snippets).not_to include(@snippet1, @snippet2, @snippet3)
end
it "returns all snippets for project members" do
project1.team << [user, :developer]
- snippets = SnippetsFinder.new.execute(user, filter: :by_project, project: project1)
+
+ snippets = described_class.new(user, project: project1).execute
+
expect(snippets).to include(@snippet1, @snippet2, @snippet3)
end
it "returns private snippets for project members" do
project1.team << [user, :developer]
- snippets = SnippetsFinder.new.execute(user, filter: :by_project, project: project1, scope: "are_private")
+
+ snippets = described_class.new(user, project: project1, visibility: Snippet::PRIVATE).execute
+
expect(snippets).to include(@snippet1)
end
end
diff --git a/spec/fixtures/api/schemas/branch.json b/spec/fixtures/api/schemas/branch.json
new file mode 100644
index 00000000000..0bb74577010
--- /dev/null
+++ b/spec/fixtures/api/schemas/branch.json
@@ -0,0 +1,12 @@
+{
+ "type": "object",
+ "required" : [
+ "name",
+ "url"
+ ],
+ "properties" : {
+ "name": { "type": "string" },
+ "url": { "type": "uri" }
+ },
+ "additionalProperties": false
+}
diff --git a/spec/fixtures/api/schemas/deployments.json b/spec/fixtures/api/schemas/deployments.json
new file mode 100644
index 00000000000..1112f23aab2
--- /dev/null
+++ b/spec/fixtures/api/schemas/deployments.json
@@ -0,0 +1,58 @@
+{
+ "additionalProperties": false,
+ "properties": {
+ "deployments": {
+ "items": {
+ "additionalProperties": false,
+ "properties": {
+ "created_at": {
+ "type": "string"
+ },
+ "id": {
+ "type": "integer"
+ },
+ "iid": {
+ "type": "integer"
+ },
+ "last?": {
+ "type": "boolean"
+ },
+ "ref": {
+ "additionalProperties": false,
+ "properties": {
+ "name": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "name"
+ ],
+ "type": "object"
+ },
+ "sha": {
+ "type": "string"
+ },
+ "tag": {
+ "type": "boolean"
+ }
+ },
+ "required": [
+ "sha",
+ "created_at",
+ "iid",
+ "tag",
+ "last?",
+ "ref",
+ "id"
+ ],
+ "type": "object"
+ },
+ "minItems": 1,
+ "type": "array"
+ }
+ },
+ "required": [
+ "deployments"
+ ],
+ "type": "object"
+}
diff --git a/spec/fixtures/api/schemas/entities/merge_request.json b/spec/fixtures/api/schemas/entities/merge_request.json
new file mode 100644
index 00000000000..0a7e0e2d5f2
--- /dev/null
+++ b/spec/fixtures/api/schemas/entities/merge_request.json
@@ -0,0 +1,98 @@
+{
+ "type": "object",
+ "properties" : {
+ "id": { "type": "integer" },
+ "iid": { "type": "integer" },
+ "assignee_id": { "type": ["integer", "null"] },
+ "author_id": { "type": "integer" },
+ "description": { "type": ["string", "null"] },
+ "lock_version": { "type": ["string", "null"] },
+ "milestone_id": { "type": ["string", "null"] },
+ "position": { "type": "integer" },
+ "state": { "type": "string" },
+ "title": { "type": "string" },
+ "updated_by_id": { "type": ["string", "null"] },
+ "created_at": { "type": "string" },
+ "updated_at": { "type": "string" },
+ "deleted_at": { "type": ["string", "null"] },
+ "time_estimate": { "type": "integer" },
+ "total_time_spent": { "type": "integer" },
+ "human_time_estimate": { "type": ["integer", "null"] },
+ "human_total_time_spent": { "type": ["integer", "null"] },
+ "in_progress_merge_commit_sha": { "type": ["string", "null"] },
+ "locked_at": { "type": ["string", "null"] },
+ "merge_error": { "type": ["string", "null"] },
+ "merge_commit_sha": { "type": ["string", "null"] },
+ "merge_params": { "type": ["object", "null"] },
+ "merge_status": { "type": "string" },
+ "merge_user_id": { "type": ["integer", "null"] },
+ "merge_when_pipeline_succeeds": { "type": "boolean" },
+ "source_branch": { "type": "string" },
+ "source_project_id": { "type": "integer" },
+ "target_branch": { "type": "string" },
+ "target_project_id": { "type": "integer" },
+ "merge_event": { "type": ["object", "null"] },
+ "closed_event": { "type": ["object", "null"] },
+ "author": { "type": ["object", "null"] },
+ "merge_user": { "type": ["object", "null"] },
+ "diff_head_sha": { "type": ["string", "null"] },
+ "diff_head_commit_short_id": { "type": ["string", "null"] },
+ "merge_commit_message": { "type": ["string", "null"] },
+ "pipeline": { "type": ["object", "null"] },
+ "work_in_progress": { "type": "boolean" },
+ "source_branch_exists": { "type": "boolean" },
+ "mergeable_discussions_state": { "type": "boolean" },
+ "conflicts_can_be_resolved_in_ui": { "type": "boolean" },
+ "branch_missing": { "type": "boolean" },
+ "has_conflicts": { "type": "boolean" },
+ "can_be_merged": { "type": "boolean" },
+ "project_archived": { "type": "boolean" },
+ "only_allow_merge_if_pipeline_succeeds": { "type": "boolean" },
+ "has_ci": { "type": "boolean" },
+ "ci_status": { "type": ["string", "null"] },
+ "issues_links": {
+ "type": "object",
+ "required": ["closing", "mentioned_but_not_closing", "assign_to_closing"],
+ "properties" : {
+ "closing": { "type": "string" },
+ "mentioned_but_not_closing": { "type": "string" },
+ "assign_to_closing": { "type": ["string", "null"] }
+ },
+ "additionalProperties": false
+ },
+ "source_branch_with_namespace_link": { "type": "string" },
+ "current_user": {
+ "type": "object",
+ "required": [
+ "can_remove_source_branch",
+ "can_revert_on_current_merge_request",
+ "can_cherry_pick_on_current_merge_request"
+ ],
+ "properties": {
+ "can_remove_source_branch": { "type": "boolean" },
+ "can_revert_on_current_merge_request": { "type": ["boolean", "null"] },
+ "can_cherry_pick_on_current_merge_request": { "type": ["boolean", "null"] }
+ },
+ "additionalProperties": false
+ },
+ "target_branch_commits_path": { "type": "string" },
+ "source_branch_path": { "type": "string" },
+ "conflict_resolution_path": { "type": ["string", "null"] },
+ "cancel_merge_when_pipeline_succeeds_path": { "type": "string" },
+ "create_issue_to_resolve_discussions_path": { "type": "string" },
+ "merge_path": { "type": "string" },
+ "cherry_pick_in_fork_path": { "type": ["string", "null"] },
+ "revert_in_fork_path": { "type": ["string", "null"] },
+ "email_patches_path": { "type": "string" },
+ "plain_diff_path": { "type": "string" },
+ "status_path": { "type": "string" },
+ "merge_check_path": { "type": "string" },
+ "ci_environments_status_path": { "type": "string" },
+ "merge_commit_message_with_description": { "type": "string" },
+ "diverged_commits_count": { "type": "integer" },
+ "commit_change_content_path": { "type": "string" },
+ "remove_wip_path": { "type": "string" },
+ "commits_count": { "type": "integer" }
+ },
+ "additionalProperties": false
+}
diff --git a/spec/fixtures/api/schemas/entities/merge_request_basic.json b/spec/fixtures/api/schemas/entities/merge_request_basic.json
new file mode 100644
index 00000000000..ea6364b878c
--- /dev/null
+++ b/spec/fixtures/api/schemas/entities/merge_request_basic.json
@@ -0,0 +1,14 @@
+{
+ "type": "object",
+ "properties" : {
+ "state": { "type": "string" },
+ "merge_status": { "type": "string" },
+ "source_branch_exists": { "type": "boolean" },
+ "time_estimate": { "type": "integer" },
+ "total_time_spent": { "type": "integer" },
+ "human_time_estimate": { "type": ["string", "null"] },
+ "human_total_time_spent": { "type": ["string", "null"] },
+ "merge_error": { "type": ["string", "null"] }
+ },
+ "additionalProperties": false
+}
diff --git a/spec/fixtures/api/schemas/issue.json b/spec/fixtures/api/schemas/issue.json
index 21c078e0f44..ff86437fdd5 100644
--- a/spec/fixtures/api/schemas/issue.json
+++ b/spec/fixtures/api/schemas/issue.json
@@ -46,6 +46,24 @@
"username": { "type": "string" },
"avatar_url": { "type": "uri" }
},
+ "assignees": {
+ "type": "array",
+ "items": {
+ "type": ["object", "null"],
+ "required": [
+ "id",
+ "name",
+ "username",
+ "avatar_url"
+ ],
+ "properties": {
+ "id": { "type": "integer" },
+ "name": { "type": "string" },
+ "username": { "type": "string" },
+ "avatar_url": { "type": "uri" }
+ }
+ }
+ },
"subscribed": { "type": ["boolean", "null"] }
},
"additionalProperties": false
diff --git a/spec/fixtures/api/schemas/merge_request.json b/spec/fixtures/api/schemas/merge_request.json
new file mode 100644
index 00000000000..36962660cd9
--- /dev/null
+++ b/spec/fixtures/api/schemas/merge_request.json
@@ -0,0 +1,12 @@
+{
+ "type": "object",
+ "required" : [
+ "iid",
+ "url"
+ ],
+ "properties" : {
+ "iid": { "type": "integer" },
+ "url": { "type": "uri" }
+ },
+ "additionalProperties": false
+}
diff --git a/spec/fixtures/api/schemas/pipeline.json b/spec/fixtures/api/schemas/pipeline.json
new file mode 100644
index 00000000000..55511d17b5e
--- /dev/null
+++ b/spec/fixtures/api/schemas/pipeline.json
@@ -0,0 +1,354 @@
+{
+ "$schema": "http://json-schema.org/draft-04/schema#",
+ "definitions": {},
+ "id": "http://example.com/example.json",
+ "properties": {
+ "commit": {
+ "id": "/properties/commit",
+ "properties": {
+ "author": {
+ "id": "/properties/commit/properties/author",
+ "type": "null"
+ },
+ "author_email": {
+ "id": "/properties/commit/properties/author_email",
+ "type": "string"
+ },
+ "author_gravatar_url": {
+ "id": "/properties/commit/properties/author_gravatar_url",
+ "type": "string"
+ },
+ "author_name": {
+ "id": "/properties/commit/properties/author_name",
+ "type": "string"
+ },
+ "authored_date": {
+ "id": "/properties/commit/properties/authored_date",
+ "type": "string"
+ },
+ "commit_path": {
+ "id": "/properties/commit/properties/commit_path",
+ "type": "string"
+ },
+ "commit_url": {
+ "id": "/properties/commit/properties/commit_url",
+ "type": "string"
+ },
+ "committed_date": {
+ "id": "/properties/commit/properties/committed_date",
+ "type": "string"
+ },
+ "committer_email": {
+ "id": "/properties/commit/properties/committer_email",
+ "type": "string"
+ },
+ "committer_name": {
+ "id": "/properties/commit/properties/committer_name",
+ "type": "string"
+ },
+ "created_at": {
+ "id": "/properties/commit/properties/created_at",
+ "type": "string"
+ },
+ "id": {
+ "id": "/properties/commit/properties/id",
+ "type": "string"
+ },
+ "message": {
+ "id": "/properties/commit/properties/message",
+ "type": "string"
+ },
+ "parent_ids": {
+ "id": "/properties/commit/properties/parent_ids",
+ "items": {
+ "id": "/properties/commit/properties/parent_ids/items",
+ "type": "string"
+ },
+ "type": "array"
+ },
+ "short_id": {
+ "id": "/properties/commit/properties/short_id",
+ "type": "string"
+ },
+ "title": {
+ "id": "/properties/commit/properties/title",
+ "type": "string"
+ }
+ },
+ "type": "object"
+ },
+ "created_at": {
+ "id": "/properties/created_at",
+ "type": "string"
+ },
+ "details": {
+ "id": "/properties/details",
+ "properties": {
+ "artifacts": {
+ "id": "/properties/details/properties/artifacts",
+ "items": {},
+ "type": "array"
+ },
+ "duration": {
+ "id": "/properties/details/properties/duration",
+ "type": "integer"
+ },
+ "finished_at": {
+ "id": "/properties/details/properties/finished_at",
+ "type": "string"
+ },
+ "manual_actions": {
+ "id": "/properties/details/properties/manual_actions",
+ "items": {},
+ "type": "array"
+ },
+ "stages": {
+ "id": "/properties/details/properties/stages",
+ "items": {
+ "id": "/properties/details/properties/stages/items",
+ "properties": {
+ "dropdown_path": {
+ "id": "/properties/details/properties/stages/items/properties/dropdown_path",
+ "type": "string"
+ },
+ "groups": {
+ "id": "/properties/details/properties/stages/items/properties/groups",
+ "items": {
+ "id": "/properties/details/properties/stages/items/properties/groups/items",
+ "properties": {
+ "name": {
+ "id": "/properties/details/properties/stages/items/properties/groups/items/properties/name",
+ "type": "string"
+ },
+ "size": {
+ "id": "/properties/details/properties/stages/items/properties/groups/items/properties/size",
+ "type": "integer"
+ },
+ "status": {
+ "id": "/properties/details/properties/stages/items/properties/groups/items/properties/status",
+ "properties": {
+ "details_path": {
+ "id": "/properties/details/properties/stages/items/properties/groups/items/properties/status/properties/details_path",
+ "type": "null"
+ },
+ "favicon": {
+ "id": "/properties/details/properties/stages/items/properties/groups/items/properties/status/properties/favicon",
+ "type": "string"
+ },
+ "group": {
+ "id": "/properties/details/properties/stages/items/properties/groups/items/properties/status/properties/group",
+ "type": "string"
+ },
+ "has_details": {
+ "id": "/properties/details/properties/stages/items/properties/groups/items/properties/status/properties/has_details",
+ "type": "boolean"
+ },
+ "icon": {
+ "id": "/properties/details/properties/stages/items/properties/groups/items/properties/status/properties/icon",
+ "type": "string"
+ },
+ "label": {
+ "id": "/properties/details/properties/stages/items/properties/groups/items/properties/status/properties/label",
+ "type": "string"
+ },
+ "text": {
+ "id": "/properties/details/properties/stages/items/properties/groups/items/properties/status/properties/text",
+ "type": "string"
+ }
+ },
+ "type": "object"
+ }
+ },
+ "type": "object"
+ },
+ "type": "array"
+ },
+ "name": {
+ "id": "/properties/details/properties/stages/items/properties/name",
+ "type": "string"
+ },
+ "path": {
+ "id": "/properties/details/properties/stages/items/properties/path",
+ "type": "string"
+ },
+ "status": {
+ "id": "/properties/details/properties/stages/items/properties/status",
+ "properties": {
+ "details_path": {
+ "id": "/properties/details/properties/stages/items/properties/status/properties/details_path",
+ "type": "string"
+ },
+ "favicon": {
+ "id": "/properties/details/properties/stages/items/properties/status/properties/favicon",
+ "type": "string"
+ },
+ "group": {
+ "id": "/properties/details/properties/stages/items/properties/status/properties/group",
+ "type": "string"
+ },
+ "has_details": {
+ "id": "/properties/details/properties/stages/items/properties/status/properties/has_details",
+ "type": "boolean"
+ },
+ "icon": {
+ "id": "/properties/details/properties/stages/items/properties/status/properties/icon",
+ "type": "string"
+ },
+ "label": {
+ "id": "/properties/details/properties/stages/items/properties/status/properties/label",
+ "type": "string"
+ },
+ "text": {
+ "id": "/properties/details/properties/stages/items/properties/status/properties/text",
+ "type": "string"
+ }
+ },
+ "type": "object"
+ },
+ "title": {
+ "id": "/properties/details/properties/stages/items/properties/title",
+ "type": "string"
+ }
+ },
+ "type": "object"
+ },
+ "type": "array"
+ },
+ "status": {
+ "id": "/properties/details/properties/status",
+ "properties": {
+ "details_path": {
+ "id": "/properties/details/properties/status/properties/details_path",
+ "type": "string"
+ },
+ "favicon": {
+ "id": "/properties/details/properties/status/properties/favicon",
+ "type": "string"
+ },
+ "group": {
+ "id": "/properties/details/properties/status/properties/group",
+ "type": "string"
+ },
+ "has_details": {
+ "id": "/properties/details/properties/status/properties/has_details",
+ "type": "boolean"
+ },
+ "icon": {
+ "id": "/properties/details/properties/status/properties/icon",
+ "type": "string"
+ },
+ "label": {
+ "id": "/properties/details/properties/status/properties/label",
+ "type": "string"
+ },
+ "text": {
+ "id": "/properties/details/properties/status/properties/text",
+ "type": "string"
+ }
+ },
+ "type": "object"
+ }
+ },
+ "type": "object"
+ },
+ "flags": {
+ "id": "/properties/flags",
+ "properties": {
+ "cancelable": {
+ "id": "/properties/flags/properties/cancelable",
+ "type": "boolean"
+ },
+ "latest": {
+ "id": "/properties/flags/properties/latest",
+ "type": "boolean"
+ },
+ "retryable": {
+ "id": "/properties/flags/properties/retryable",
+ "type": "boolean"
+ },
+ "stuck": {
+ "id": "/properties/flags/properties/stuck",
+ "type": "boolean"
+ },
+ "triggered": {
+ "id": "/properties/flags/properties/triggered",
+ "type": "boolean"
+ },
+ "yaml_errors": {
+ "id": "/properties/flags/properties/yaml_errors",
+ "type": "boolean"
+ }
+ },
+ "type": "object"
+ },
+ "id": {
+ "id": "/properties/id",
+ "type": "integer"
+ },
+ "path": {
+ "id": "/properties/path",
+ "type": "string"
+ },
+ "ref": {
+ "id": "/properties/ref",
+ "properties": {
+ "branch": {
+ "id": "/properties/ref/properties/branch",
+ "type": "boolean"
+ },
+ "name": {
+ "id": "/properties/ref/properties/name",
+ "type": "string"
+ },
+ "path": {
+ "id": "/properties/ref/properties/path",
+ "type": "string"
+ },
+ "tag": {
+ "id": "/properties/ref/properties/tag",
+ "type": "boolean"
+ }
+ },
+ "type": "object"
+ },
+ "retry_path": {
+ "id": "/properties/retry_path",
+ "type": "string"
+ },
+ "updated_at": {
+ "id": "/properties/updated_at",
+ "type": "string"
+ },
+ "user": {
+ "id": "/properties/user",
+ "properties": {
+ "avatar_url": {
+ "id": "/properties/user/properties/avatar_url",
+ "type": "string"
+ },
+ "id": {
+ "id": "/properties/user/properties/id",
+ "type": "integer"
+ },
+ "name": {
+ "id": "/properties/user/properties/name",
+ "type": "string"
+ },
+ "state": {
+ "id": "/properties/user/properties/state",
+ "type": "string"
+ },
+ "username": {
+ "id": "/properties/user/properties/username",
+ "type": "string"
+ },
+ "web_url": {
+ "id": "/properties/user/properties/web_url",
+ "type": "string"
+ }
+ },
+ "type": "object"
+ }
+ },
+ "type": "object"
+}
diff --git a/spec/fixtures/api/schemas/public_api/v4/issues.json b/spec/fixtures/api/schemas/public_api/v4/issues.json
index 52199e75734..2d1c84ee93d 100644
--- a/spec/fixtures/api/schemas/public_api/v4/issues.json
+++ b/spec/fixtures/api/schemas/public_api/v4/issues.json
@@ -33,6 +33,21 @@
},
"additionalProperties": false
},
+ "assignees": {
+ "type": "array",
+ "items": {
+ "type": ["object", "null"],
+ "properties": {
+ "name": { "type": "string" },
+ "username": { "type": "string" },
+ "id": { "type": "integer" },
+ "state": { "type": "string" },
+ "avatar_url": { "type": "uri" },
+ "web_url": { "type": "uri" }
+ },
+ "additionalProperties": false
+ }
+ },
"assignee": {
"type": ["object", "null"],
"properties": {
@@ -67,7 +82,7 @@
"required": [
"id", "iid", "project_id", "title", "description",
"state", "created_at", "updated_at", "labels",
- "milestone", "assignee", "author", "user_notes_count",
+ "milestone", "assignees", "author", "user_notes_count",
"upvotes", "downvotes", "due_date", "confidential",
"web_url"
],
diff --git a/spec/fixtures/api/schemas/public_api/v4/user/public.json b/spec/fixtures/api/schemas/public_api/v4/user/public.json
index 5587cfec61a..faa126b65f2 100644
--- a/spec/fixtures/api/schemas/public_api/v4/user/public.json
+++ b/spec/fixtures/api/schemas/public_api/v4/user/public.json
@@ -9,7 +9,6 @@
"avatar_url",
"web_url",
"created_at",
- "is_admin",
"bio",
"location",
"skype",
@@ -43,7 +42,6 @@
"avatar_url": { "type": "string" },
"web_url": { "type": "string" },
"created_at": { "type": "date" },
- "is_admin": { "type": "boolean" },
"bio": { "type": ["string", "null"] },
"location": { "type": ["string", "null"] },
"skype": { "type": "string" },
diff --git a/spec/fixtures/emails/forwarded_new_issue.eml b/spec/fixtures/emails/forwarded_new_issue.eml
new file mode 100644
index 00000000000..258106bb897
--- /dev/null
+++ b/spec/fixtures/emails/forwarded_new_issue.eml
@@ -0,0 +1,25 @@
+Delivered-To: incoming+gitlabhq/gitlabhq+auth_token@appmail.adventuretime.ooo
+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>
+Delivered-To: support@adventuretime.ooo
+To: support@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/markdown.md.erb b/spec/fixtures/markdown.md.erb
index 0cdbc32431d..51a3e91d201 100644
--- a/spec/fixtures/markdown.md.erb
+++ b/spec/fixtures/markdown.md.erb
@@ -116,7 +116,7 @@ Linking to a file relative to this project's repository should work.
Because life would be :zzz: without Emoji, right? :rocket:
-Get ready for the Emoji :bomb:: :+1::-1::ok_hand::wave::v::raised_hand::muscle:
+Get ready for the Emoji :bomb: : :+1: :-1: :ok_hand: :wave: :v: :raised_hand: :muscle:
### TableOfContentsFilter
diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb
index 5c07ea8a872..785fb724132 100644
--- a/spec/helpers/application_helper_spec.rb
+++ b/spec/helpers/application_helper_spec.rb
@@ -3,6 +3,8 @@ require 'spec_helper'
describe ApplicationHelper do
include UploadHelpers
+ let(:gitlab_host) { "http://#{Gitlab.config.gitlab.host}" }
+
describe 'current_controller?' do
it 'returns true when controller matches argument' do
stub_controller_name('foo')
@@ -56,8 +58,14 @@ describe ApplicationHelper do
describe 'project_icon' do
it 'returns an url for the avatar' do
project = create(:empty_project, avatar: File.open(uploaded_image_temp_path))
+ avatar_url = "/uploads/project/avatar/#{project.id}/banana_sample.gif"
+
+ expect(helper.project_icon(project.full_path).to_s).
+ to eq "<img src=\"#{avatar_url}\" alt=\"Banana sample\" />"
+
+ allow(ActionController::Base).to receive(:asset_host).and_return(gitlab_host)
+ avatar_url = "#{gitlab_host}/uploads/project/avatar/#{project.id}/banana_sample.gif"
- avatar_url = "http://#{Gitlab.config.gitlab.host}/uploads/project/avatar/#{project.id}/banana_sample.gif"
expect(helper.project_icon(project.full_path).to_s).
to eq "<img src=\"#{avatar_url}\" alt=\"Banana sample\" />"
end
@@ -67,9 +75,8 @@ describe ApplicationHelper do
allow_any_instance_of(Project).to receive(:avatar_in_git).and_return(true)
- avatar_url = "http://#{Gitlab.config.gitlab.host}#{namespace_project_avatar_path(project.namespace, project)}"
- expect(helper.project_icon(project.full_path).to_s).to match(
- image_tag(avatar_url))
+ avatar_url = "#{gitlab_host}#{namespace_project_avatar_path(project.namespace, project)}"
+ expect(helper.project_icon(project.full_path).to_s).to match(image_tag(avatar_url))
end
end
@@ -77,8 +84,14 @@ describe ApplicationHelper do
it 'returns an url for the avatar' do
user = create(:user, avatar: File.open(uploaded_image_temp_path))
- expect(helper.avatar_icon(user.email).to_s).
- to match("/uploads/user/avatar/#{user.id}/banana_sample.gif")
+ avatar_url = "/uploads/user/avatar/#{user.id}/banana_sample.gif"
+
+ expect(helper.avatar_icon(user.email).to_s).to match(avatar_url)
+
+ allow(ActionController::Base).to receive(:asset_host).and_return(gitlab_host)
+ avatar_url = "#{gitlab_host}/uploads/user/avatar/#{user.id}/banana_sample.gif"
+
+ expect(helper.avatar_icon(user.email).to_s).to match(avatar_url)
end
it 'returns an url for the avatar with relative url' do
@@ -239,33 +252,6 @@ describe ApplicationHelper do
end
end
- describe 'render_markup' do
- let(:content) { 'Noël' }
- let(:user) { create(:user) }
- before do
- allow(helper).to receive(:current_user).and_return(user)
- end
-
- 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 "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 "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')
-
- expect(helper.render_markup('foo.adoc', content)).to eq('NOEL')
- end
- end
-
describe '#active_when' do
it { expect(helper.active_when(true)).to eq('active') }
it { expect(helper.active_when(false)).to eq(nil) }
diff --git a/spec/helpers/avatars_helper_spec.rb b/spec/helpers/avatars_helper_spec.rb
index 581726c1d0e..6157abfe339 100644
--- a/spec/helpers/avatars_helper_spec.rb
+++ b/spec/helpers/avatars_helper_spec.rb
@@ -15,7 +15,7 @@ describe AvatarsHelper do
end
it "contains the user's avatar image" do
- is_expected.to include(CGI.escapeHTML(user.avatar_url(16)))
+ is_expected.to include(CGI.escapeHTML(user.avatar_url(size: 16)))
end
end
end
diff --git a/spec/helpers/award_emoji_helper_spec.rb b/spec/helpers/award_emoji_helper_spec.rb
new file mode 100644
index 00000000000..7dfd6a3f6b4
--- /dev/null
+++ b/spec/helpers/award_emoji_helper_spec.rb
@@ -0,0 +1,61 @@
+require 'spec_helper'
+
+describe AwardEmojiHelper do
+ describe '.toggle_award_url' do
+ context 'note on personal snippet' do
+ let(:note) { create(:note_on_personal_snippet) }
+
+ it 'returns correct url' do
+ expected_url = "/snippets/#{note.noteable.id}/notes/#{note.id}/toggle_award_emoji"
+
+ expect(helper.toggle_award_url(note)).to eq(expected_url)
+ end
+ end
+
+ context 'note on project item' do
+ let(:note) { create(:note_on_project_snippet) }
+
+ it 'returns correct url' do
+ @project = note.noteable.project
+
+ expected_url = "/#{@project.namespace.path}/#{@project.path}/notes/#{note.id}/toggle_award_emoji"
+
+ expect(helper.toggle_award_url(note)).to eq(expected_url)
+ end
+ end
+
+ context 'personal snippet' do
+ let(:snippet) { create(:personal_snippet) }
+
+ it 'returns correct url' do
+ expected_url = "/snippets/#{snippet.id}/toggle_award_emoji"
+
+ expect(helper.toggle_award_url(snippet)).to eq(expected_url)
+ end
+ end
+
+ context 'merge request' do
+ let(:merge_request) { create(:merge_request) }
+
+ it 'returns correct url' do
+ @project = merge_request.project
+
+ expected_url = "/#{@project.namespace.path}/#{@project.path}/merge_requests/#{merge_request.id}/toggle_award_emoji"
+
+ expect(helper.toggle_award_url(merge_request)).to eq(expected_url)
+ end
+ end
+
+ context 'issue' do
+ let(:issue) { create(:issue) }
+
+ it 'returns correct url' do
+ @project = issue.project
+
+ expected_url = "/#{@project.namespace.path}/#{@project.path}/issues/#{issue.id}/toggle_award_emoji"
+
+ expect(helper.toggle_award_url(issue)).to eq(expected_url)
+ end
+ end
+ end
+end
diff --git a/spec/helpers/blob_helper_spec.rb b/spec/helpers/blob_helper_spec.rb
index 508aeb7cf67..1b4393e6167 100644
--- a/spec/helpers/blob_helper_spec.rb
+++ b/spec/helpers/blob_helper_spec.rb
@@ -56,15 +56,14 @@ describe BlobHelper do
end
end
- describe "#sanitize_svg" do
+ describe "#sanitize_svg_data" 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 'retains essential elements' do
- blob = OpenStruct.new(data: data)
- expect(sanitize_svg(blob).data).to eq(expected)
+ expect(sanitize_svg_data(data)).to eq(expected)
end
end
@@ -105,4 +104,136 @@ describe BlobHelper do
expect(Capybara.string(link).find_link('Edit')[:href]).to eq('/gitlab/gitlabhq/edit/master/README.md?mr_id=10')
end
end
+
+ context 'viewer related' do
+ include FakeBlobHelpers
+
+ let(:project) { build(:empty_project, lfs_enabled: true) }
+
+ before do
+ allow(Gitlab.config.lfs).to receive(:enabled).and_return(true)
+ end
+
+ let(:viewer_class) do
+ Class.new(BlobViewer::Base) do
+ self.max_size = 1.megabyte
+ self.absolute_max_size = 5.megabytes
+ self.type = :rich
+ self.client_side = false
+ end
+ end
+
+ let(:viewer) { viewer_class.new(blob) }
+ let(:blob) { fake_blob }
+
+ describe '#blob_render_error_reason' do
+ context 'for error :too_large' do
+ context 'when the blob size is larger than the absolute max size' do
+ let(:blob) { fake_blob(size: 10.megabytes) }
+
+ it 'returns an error message' do
+ expect(helper.blob_render_error_reason(viewer)).to eq('it is larger than 5 MB')
+ end
+ end
+
+ context 'when the blob size is larger than the max size' do
+ let(:blob) { fake_blob(size: 2.megabytes) }
+
+ it 'returns an error message' do
+ expect(helper.blob_render_error_reason(viewer)).to eq('it is larger than 1 MB')
+ end
+ end
+ end
+
+ context 'for error :server_side_but_stored_externally' do
+ let(:blob) { fake_blob(lfs: true) }
+
+ it 'returns an error message' do
+ expect(helper.blob_render_error_reason(viewer)).to eq('it is stored in LFS')
+ end
+ end
+ end
+
+ describe '#blob_render_error_options' do
+ before do
+ assign(:project, project)
+ assign(:blob, blob)
+ assign(:id, File.join('master', blob.path))
+
+ controller.params[:controller] = 'projects/blob'
+ controller.params[:action] = 'show'
+ controller.params[:namespace_id] = project.namespace.to_param
+ controller.params[:project_id] = project.to_param
+ controller.params[:id] = File.join('master', blob.path)
+ end
+
+ context 'for error :too_large' do
+ context 'when the max size can be overridden' do
+ let(:blob) { fake_blob(size: 2.megabytes) }
+
+ it 'includes a "load it anyway" link' do
+ expect(helper.blob_render_error_options(viewer)).to include(/load it anyway/)
+ end
+ end
+
+ context 'when the max size cannot be overridden' do
+ let(:blob) { fake_blob(size: 10.megabytes) }
+
+ it 'does not include a "load it anyway" link' do
+ expect(helper.blob_render_error_options(viewer)).not_to include(/load it anyway/)
+ end
+ end
+
+ context 'when the viewer is rich' do
+ context 'the blob is rendered as text' do
+ let(:blob) { fake_blob(path: 'file.md', size: 2.megabytes) }
+
+ it 'includes a "view the source" link' do
+ expect(helper.blob_render_error_options(viewer)).to include(/view the source/)
+ end
+ end
+
+ context 'the blob is not rendered as text' do
+ let(:blob) { fake_blob(path: 'file.pdf', binary: true, size: 2.megabytes) }
+
+ it 'does not include a "view the source" link' do
+ expect(helper.blob_render_error_options(viewer)).not_to include(/view the source/)
+ end
+ end
+ end
+
+ context 'when the viewer is not rich' do
+ before do
+ viewer_class.type = :simple
+ end
+
+ let(:blob) { fake_blob(path: 'file.md', size: 2.megabytes) }
+
+ it 'does not include a "view the source" link' do
+ expect(helper.blob_render_error_options(viewer)).not_to include(/view the source/)
+ end
+ end
+
+ it 'includes a "download it" link' do
+ expect(helper.blob_render_error_options(viewer)).to include(/download it/)
+ end
+ end
+
+ context 'for error :server_side_but_stored_externally' do
+ let(:blob) { fake_blob(path: 'file.md', lfs: true) }
+
+ it 'does not include a "load it anyway" link' do
+ expect(helper.blob_render_error_options(viewer)).not_to include(/load it anyway/)
+ end
+
+ it 'does not include a "view the source" link' do
+ expect(helper.blob_render_error_options(viewer)).not_to include(/view the source/)
+ end
+
+ it 'includes a "download it" link' do
+ expect(helper.blob_render_error_options(viewer)).to include(/download it/)
+ end
+ end
+ end
+ end
end
diff --git a/spec/helpers/ci_status_helper_spec.rb b/spec/helpers/ci_status_helper_spec.rb
index c795fe5a2a3..e6bb953e9d8 100644
--- a/spec/helpers/ci_status_helper_spec.rb
+++ b/spec/helpers/ci_status_helper_spec.rb
@@ -6,23 +6,53 @@ describe CiStatusHelper do
let(:success_commit) { double("Ci::Pipeline", status: 'success') }
let(:failed_commit) { double("Ci::Pipeline", status: 'failed') }
- describe 'ci_icon_for_status' do
+ describe '#ci_icon_for_status' do
it 'renders to correct svg on success' do
- expect(helper).to receive(:render).with('shared/icons/icon_status_success.svg', anything)
+ 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)
+ expect(helper).to receive(:render)
+ .with('shared/icons/icon_status_failed.svg', anything)
+
helper.ci_icon_for_status(failed_commit.status)
end
end
+ describe '#ci_text_for_status' do
+ context 'when status is manual' do
+ it 'changes the status to blocked' do
+ expect(helper.ci_text_for_status('manual'))
+ .to eq 'blocked'
+ end
+ end
+
+ context 'when status is success' do
+ it 'changes the status to passed' do
+ expect(helper.ci_text_for_status('success'))
+ .to eq 'passed'
+ end
+ end
+
+ context 'when status is something else' do
+ it 'returns status unchanged' do
+ expect(helper.ci_text_for_status('some-status'))
+ .to eq 'some-status'
+ end
+ end
+ end
+
describe "#pipeline_status_cache_key" do
it "builds a cache key for pipeline status" do
pipeline_status = Gitlab::Cache::Ci::ProjectPipelineStatus.new(
build(:project),
- sha: "123abc",
- status: "success"
+ pipeline_info: {
+ sha: "123abc",
+ status: "success"
+ }
)
expect(helper.pipeline_status_cache_key(pipeline_status)).to eq("pipeline-status/123abc-success")
end
diff --git a/spec/helpers/diff_helper_spec.rb b/spec/helpers/diff_helper_spec.rb
index eae097126ce..dd6566d25bb 100644
--- a/spec/helpers/diff_helper_spec.rb
+++ b/spec/helpers/diff_helper_spec.rb
@@ -122,9 +122,9 @@ describe DiffHelper do
it "returns strings with marked inline diffs" do
marked_old_line, marked_new_line = mark_inline_diffs(old_line, new_line)
- expect(marked_old_line).to eq("abc <span class='idiff left right deletion'>&#39;def&#39;</span>")
+ expect(marked_old_line).to eq(%q{abc <span class="idiff left right deletion">&#39;def&#39;</span>})
expect(marked_old_line).to be_html_safe
- expect(marked_new_line).to eq("abc <span class='idiff left right addition'>&quot;def&quot;</span>")
+ expect(marked_new_line).to eq(%q{abc <span class="idiff left right addition">&quot;def&quot;</span>})
expect(marked_new_line).to be_html_safe
end
end
diff --git a/spec/helpers/events_helper_spec.rb b/spec/helpers/events_helper_spec.rb
index a7c3c281083..c3bd0cb3542 100644
--- a/spec/helpers/events_helper_spec.rb
+++ b/spec/helpers/events_helper_spec.rb
@@ -56,7 +56,7 @@ describe EventsHelper 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">' \
+ expected = "\n<pre class=\"code highlight js-syntax-highlight ruby\">" \
"<code><span class=\"line\"><span class=\"k\">def</span> <span class=\"nf\">test</span>...</span>\n" \
"</code></pre>"
expect(helper.event_note(input)).to eq(expected)
diff --git a/spec/helpers/icons_helper_spec.rb b/spec/helpers/icons_helper_spec.rb
index c052981fe73..91c8faea7fd 100644
--- a/spec/helpers/icons_helper_spec.rb
+++ b/spec/helpers/icons_helper_spec.rb
@@ -1,6 +1,21 @@
require 'spec_helper'
describe IconsHelper do
+ describe 'icon' do
+ it 'returns aria-hidden by default' do
+ star = icon('star')
+
+ expect(star['aria-hidden']).to eq 'aria-hidden'
+ end
+
+ it 'does not return aria-hidden if aria-label is set' do
+ up = icon('up', 'aria-label' => 'up')
+
+ expect(up['aria-hidden']).to be_nil
+ expect(up['aria-label']).to eq 'aria-label'
+ end
+ end
+
describe 'file_type_icon_class' do
it 'returns folder class' do
expect(file_type_icon_class('folder', 0, 'folder_name')).to eq 'folder'
diff --git a/spec/helpers/issuables_helper_spec.rb b/spec/helpers/issuables_helper_spec.rb
index 93bb711f29a..c1ecb46aece 100644
--- a/spec/helpers/issuables_helper_spec.rb
+++ b/spec/helpers/issuables_helper_spec.rb
@@ -4,6 +4,23 @@ describe IssuablesHelper do
let(:label) { build_stubbed(:label) }
let(:label2) { build_stubbed(:label) }
+ describe '#users_dropdown_label' do
+ let(:user) { build_stubbed(:user) }
+ let(:user2) { build_stubbed(:user) }
+
+ it 'returns unassigned' do
+ expect(users_dropdown_label([])).to eq('Unassigned')
+ end
+
+ it 'returns selected user\'s name' do
+ expect(users_dropdown_label([user])).to eq(user.name)
+ end
+
+ it 'returns selected user\'s name and counter' do
+ expect(users_dropdown_label([user, user2])).to eq("#{user.name} + 1 more")
+ end
+ end
+
describe '#issuable_labels_tooltip' do
it 'returns label text' do
expect(issuable_labels_tooltip([label])).to eq(label.title)
diff --git a/spec/helpers/gitlab_markdown_helper_spec.rb b/spec/helpers/markup_helper_spec.rb
index 6cf3f86680a..2a0de0b0656 100644
--- a/spec/helpers/gitlab_markdown_helper_spec.rb
+++ b/spec/helpers/markup_helper_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
-describe GitlabMarkdownHelper do
- include ApplicationHelper
-
+describe MarkupHelper do
let!(:project) { create(:project, :repository) }
let(:user) { create(:user, username: 'gfm') }
@@ -111,9 +109,9 @@ describe GitlabMarkdownHelper do
end
it 'replaces commit message with emoji to link' do
- actual = link_to_gfm(':book:Book', '/foo')
+ actual = link_to_gfm(':book: Book', '/foo')
expect(actual).
- to eq '<gl-emoji data-name="book" data-unicode-version="6.0">📖</gl-emoji><a href="/foo">Book</a>'
+ to eq '<gl-emoji title="open book" data-name="book" data-unicode-version="6.0">📖</gl-emoji><a href="/foo"> Book</a>'
end
end
@@ -128,7 +126,7 @@ describe GitlabMarkdownHelper 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")
+ expect(helper).to receive(:markdown_unsafe).with('wiki content', pipeline: :wiki, project: project, project_wiki: @wiki, page_slug: "nested/page")
helper.render_wiki_content(@wiki)
end
@@ -136,7 +134,7 @@ describe GitlabMarkdownHelper do
it "uses Asciidoctor for asciidoc files" do
allow(@wiki).to receive(:format).and_return(:asciidoc)
- expect(helper).to receive(:asciidoc).with('wiki content')
+ expect(helper).to receive(:asciidoc_unsafe).with('wiki content')
helper.render_wiki_content(@wiki)
end
@@ -151,6 +149,29 @@ describe GitlabMarkdownHelper do
end
end
+ describe 'markup' do
+ let(:content) { 'Noël' }
+
+ it 'preserves encoding' do
+ expect(content.encoding.name).to eq('UTF-8')
+ expect(helper.markup('foo.rst', content).encoding.name).to eq('UTF-8')
+ end
+
+ it "delegates to #markdown_unsafe when file name corresponds to Markdown" do
+ expect(helper).to receive(:gitlab_markdown?).with('foo.md').and_return(true)
+ expect(helper).to receive(:markdown_unsafe).and_return('NOEL')
+
+ expect(helper.markup('foo.md', content)).to eq('NOEL')
+ end
+
+ it "delegates to #asciidoc_unsafe when file name corresponds to AsciiDoc" do
+ expect(helper).to receive(:asciidoc?).with('foo.adoc').and_return(true)
+ expect(helper).to receive(:asciidoc_unsafe).and_return('NOEL')
+
+ expect(helper.markup('foo.adoc', content)).to eq('NOEL')
+ end
+ end
+
describe '#first_line_in_markdown' do
it 'truncates Markdown properly' do
text = "@#{user.username}, can you look at this?\nHello world\n"
diff --git a/spec/helpers/merge_requests_helper_spec.rb b/spec/helpers/merge_requests_helper_spec.rb
index 25f23826648..f2c9d927388 100644
--- a/spec/helpers/merge_requests_helper_spec.rb
+++ b/spec/helpers/merge_requests_helper_spec.rb
@@ -21,28 +21,6 @@ describe MergeRequestsHelper do
end
end
- describe '#issues_sentence' do
- subject { issues_sentence(issues) }
- let(:issues) do
- [build(:issue, iid: 1), build(:issue, iid: 2), build(:issue, iid: 3)]
- end
-
- it { is_expected.to eq('#1, #2, and #3') }
-
- context 'for JIRA issues' do
- let(:project) { create(:empty_project) }
- let(:issues) do
- [
- ExternalIssue.new('JIRA-123', project),
- ExternalIssue.new('JIRA-456', project),
- ExternalIssue.new('FOOBAR-7890', project)
- ]
- end
-
- it { is_expected.to eq('FOOBAR-7890, JIRA-123, and JIRA-456') }
- end
- end
-
describe '#format_mr_branch_names' do
describe 'within the same project' do
let(:merge_request) { create(:merge_request) }
@@ -62,103 +40,4 @@ describe MergeRequestsHelper do
it { is_expected.to eq([source_title, target_title]) }
end
end
-
- describe '#mr_widget_refresh_url' do
- let(:guest) { create(:user) }
- let(:project) { create(:project, :public) }
- let(:project_fork) { Projects::ForkService.new(project, guest).execute }
- let(:merge_request) { create(:merge_request, source_project: project_fork, target_project: project) }
-
- it 'returns correct url for MR' do
- expected_url = "#{project.path_with_namespace}/merge_requests/#{merge_request.iid}/merge_widget_refresh"
-
- expect(mr_widget_refresh_url(merge_request)).to end_with(expected_url)
- end
-
- it 'returns empty string for nil' do
- expect(mr_widget_refresh_url(nil)).to eq('')
- end
- end
-
- describe '#mr_closes_issues' do
- let(:user_1) { create(:user) }
- let(:user_2) { create(:user) }
-
- let(:project_1) { create(:project, :private, creator: user_1, namespace: user_1.namespace) }
- let(:project_2) { create(:project, :private, creator: user_2, namespace: user_2.namespace) }
-
- let(:issue_1) { create(:issue, project: project_1) }
- let(:issue_2) { create(:issue, project: project_2) }
-
- let(:merge_request) { create(:merge_request, source_project: project_1, target_project: project_1,) }
-
- let(:merge_request) do
- create(:merge_request,
- source_project: project_1, target_project: project_1,
- description: "Fixes #{issue_1.to_reference} Fixes #{issue_2.to_reference(project_1)}")
- end
-
- before do
- project_1.team << [user_2, :developer]
- project_2.team << [user_2, :developer]
- allow(merge_request.project).to receive(:default_branch).and_return(merge_request.target_branch)
- @merge_request = merge_request
- end
-
- context 'user without access to another private project' do
- let(:current_user) { user_1 }
-
- it 'cannot see that project\'s issue that will be closed on acceptance' do
- expect(mr_closes_issues).to contain_exactly(issue_1)
- end
- end
-
- context 'user with access to another private project' do
- let(:current_user) { user_2 }
-
- it 'can see that project\'s issue that will be closed on acceptance' do
- expect(mr_closes_issues).to contain_exactly(issue_1, issue_2)
- end
- end
- end
-
- describe '#mr_issues_mentioned_but_not_closing' do
- let(:user_1) { create(:user) }
- let(:user_2) { create(:user) }
-
- let(:project_1) { create(:project, :private, creator: user_1, namespace: user_1.namespace) }
- let(:project_2) { create(:project, :private, creator: user_2, namespace: user_2.namespace) }
-
- let(:issue_1) { create(:issue, project: project_1) }
- let(:issue_2) { create(:issue, project: project_2) }
-
- let(:merge_request) do
- create(:merge_request,
- source_project: project_1, target_project: project_1,
- description: "#{issue_1.to_reference} #{issue_2.to_reference(project_1)}")
- end
-
- before do
- project_1.team << [user_2, :developer]
- project_2.team << [user_2, :developer]
- allow(merge_request.project).to receive(:default_branch).and_return(merge_request.target_branch)
- @merge_request = merge_request
- end
-
- context 'user without access to another private project' do
- let(:current_user) { user_1 }
-
- it 'cannot see that project\'s issue that will be closed on acceptance' do
- expect(mr_issues_mentioned_but_not_closing).to contain_exactly(issue_1)
- end
- end
-
- context 'user with access to another private project' do
- let(:current_user) { user_2 }
-
- it 'can see that project\'s issue that will be closed on acceptance' do
- expect(mr_issues_mentioned_but_not_closing).to contain_exactly(issue_1, issue_2)
- end
- end
- end
end
diff --git a/spec/helpers/notes_helper_spec.rb b/spec/helpers/notes_helper_spec.rb
index a427de32c4c..099146678ae 100644
--- a/spec/helpers/notes_helper_spec.rb
+++ b/spec/helpers/notes_helper_spec.rb
@@ -1,6 +1,8 @@
require "spec_helper"
describe NotesHelper do
+ include RepoHelpers
+
let(:owner) { create(:owner) }
let(:group) { create(:group) }
let(:project) { create(:empty_project, namespace: group) }
@@ -36,4 +38,216 @@ describe NotesHelper do
expect(helper.note_max_access_for_user(other_note)).to eq('Reporter')
end
end
+
+ describe '#discussion_path' do
+ let(:project) { create(:project) }
+
+ context 'for a merge request discusion' do
+ let(:merge_request) { create(:merge_request, source_project: project, target_project: project, importing: true) }
+ let!(:merge_request_diff1) { merge_request.merge_request_diffs.create(head_commit_sha: '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9') }
+ let!(:merge_request_diff2) { merge_request.merge_request_diffs.create(head_commit_sha: nil) }
+ let!(:merge_request_diff3) { merge_request.merge_request_diffs.create(head_commit_sha: '5937ac0a7beb003549fc5fd26fc247adbce4a52e') }
+
+ context 'for a diff discussion' do
+ context 'when the discussion is active' do
+ let(:discussion) { create(:diff_note_on_merge_request, noteable: merge_request, project: project).to_discussion }
+
+ it 'returns the diff path with the line code' do
+ expect(helper.discussion_path(discussion)).to eq(diffs_namespace_project_merge_request_path(project.namespace, project, merge_request, anchor: discussion.line_code))
+ end
+ end
+
+ context 'when the discussion is on an older merge request version' do
+ let(:position) do
+ Gitlab::Diff::Position.new(
+ old_path: ".gitmodules",
+ new_path: ".gitmodules",
+ old_line: nil,
+ new_line: 4,
+ diff_refs: merge_request_diff1.diff_refs
+ )
+ end
+
+ let(:diff_note) { create(:diff_note_on_merge_request, noteable: merge_request, project: project, position: position) }
+ let(:discussion) { diff_note.to_discussion }
+
+ before do
+ diff_note.position = diff_note.original_position
+ diff_note.save!
+ end
+
+ it 'returns the diff version path with the line code' do
+ expect(helper.discussion_path(discussion)).to eq(diffs_namespace_project_merge_request_path(project.namespace, project, merge_request, diff_id: merge_request_diff1, anchor: discussion.line_code))
+ end
+ end
+
+ context 'when the discussion is on a comparison between merge request versions' do
+ let(:position) do
+ Gitlab::Diff::Position.new(
+ old_path: ".gitmodules",
+ new_path: ".gitmodules",
+ old_line: 4,
+ new_line: 4,
+ diff_refs: merge_request_diff3.compare_with(merge_request_diff1.head_commit_sha).diff_refs
+ )
+ end
+
+ let(:discussion) { create(:diff_note_on_merge_request, noteable: merge_request, project: project, position: position).to_discussion }
+
+ it 'returns the diff version comparison path with the line code' do
+ expect(helper.discussion_path(discussion)).to eq(diffs_namespace_project_merge_request_path(project.namespace, project, merge_request, diff_id: merge_request_diff3, start_sha: merge_request_diff1.head_commit_sha, anchor: discussion.line_code))
+ end
+ end
+
+ context 'when the discussion does not have a merge request version' do
+ let(:outdated_diff_note) { create(:diff_note_on_merge_request, noteable: merge_request, project: project, diff_refs: project.commit(sample_commit.id).diff_refs) }
+ let(:discussion) { outdated_diff_note.to_discussion }
+
+ before do
+ outdated_diff_note.position = outdated_diff_note.original_position
+ outdated_diff_note.save!
+ end
+
+ it 'returns nil' do
+ expect(helper.discussion_path(discussion)).to be_nil
+ end
+ end
+ end
+
+ context 'for a legacy diff discussion' do
+ let(:discussion) { create(:legacy_diff_note_on_merge_request, noteable: merge_request, project: project).to_discussion }
+
+ context 'when the discussion is active' do
+ before do
+ allow(discussion).to receive(:active?).and_return(true)
+ end
+
+ it 'returns the diff path with the line code' do
+ expect(helper.discussion_path(discussion)).to eq(diffs_namespace_project_merge_request_path(project.namespace, project, merge_request, anchor: discussion.line_code))
+ end
+ end
+
+ context 'when the discussion is outdated' do
+ before do
+ allow(discussion).to receive(:active?).and_return(false)
+ end
+
+ it 'returns nil' do
+ expect(helper.discussion_path(discussion)).to be_nil
+ end
+ end
+ end
+
+ context 'for a non-diff discussion' do
+ let(:discussion) { create(:discussion_note_on_merge_request, noteable: merge_request, project: project).to_discussion }
+
+ it 'returns nil' do
+ expect(helper.discussion_path(discussion)).to be_nil
+ end
+ end
+ end
+
+ context 'for a commit discussion' do
+ let(:commit) { discussion.noteable }
+
+ context 'for a diff discussion' do
+ let(:discussion) { create(:diff_note_on_commit, project: project).to_discussion }
+
+ it 'returns the commit path with the line code' do
+ expect(helper.discussion_path(discussion)).to eq(namespace_project_commit_path(project.namespace, project, commit, anchor: discussion.line_code))
+ end
+ end
+
+ context 'for a legacy diff discussion' do
+ let(:discussion) { create(:legacy_diff_note_on_commit, project: project).to_discussion }
+
+ it 'returns the commit path with the line code' do
+ expect(helper.discussion_path(discussion)).to eq(namespace_project_commit_path(project.namespace, project, commit, anchor: discussion.line_code))
+ end
+ end
+
+ context 'for a non-diff discussion' do
+ let(:discussion) { create(:discussion_note_on_commit, project: project).to_discussion }
+
+ it 'returns the commit path' do
+ expect(helper.discussion_path(discussion)).to eq(namespace_project_commit_path(project.namespace, project, commit))
+ end
+ end
+ end
+ end
+
+ describe '#notes_url' do
+ it 'return snippet notes path for personal snippet' do
+ @snippet = create(:personal_snippet)
+
+ expect(helper.notes_url).to eq("/snippets/#{@snippet.id}/notes")
+ end
+
+ it 'return project notes path for project snippet' do
+ namespace = create(:namespace, path: 'nm')
+ @project = create(:empty_project, path: 'test', namespace: namespace)
+ @snippet = create(:project_snippet, project: @project)
+ @noteable = @snippet
+
+ expect(helper.notes_url).to eq("/nm/test/noteable/project_snippet/#{@noteable.id}/notes")
+ end
+
+ it 'return project notes path for other noteables' do
+ namespace = create(:namespace, path: 'nm')
+ @project = create(:empty_project, path: 'test', namespace: namespace)
+ @noteable = create(:issue, project: @project)
+
+ expect(helper.notes_url).to eq("/nm/test/noteable/issue/#{@noteable.id}/notes")
+ end
+ end
+
+ describe '#note_url' do
+ it 'return snippet notes path for personal snippet' do
+ note = create(:note_on_personal_snippet)
+
+ expect(helper.note_url(note)).to eq("/snippets/#{note.noteable.id}/notes/#{note.id}")
+ end
+
+ it 'return project notes path for project snippet' do
+ namespace = create(:namespace, path: 'nm')
+ @project = create(:empty_project, path: 'test', namespace: namespace)
+ note = create(:note_on_project_snippet, project: @project)
+
+ expect(helper.note_url(note)).to eq("/nm/test/notes/#{note.id}")
+ end
+
+ it 'return project notes path for other noteables' do
+ namespace = create(:namespace, path: 'nm')
+ @project = create(:empty_project, path: 'test', namespace: namespace)
+ note = create(:note_on_issue, project: @project)
+
+ expect(helper.note_url(note)).to eq("/nm/test/notes/#{note.id}")
+ end
+ end
+
+ describe '#form_resurces' do
+ it 'returns note for personal snippet' do
+ @snippet = create(:personal_snippet)
+ @note = create(:note_on_personal_snippet)
+
+ expect(helper.form_resources).to eq([@note])
+ end
+
+ it 'returns namespace, project and note for project snippet' do
+ namespace = create(:namespace, path: 'nm')
+ @project = create(:empty_project, path: 'test', namespace: namespace)
+ @snippet = create(:project_snippet, project: @project)
+ @note = create(:note_on_personal_snippet)
+
+ expect(helper.form_resources).to eq([@project.namespace, @project, @note])
+ end
+
+ it 'returns namespace, project and note path for other noteables' do
+ namespace = create(:namespace, path: 'nm')
+ @project = create(:empty_project, path: 'test', namespace: namespace)
+ @note = create(:note_on_issue, project: @project)
+
+ expect(helper.form_resources).to eq([@project.namespace, @project, @note])
+ end
+ end
end
diff --git a/spec/helpers/projects_helper_spec.rb b/spec/helpers/projects_helper_spec.rb
index a7fc5d14859..54c5ba57bdf 100644
--- a/spec/helpers/projects_helper_spec.rb
+++ b/spec/helpers/projects_helper_spec.rb
@@ -66,8 +66,8 @@ describe ProjectsHelper do
describe "#project_list_cache_key", redis: true do
let(:project) { create(:project) }
- it "includes the namespace" do
- expect(helper.project_list_cache_key(project)).to include(project.namespace.cache_key)
+ it "includes the route" do
+ expect(helper.project_list_cache_key(project)).to include(project.route.cache_key)
end
it "includes the project" do
@@ -93,7 +93,7 @@ describe ProjectsHelper do
end
it "includes a version" do
- expect(helper.project_list_cache_key(project)).to include("v2.3")
+ expect(helper.project_list_cache_key(project).last).to start_with('v')
end
it "includes the pipeline status when there is a status" do
@@ -103,6 +103,18 @@ describe ProjectsHelper do
end
end
+ describe '#load_pipeline_status' do
+ it 'loads the pipeline status in batch' do
+ project = build(:empty_project)
+
+ helper.load_pipeline_status([project])
+ # Skip lazy loading of the `pipeline_status` attribute
+ pipeline_status = project.instance_variable_get('@pipeline_status')
+
+ expect(pipeline_status).to be_a(Gitlab::Cache::Ci::ProjectPipelineStatus)
+ end
+ end
+
describe 'link_to_member' do
let(:group) { create(:group) }
let(:project) { create(:empty_project, group: group) }
diff --git a/spec/helpers/submodule_helper_spec.rb b/spec/helpers/submodule_helper_spec.rb
index 345bc33a67b..9da33792659 100644
--- a/spec/helpers/submodule_helper_spec.rb
+++ b/spec/helpers/submodule_helper_spec.rb
@@ -109,6 +109,18 @@ describe SubmoduleHelper do
end
context 'submodule on unsupported' do
+ it 'sanitizes unsupported protocols' do
+ stub_url('javascript:alert("XSS");')
+
+ expect(helper.submodule_links(submodule_item)).to eq([nil, nil])
+ end
+
+ it 'sanitizes unsupported protocols disguised as a repository URL' do
+ stub_url('javascript:alert("XSS");foo/bar.git')
+
+ expect(helper.submodule_links(submodule_item)).to eq([nil, nil])
+ end
+
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/javascripts/autosave_spec.js b/spec/javascripts/autosave_spec.js
new file mode 100644
index 00000000000..9f9acc392c2
--- /dev/null
+++ b/spec/javascripts/autosave_spec.js
@@ -0,0 +1,134 @@
+import Autosave from '~/autosave';
+import AccessorUtilities from '~/lib/utils/accessor';
+
+describe('Autosave', () => {
+ let autosave;
+
+ describe('class constructor', () => {
+ const key = 'key';
+ const field = jasmine.createSpyObj('field', ['data', 'on']);
+
+ beforeEach(() => {
+ spyOn(AccessorUtilities, 'isLocalStorageAccessSafe').and.returnValue(true);
+ spyOn(Autosave.prototype, 'restore');
+
+ autosave = new Autosave(field, key);
+ });
+
+ it('should set .isLocalStorageAvailable', () => {
+ expect(AccessorUtilities.isLocalStorageAccessSafe).toHaveBeenCalled();
+ expect(autosave.isLocalStorageAvailable).toBe(true);
+ });
+ });
+
+ describe('restore', () => {
+ const key = 'key';
+ const field = jasmine.createSpyObj('field', ['trigger']);
+
+ beforeEach(() => {
+ autosave = {
+ field,
+ key,
+ };
+
+ spyOn(window.localStorage, 'getItem');
+ });
+
+ describe('if .isLocalStorageAvailable is `false`', () => {
+ beforeEach(() => {
+ autosave.isLocalStorageAvailable = false;
+
+ Autosave.prototype.restore.call(autosave);
+ });
+
+ it('should not call .getItem', () => {
+ expect(window.localStorage.getItem).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('if .isLocalStorageAvailable is `true`', () => {
+ beforeEach(() => {
+ autosave.isLocalStorageAvailable = true;
+
+ Autosave.prototype.restore.call(autosave);
+ });
+
+ it('should call .getItem', () => {
+ expect(window.localStorage.getItem).toHaveBeenCalledWith(key);
+ });
+ });
+ });
+
+ describe('save', () => {
+ const field = jasmine.createSpyObj('field', ['val']);
+
+ beforeEach(() => {
+ autosave = jasmine.createSpyObj('autosave', ['reset']);
+ autosave.field = field;
+
+ field.val.and.returnValue('value');
+
+ spyOn(window.localStorage, 'setItem');
+ });
+
+ describe('if .isLocalStorageAvailable is `false`', () => {
+ beforeEach(() => {
+ autosave.isLocalStorageAvailable = false;
+
+ Autosave.prototype.save.call(autosave);
+ });
+
+ it('should not call .setItem', () => {
+ expect(window.localStorage.setItem).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('if .isLocalStorageAvailable is `true`', () => {
+ beforeEach(() => {
+ autosave.isLocalStorageAvailable = true;
+
+ Autosave.prototype.save.call(autosave);
+ });
+
+ it('should call .setItem', () => {
+ expect(window.localStorage.setItem).toHaveBeenCalled();
+ });
+ });
+ });
+
+ describe('reset', () => {
+ const key = 'key';
+
+ beforeEach(() => {
+ autosave = {
+ key,
+ };
+
+ spyOn(window.localStorage, 'removeItem');
+ });
+
+ describe('if .isLocalStorageAvailable is `false`', () => {
+ beforeEach(() => {
+ autosave.isLocalStorageAvailable = false;
+
+ Autosave.prototype.reset.call(autosave);
+ });
+
+ it('should not call .removeItem', () => {
+ expect(window.localStorage.removeItem).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('if .isLocalStorageAvailable is `true`', () => {
+ beforeEach(() => {
+ autosave.isLocalStorageAvailable = true;
+
+ Autosave.prototype.reset.call(autosave);
+ });
+
+ it('should call .removeItem', () => {
+ expect(window.localStorage.removeItem).toHaveBeenCalledWith(key);
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/behaviors/bind_in_out_spec.js b/spec/javascripts/behaviors/bind_in_out_spec.js
index dd9ab33289f..5ff66167718 100644
--- a/spec/javascripts/behaviors/bind_in_out_spec.js
+++ b/spec/javascripts/behaviors/bind_in_out_spec.js
@@ -2,7 +2,7 @@ import BindInOut from '~/behaviors/bind_in_out';
import ClassSpecHelper from '../helpers/class_spec_helper';
describe('BindInOut', function () {
- describe('.constructor', function () {
+ describe('constructor', function () {
beforeEach(function () {
this.in = {};
this.out = {};
@@ -53,7 +53,7 @@ describe('BindInOut', function () {
});
});
- describe('.addEvents', function () {
+ describe('addEvents', function () {
beforeEach(function () {
this.in = jasmine.createSpyObj('in', ['addEventListener']);
@@ -79,7 +79,7 @@ describe('BindInOut', function () {
});
});
- describe('.updateOut', function () {
+ describe('updateOut', function () {
beforeEach(function () {
this.in = { value: 'the-value' };
this.out = { textContent: 'not-the-value' };
@@ -98,7 +98,7 @@ describe('BindInOut', function () {
});
});
- describe('.removeEvents', function () {
+ describe('removeEvents', function () {
beforeEach(function () {
this.in = jasmine.createSpyObj('in', ['removeEventListener']);
this.updateOut = () => {};
@@ -122,7 +122,7 @@ describe('BindInOut', function () {
});
});
- describe('.initAll', function () {
+ describe('initAll', function () {
beforeEach(function () {
this.ins = [0, 1, 2];
this.instances = [];
@@ -153,7 +153,7 @@ describe('BindInOut', function () {
});
});
- describe('.init', function () {
+ describe('init', function () {
beforeEach(function () {
spyOn(BindInOut.prototype, 'addEvents').and.callFake(function () { return this; });
spyOn(BindInOut.prototype, 'updateOut').and.callFake(function () { return this; });
diff --git a/spec/javascripts/behaviors/gl_emoji/unicode_support_map_spec.js b/spec/javascripts/behaviors/gl_emoji/unicode_support_map_spec.js
new file mode 100644
index 00000000000..1ed96a67478
--- /dev/null
+++ b/spec/javascripts/behaviors/gl_emoji/unicode_support_map_spec.js
@@ -0,0 +1,47 @@
+import { getUnicodeSupportMap } from '~/behaviors/gl_emoji/unicode_support_map';
+import AccessorUtilities from '~/lib/utils/accessor';
+
+describe('Unicode Support Map', () => {
+ describe('getUnicodeSupportMap', () => {
+ const stringSupportMap = 'stringSupportMap';
+
+ beforeEach(() => {
+ spyOn(AccessorUtilities, 'isLocalStorageAccessSafe');
+ spyOn(window.localStorage, 'getItem');
+ spyOn(window.localStorage, 'setItem');
+ spyOn(JSON, 'parse');
+ spyOn(JSON, 'stringify').and.returnValue(stringSupportMap);
+ });
+
+ describe('if isLocalStorageAvailable is `true`', function () {
+ beforeEach(() => {
+ AccessorUtilities.isLocalStorageAccessSafe.and.returnValue(true);
+
+ getUnicodeSupportMap();
+ });
+
+ it('should call .getItem and .setItem', () => {
+ const allArgs = window.localStorage.setItem.calls.allArgs();
+
+ expect(window.localStorage.getItem).toHaveBeenCalledWith('gl-emoji-user-agent');
+ expect(allArgs[0][0]).toBe('gl-emoji-user-agent');
+ expect(allArgs[0][1]).toBe(navigator.userAgent);
+ expect(allArgs[1][0]).toBe('gl-emoji-unicode-support-map');
+ expect(allArgs[1][1]).toBe(stringSupportMap);
+ });
+ });
+
+ describe('if isLocalStorageAvailable is `false`', function () {
+ beforeEach(() => {
+ AccessorUtilities.isLocalStorageAccessSafe.and.returnValue(false);
+
+ getUnicodeSupportMap();
+ });
+
+ it('should not call .getItem or .setItem', () => {
+ expect(window.localStorage.getItem.calls.count()).toBe(1);
+ expect(window.localStorage.setItem).not.toHaveBeenCalled();
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/blob/balsamiq/balsamiq_viewer_integration_spec.js b/spec/javascripts/blob/balsamiq/balsamiq_viewer_integration_spec.js
new file mode 100644
index 00000000000..acd0aaf2a86
--- /dev/null
+++ b/spec/javascripts/blob/balsamiq/balsamiq_viewer_integration_spec.js
@@ -0,0 +1,51 @@
+/* eslint-disable import/no-unresolved */
+
+import BalsamiqViewer from '~/blob/balsamiq/balsamiq_viewer';
+import bmprPath from '../../fixtures/blob/balsamiq/test.bmpr';
+
+describe('Balsamiq integration spec', () => {
+ let container;
+ let endpoint;
+ let balsamiqViewer;
+
+ preloadFixtures('static/balsamiq_viewer.html.raw');
+
+ beforeEach(() => {
+ loadFixtures('static/balsamiq_viewer.html.raw');
+
+ container = document.getElementById('js-balsamiq-viewer');
+ balsamiqViewer = new BalsamiqViewer(container);
+ });
+
+ describe('successful response', () => {
+ beforeEach((done) => {
+ endpoint = bmprPath;
+
+ balsamiqViewer.loadFile(endpoint).then(done).catch(done.fail);
+ });
+
+ it('does not show loading icon', () => {
+ expect(document.querySelector('.loading')).toBeNull();
+ });
+
+ it('renders the balsamiq previews', () => {
+ expect(document.querySelectorAll('.previews .preview').length).not.toEqual(0);
+ });
+ });
+
+ describe('error getting file', () => {
+ beforeEach((done) => {
+ endpoint = 'invalid/path/to/file.bmpr';
+
+ balsamiqViewer.loadFile(endpoint).then(done.fail, null).catch(done);
+ });
+
+ it('does not show loading icon', () => {
+ expect(document.querySelector('.loading')).toBeNull();
+ });
+
+ it('does not render the balsamiq previews', () => {
+ expect(document.querySelectorAll('.previews .preview').length).toEqual(0);
+ });
+ });
+});
diff --git a/spec/javascripts/blob/balsamiq/balsamiq_viewer_spec.js b/spec/javascripts/blob/balsamiq/balsamiq_viewer_spec.js
new file mode 100644
index 00000000000..aa87956109f
--- /dev/null
+++ b/spec/javascripts/blob/balsamiq/balsamiq_viewer_spec.js
@@ -0,0 +1,326 @@
+import sqljs from 'sql.js';
+import BalsamiqViewer from '~/blob/balsamiq/balsamiq_viewer';
+import ClassSpecHelper from '../../helpers/class_spec_helper';
+
+describe('BalsamiqViewer', () => {
+ let balsamiqViewer;
+ let viewer;
+
+ describe('class constructor', () => {
+ beforeEach(() => {
+ viewer = {};
+
+ balsamiqViewer = new BalsamiqViewer(viewer);
+ });
+
+ it('should set .viewer', () => {
+ expect(balsamiqViewer.viewer).toBe(viewer);
+ });
+ });
+
+ describe('fileLoaded', () => {
+
+ });
+
+ describe('loadFile', () => {
+ let xhr;
+ let loadFile;
+ const endpoint = 'endpoint';
+
+ beforeEach(() => {
+ xhr = jasmine.createSpyObj('xhr', ['open', 'send']);
+
+ balsamiqViewer = jasmine.createSpyObj('balsamiqViewer', ['renderFile']);
+
+ spyOn(window, 'XMLHttpRequest').and.returnValue(xhr);
+
+ loadFile = BalsamiqViewer.prototype.loadFile.call(balsamiqViewer, endpoint);
+ });
+
+ it('should call .open', () => {
+ expect(xhr.open).toHaveBeenCalledWith('GET', endpoint, true);
+ });
+
+ it('should set .responseType', () => {
+ expect(xhr.responseType).toBe('arraybuffer');
+ });
+
+ it('should call .send', () => {
+ expect(xhr.send).toHaveBeenCalled();
+ });
+
+ it('should return a promise', () => {
+ expect(loadFile).toEqual(jasmine.any(Promise));
+ });
+ });
+
+ describe('renderFile', () => {
+ let container;
+ let loadEvent;
+ let previews;
+
+ beforeEach(() => {
+ loadEvent = { target: { response: {} } };
+ viewer = jasmine.createSpyObj('viewer', ['appendChild']);
+ previews = [document.createElement('ul'), document.createElement('ul')];
+
+ balsamiqViewer = jasmine.createSpyObj('balsamiqViewer', ['initDatabase', 'getPreviews', 'renderPreview']);
+ balsamiqViewer.viewer = viewer;
+
+ balsamiqViewer.getPreviews.and.returnValue(previews);
+ balsamiqViewer.renderPreview.and.callFake(preview => preview);
+ viewer.appendChild.and.callFake((containerElement) => {
+ container = containerElement;
+ });
+
+ BalsamiqViewer.prototype.renderFile.call(balsamiqViewer, loadEvent);
+ });
+
+ it('should call .initDatabase', () => {
+ expect(balsamiqViewer.initDatabase).toHaveBeenCalledWith(loadEvent.target.response);
+ });
+
+ it('should call .getPreviews', () => {
+ expect(balsamiqViewer.getPreviews).toHaveBeenCalled();
+ });
+
+ it('should call .renderPreview for each preview', () => {
+ const allArgs = balsamiqViewer.renderPreview.calls.allArgs();
+
+ expect(allArgs.length).toBe(2);
+
+ previews.forEach((preview, i) => {
+ expect(allArgs[i][0]).toBe(preview);
+ });
+ });
+
+ it('should set the container HTML', () => {
+ expect(container.innerHTML).toBe('<ul></ul><ul></ul>');
+ });
+
+ it('should add inline preview classes', () => {
+ expect(container.classList[0]).toBe('list-inline');
+ expect(container.classList[1]).toBe('previews');
+ });
+
+ it('should call viewer.appendChild', () => {
+ expect(viewer.appendChild).toHaveBeenCalledWith(container);
+ });
+ });
+
+ describe('initDatabase', () => {
+ let database;
+ let uint8Array;
+ let data;
+
+ beforeEach(() => {
+ uint8Array = {};
+ database = {};
+ data = 'data';
+
+ balsamiqViewer = {};
+
+ spyOn(window, 'Uint8Array').and.returnValue(uint8Array);
+ spyOn(sqljs, 'Database').and.returnValue(database);
+
+ BalsamiqViewer.prototype.initDatabase.call(balsamiqViewer, data);
+ });
+
+ it('should instantiate Uint8Array', () => {
+ expect(window.Uint8Array).toHaveBeenCalledWith(data);
+ });
+
+ it('should call sqljs.Database', () => {
+ expect(sqljs.Database).toHaveBeenCalledWith(uint8Array);
+ });
+
+ it('should set .database', () => {
+ expect(balsamiqViewer.database).toBe(database);
+ });
+ });
+
+ describe('getPreviews', () => {
+ let database;
+ let thumbnails;
+ let getPreviews;
+
+ beforeEach(() => {
+ database = jasmine.createSpyObj('database', ['exec']);
+ thumbnails = [{ values: [0, 1, 2] }];
+
+ balsamiqViewer = {
+ database,
+ };
+
+ spyOn(BalsamiqViewer, 'parsePreview').and.callFake(preview => preview.toString());
+ database.exec.and.returnValue(thumbnails);
+
+ getPreviews = BalsamiqViewer.prototype.getPreviews.call(balsamiqViewer);
+ });
+
+ it('should call database.exec', () => {
+ expect(database.exec).toHaveBeenCalledWith('SELECT * FROM thumbnails');
+ });
+
+ it('should call .parsePreview for each value', () => {
+ const allArgs = BalsamiqViewer.parsePreview.calls.allArgs();
+
+ expect(allArgs.length).toBe(3);
+
+ thumbnails[0].values.forEach((value, i) => {
+ expect(allArgs[i][0]).toBe(value);
+ });
+ });
+
+ it('should return an array of parsed values', () => {
+ expect(getPreviews).toEqual(['0', '1', '2']);
+ });
+ });
+
+ describe('getResource', () => {
+ let database;
+ let resourceID;
+ let resource;
+ let getResource;
+
+ beforeEach(() => {
+ database = jasmine.createSpyObj('database', ['exec']);
+ resourceID = 4;
+ resource = ['resource'];
+
+ balsamiqViewer = {
+ database,
+ };
+
+ database.exec.and.returnValue(resource);
+
+ getResource = BalsamiqViewer.prototype.getResource.call(balsamiqViewer, resourceID);
+ });
+
+ it('should call database.exec', () => {
+ expect(database.exec).toHaveBeenCalledWith(`SELECT * FROM resources WHERE id = '${resourceID}'`);
+ });
+
+ it('should return the selected resource', () => {
+ expect(getResource).toBe(resource[0]);
+ });
+ });
+
+ describe('renderPreview', () => {
+ let previewElement;
+ let innerHTML;
+ let preview;
+ let renderPreview;
+
+ beforeEach(() => {
+ innerHTML = '<a>innerHTML</a>';
+ previewElement = {
+ outerHTML: '<p>outerHTML</p>',
+ classList: jasmine.createSpyObj('classList', ['add']),
+ };
+ preview = {};
+
+ balsamiqViewer = jasmine.createSpyObj('balsamiqViewer', ['renderTemplate']);
+
+ spyOn(document, 'createElement').and.returnValue(previewElement);
+ balsamiqViewer.renderTemplate.and.returnValue(innerHTML);
+
+ renderPreview = BalsamiqViewer.prototype.renderPreview.call(balsamiqViewer, preview);
+ });
+
+ it('should call classList.add', () => {
+ expect(previewElement.classList.add).toHaveBeenCalledWith('preview');
+ });
+
+ it('should call .renderTemplate', () => {
+ expect(balsamiqViewer.renderTemplate).toHaveBeenCalledWith(preview);
+ });
+
+ it('should set .innerHTML', () => {
+ expect(previewElement.innerHTML).toBe(innerHTML);
+ });
+
+ it('should return element', () => {
+ expect(renderPreview).toBe(previewElement);
+ });
+ });
+
+ describe('renderTemplate', () => {
+ let preview;
+ let name;
+ let resource;
+ let template;
+ let renderTemplate;
+
+ beforeEach(() => {
+ preview = { resourceID: 1, image: 'image' };
+ name = 'name';
+ resource = 'resource';
+ template = `
+ <div class="panel panel-default">
+ <div class="panel-heading">name</div>
+ <div class="panel-body">
+ <img class="img-thumbnail" src="data:image/png;base64,image"/>
+ </div>
+ </div>
+ `;
+
+ balsamiqViewer = jasmine.createSpyObj('balsamiqViewer', ['getResource']);
+
+ spyOn(BalsamiqViewer, 'parseTitle').and.returnValue(name);
+ balsamiqViewer.getResource.and.returnValue(resource);
+
+ renderTemplate = BalsamiqViewer.prototype.renderTemplate.call(balsamiqViewer, preview);
+ });
+
+ it('should call .getResource', () => {
+ expect(balsamiqViewer.getResource).toHaveBeenCalledWith(preview.resourceID);
+ });
+
+ it('should call .parseTitle', () => {
+ expect(BalsamiqViewer.parseTitle).toHaveBeenCalledWith(resource);
+ });
+
+ it('should return the template string', function () {
+ expect(renderTemplate.replace(/\s/g, '')).toEqual(template.replace(/\s/g, ''));
+ });
+ });
+
+ describe('parsePreview', () => {
+ let preview;
+ let parsePreview;
+
+ beforeEach(() => {
+ preview = ['{}', '{ "id": 1 }'];
+
+ spyOn(JSON, 'parse').and.callThrough();
+
+ parsePreview = BalsamiqViewer.parsePreview(preview);
+ });
+
+ ClassSpecHelper.itShouldBeAStaticMethod(BalsamiqViewer, 'parsePreview');
+
+ it('should return the parsed JSON', () => {
+ expect(parsePreview).toEqual(JSON.parse('{ "id": 1 }'));
+ });
+ });
+
+ describe('parseTitle', () => {
+ let title;
+ let parseTitle;
+
+ beforeEach(() => {
+ title = { values: [['{}', '{}', '{"name":"name"}']] };
+
+ spyOn(JSON, 'parse').and.callThrough();
+
+ parseTitle = BalsamiqViewer.parseTitle(title);
+ });
+
+ ClassSpecHelper.itShouldBeAStaticMethod(BalsamiqViewer, 'parsePreview');
+
+ it('should return the name value', () => {
+ expect(parseTitle).toBe('name');
+ });
+ });
+});
diff --git a/spec/javascripts/blob/blob_fork_suggestion_spec.js b/spec/javascripts/blob/blob_fork_suggestion_spec.js
index d0d64d75957..d1ab0a32f85 100644
--- a/spec/javascripts/blob/blob_fork_suggestion_spec.js
+++ b/spec/javascripts/blob/blob_fork_suggestion_spec.js
@@ -3,20 +3,21 @@ import BlobForkSuggestion from '~/blob/blob_fork_suggestion';
describe('BlobForkSuggestion', () => {
let blobForkSuggestion;
- const openButtons = [document.createElement('div')];
- const forkButtons = [document.createElement('a')];
- const cancelButtons = [document.createElement('div')];
- const suggestionSections = [document.createElement('div')];
- const actionTextPieces = [document.createElement('div')];
+ const openButton = document.createElement('div');
+ const forkButton = document.createElement('a');
+ const cancelButton = document.createElement('div');
+ const suggestionSection = document.createElement('div');
+ const actionTextPiece = document.createElement('div');
beforeEach(() => {
blobForkSuggestion = new BlobForkSuggestion({
- openButtons,
- forkButtons,
- cancelButtons,
- suggestionSections,
- actionTextPieces,
- });
+ openButtons: openButton,
+ forkButtons: forkButton,
+ cancelButtons: cancelButton,
+ suggestionSections: suggestionSection,
+ actionTextPieces: actionTextPiece,
+ })
+ .init();
});
afterEach(() => {
@@ -25,13 +26,13 @@ describe('BlobForkSuggestion', () => {
it('showSuggestionSection', () => {
blobForkSuggestion.showSuggestionSection('/foo', 'foo');
- expect(suggestionSections[0].classList.contains('hidden')).toEqual(false);
- expect(forkButtons[0].getAttribute('href')).toEqual('/foo');
- expect(actionTextPieces[0].textContent).toEqual('foo');
+ expect(suggestionSection.classList.contains('hidden')).toEqual(false);
+ expect(forkButton.getAttribute('href')).toEqual('/foo');
+ expect(actionTextPiece.textContent).toEqual('foo');
});
it('hideSuggestionSection', () => {
blobForkSuggestion.hideSuggestionSection();
- expect(suggestionSections[0].classList.contains('hidden')).toEqual(true);
+ expect(suggestionSection.classList.contains('hidden')).toEqual(true);
});
});
diff --git a/spec/javascripts/blob/create_branch_dropdown_spec.js b/spec/javascripts/blob/create_branch_dropdown_spec.js
index c1179e572ae..9f0d373cb81 100644
--- a/spec/javascripts/blob/create_branch_dropdown_spec.js
+++ b/spec/javascripts/blob/create_branch_dropdown_spec.js
@@ -1,5 +1,4 @@
require('~/gl_dropdown');
-require('~/lib/utils/type_utility');
require('~/blob/create_branch_dropdown');
require('~/blob/target_branch_dropdown');
diff --git a/spec/javascripts/blob/pdf/index_spec.js b/spec/javascripts/blob/pdf/index_spec.js
index d3a4d04345b..bbeaf95e68d 100644
--- a/spec/javascripts/blob/pdf/index_spec.js
+++ b/spec/javascripts/blob/pdf/index_spec.js
@@ -1,5 +1,7 @@
+/* eslint-disable import/no-unresolved */
+
import renderPDF from '~/blob/pdf';
-import testPDF from './test.pdf';
+import testPDF from '../../fixtures/blob/pdf/test.pdf';
describe('PDF renderer', () => {
let viewer;
@@ -59,7 +61,7 @@ describe('PDF renderer', () => {
describe('error getting file', () => {
beforeEach((done) => {
- viewer.dataset.endpoint = 'invalid/endpoint';
+ viewer.dataset.endpoint = 'invalid/path/to/file.pdf';
app = renderPDF();
checkLoaded(done);
diff --git a/spec/javascripts/blob/pdf/test.pdf b/spec/javascripts/blob/pdf/test.pdf
deleted file mode 100644
index eb3d147fde3..00000000000
--- a/spec/javascripts/blob/pdf/test.pdf
+++ /dev/null
Binary files differ
diff --git a/spec/javascripts/blob/target_branch_dropdown_spec.js b/spec/javascripts/blob/target_branch_dropdown_spec.js
index 4fb79663c51..76ed3dc1a2d 100644
--- a/spec/javascripts/blob/target_branch_dropdown_spec.js
+++ b/spec/javascripts/blob/target_branch_dropdown_spec.js
@@ -1,5 +1,4 @@
require('~/gl_dropdown');
-require('~/lib/utils/type_utility');
require('~/blob/create_branch_dropdown');
require('~/blob/target_branch_dropdown');
@@ -63,7 +62,7 @@ describe('TargetBranchDropdown', () => {
expect('change.branch').toHaveBeenTriggeredOn(dropdown.$dropdown);
});
- describe('#dropdownData', () => {
+ describe('dropdownData', () => {
it('cache the refs', () => {
const refs = dropdown.cachedRefs;
dropdown.cachedRefs = null;
@@ -88,7 +87,7 @@ describe('TargetBranchDropdown', () => {
});
});
- describe('#setNewBranch', () => {
+ describe('setNewBranch', () => {
it('adds the new branch and select it', () => {
const branchName = 'new_branch';
diff --git a/spec/javascripts/blob/viewer/index_spec.js b/spec/javascripts/blob/viewer/index_spec.js
new file mode 100644
index 00000000000..13f122b68b2
--- /dev/null
+++ b/spec/javascripts/blob/viewer/index_spec.js
@@ -0,0 +1,161 @@
+/* eslint-disable no-new */
+import BlobViewer from '~/blob/viewer/index';
+
+describe('Blob viewer', () => {
+ let blob;
+ preloadFixtures('blob/show.html.raw');
+
+ beforeEach(() => {
+ loadFixtures('blob/show.html.raw');
+ $('#modal-upload-blob').remove();
+
+ blob = new BlobViewer();
+
+ spyOn($, 'ajax').and.callFake(() => {
+ const d = $.Deferred();
+
+ d.resolve({
+ html: '<div>testing</div>',
+ });
+
+ return d.promise();
+ });
+ });
+
+ afterEach(() => {
+ location.hash = '';
+ });
+
+ it('loads source file after switching views', (done) => {
+ document.querySelector('.js-blob-viewer-switch-btn[data-viewer="simple"]').click();
+
+ setTimeout(() => {
+ expect($.ajax).toHaveBeenCalled();
+ expect(
+ document.querySelector('.js-blob-viewer-switch-btn[data-viewer="simple"]')
+ .classList.contains('hidden'),
+ ).toBeFalsy();
+
+ done();
+ });
+ });
+
+ it('loads source file when line number is in hash', (done) => {
+ location.hash = '#L1';
+
+ new BlobViewer();
+
+ setTimeout(() => {
+ expect($.ajax).toHaveBeenCalled();
+ expect(
+ document.querySelector('.js-blob-viewer-switch-btn[data-viewer="simple"]')
+ .classList.contains('hidden'),
+ ).toBeFalsy();
+
+ done();
+ });
+ });
+
+ it('doesnt reload file if already loaded', (done) => {
+ const asyncClick = () => new Promise((resolve) => {
+ document.querySelector('.js-blob-viewer-switch-btn[data-viewer="simple"]').click();
+
+ setTimeout(resolve);
+ });
+
+ asyncClick()
+ .then(() => {
+ expect($.ajax).toHaveBeenCalled();
+ return asyncClick();
+ })
+ .then(() => {
+ expect($.ajax.calls.count()).toBe(1);
+ expect(
+ document.querySelector('.blob-viewer[data-type="simple"]').getAttribute('data-loaded'),
+ ).toBe('true');
+
+ done();
+ })
+ .catch(() => {
+ fail();
+ done();
+ });
+ });
+
+ describe('copy blob button', () => {
+ it('disabled on load', () => {
+ expect(
+ document.querySelector('.js-copy-blob-source-btn').classList.contains('disabled'),
+ ).toBeTruthy();
+ });
+
+ it('has tooltip when disabled', () => {
+ expect(
+ document.querySelector('.js-copy-blob-source-btn').getAttribute('data-original-title'),
+ ).toBe('Switch to the source to copy it to the clipboard');
+ });
+
+ it('enables after switching to simple view', (done) => {
+ document.querySelector('.js-blob-viewer-switch-btn[data-viewer="simple"]').click();
+
+ setTimeout(() => {
+ expect($.ajax).toHaveBeenCalled();
+ expect(
+ document.querySelector('.js-copy-blob-source-btn').classList.contains('disabled'),
+ ).toBeFalsy();
+
+ done();
+ });
+ });
+
+ it('updates tooltip after switching to simple view', (done) => {
+ document.querySelector('.js-blob-viewer-switch-btn[data-viewer="simple"]').click();
+
+ setTimeout(() => {
+ expect($.ajax).toHaveBeenCalled();
+
+ expect(
+ document.querySelector('.js-copy-blob-source-btn').getAttribute('data-original-title'),
+ ).toBe('Copy source to clipboard');
+
+ done();
+ });
+ });
+ });
+
+ describe('switchToViewer', () => {
+ it('removes active class from old viewer button', () => {
+ blob.switchToViewer('simple');
+
+ expect(
+ document.querySelector('.js-blob-viewer-switch-btn.active[data-viewer="rich"]'),
+ ).toBeNull();
+ });
+
+ it('adds active class to new viewer button', () => {
+ const simpleBtn = document.querySelector('.js-blob-viewer-switch-btn[data-viewer="simple"]');
+
+ spyOn(simpleBtn, 'blur');
+
+ blob.switchToViewer('simple');
+
+ expect(
+ simpleBtn.classList.contains('active'),
+ ).toBeTruthy();
+ expect(simpleBtn.blur).toHaveBeenCalled();
+ });
+
+ it('sends AJAX request when switching to simple view', () => {
+ blob.switchToViewer('simple');
+
+ expect($.ajax).toHaveBeenCalled();
+ });
+
+ it('does not send AJAX request when switching to rich view', () => {
+ blob.switchToViewer('simple');
+ blob.switchToViewer('rich');
+
+ expect($.ajax.calls.count()).toBe(1);
+ });
+ });
+});
diff --git a/spec/javascripts/boards/board_card_spec.js b/spec/javascripts/boards/board_card_spec.js
index de072e7e470..376e706d1db 100644
--- a/spec/javascripts/boards/board_card_spec.js
+++ b/spec/javascripts/boards/board_card_spec.js
@@ -1,12 +1,12 @@
/* global List */
-/* global ListUser */
+/* global ListAssignee */
/* global ListLabel */
/* global listObj */
/* global boardsMockInterceptor */
/* global BoardService */
import Vue from 'vue';
-import '~/boards/models/user';
+import '~/boards/models/assignee';
require('~/boards/models/list');
require('~/boards/models/label');
@@ -133,12 +133,12 @@ describe('Issue card', () => {
});
it('does not set detail issue if img is clicked', (done) => {
- vm.issue.assignee = new ListUser({
+ vm.issue.assignees = [new ListAssignee({
id: 1,
name: 'testing 123',
username: 'test',
avatar: 'test_image',
- });
+ })];
Vue.nextTick(() => {
triggerEvent('mouseup', vm.$el.querySelector('img'));
diff --git a/spec/javascripts/boards/board_list_spec.js b/spec/javascripts/boards/board_list_spec.js
index 3f598887603..a89be911667 100644
--- a/spec/javascripts/boards/board_list_spec.js
+++ b/spec/javascripts/boards/board_list_spec.js
@@ -35,6 +35,7 @@ describe('Board list component', () => {
iid: 1,
confidential: false,
labels: [],
+ assignees: [],
});
list.issuesSize = 1;
list.issues.push(issue);
diff --git a/spec/javascripts/boards/boards_store_spec.js b/spec/javascripts/boards/boards_store_spec.js
index b55ff2f473a..5ea160b7790 100644
--- a/spec/javascripts/boards/boards_store_spec.js
+++ b/spec/javascripts/boards/boards_store_spec.js
@@ -8,14 +8,14 @@
import Vue from 'vue';
import Cookies from 'js-cookie';
-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');
+import '~/lib/utils/url_utility';
+import '~/boards/models/issue';
+import '~/boards/models/label';
+import '~/boards/models/list';
+import '~/boards/models/assignee';
+import '~/boards/services/board_service';
+import '~/boards/stores/boards_store';
+import './mock_data';
describe('Store', () => {
beforeEach(() => {
@@ -212,7 +212,8 @@ describe('Store', () => {
title: 'Testing',
iid: 2,
confidential: false,
- labels: []
+ labels: [],
+ assignees: [],
});
const list = gl.issueBoards.BoardsStore.addList(listObj);
diff --git a/spec/javascripts/boards/issue_card_spec.js b/spec/javascripts/boards/issue_card_spec.js
index 1a5e9e9fd07..fddde799d01 100644
--- a/spec/javascripts/boards/issue_card_spec.js
+++ b/spec/javascripts/boards/issue_card_spec.js
@@ -1,20 +1,20 @@
-/* global ListUser */
+/* global ListAssignee */
/* global ListLabel */
/* global listObj */
/* global ListIssue */
import Vue from 'vue';
-require('~/boards/models/issue');
-require('~/boards/models/label');
-require('~/boards/models/list');
-require('~/boards/models/user');
-require('~/boards/stores/boards_store');
-require('~/boards/components/issue_card_inner');
-require('./mock_data');
+import '~/boards/models/issue';
+import '~/boards/models/label';
+import '~/boards/models/list';
+import '~/boards/models/assignee';
+import '~/boards/stores/boards_store';
+import '~/boards/components/issue_card_inner';
+import './mock_data';
describe('Issue card component', () => {
- const user = new ListUser({
+ const user = new ListAssignee({
id: 1,
name: 'testing 123',
username: 'test',
@@ -40,6 +40,7 @@ describe('Issue card component', () => {
iid: 1,
confidential: false,
labels: [list.label],
+ assignees: [],
});
component = new Vue({
@@ -92,12 +93,12 @@ describe('Issue card component', () => {
it('renders confidential icon', (done) => {
component.issue.confidential = true;
- setTimeout(() => {
+ Vue.nextTick(() => {
expect(
component.$el.querySelector('.confidential-icon'),
).not.toBeNull();
done();
- }, 0);
+ });
});
it('renders issue ID with #', () => {
@@ -109,34 +110,32 @@ describe('Issue card component', () => {
describe('assignee', () => {
it('does not render assignee', () => {
expect(
- component.$el.querySelector('.card-assignee'),
+ component.$el.querySelector('.card-assignee .avatar'),
).toBeNull();
});
describe('exists', () => {
beforeEach((done) => {
- component.issue.assignee = user;
+ component.issue.assignees = [user];
- setTimeout(() => {
- done();
- }, 0);
+ Vue.nextTick(() => done());
});
it('renders assignee', () => {
expect(
- component.$el.querySelector('.card-assignee'),
+ component.$el.querySelector('.card-assignee .avatar'),
).not.toBeNull();
});
it('sets title', () => {
expect(
- component.$el.querySelector('.card-assignee').getAttribute('title'),
+ component.$el.querySelector('.card-assignee a').getAttribute('title'),
).toContain(`Assigned to ${user.name}`);
});
it('sets users path', () => {
expect(
- component.$el.querySelector('.card-assignee').getAttribute('href'),
+ component.$el.querySelector('.card-assignee a').getAttribute('href'),
).toBe('/test');
});
@@ -146,6 +145,96 @@ describe('Issue card component', () => {
).not.toBeNull();
});
});
+
+ describe('assignee default avatar', () => {
+ beforeEach((done) => {
+ component.issue.assignees = [new ListAssignee({
+ id: 1,
+ name: 'testing 123',
+ username: 'test',
+ }, 'default_avatar')];
+
+ Vue.nextTick(done);
+ });
+
+ it('displays defaults avatar if users avatar is null', () => {
+ expect(
+ component.$el.querySelector('.card-assignee img'),
+ ).not.toBeNull();
+ expect(
+ component.$el.querySelector('.card-assignee img').getAttribute('src'),
+ ).toBe('default_avatar');
+ });
+ });
+ });
+
+ describe('multiple assignees', () => {
+ beforeEach((done) => {
+ component.issue.assignees = [
+ user,
+ new ListAssignee({
+ id: 2,
+ name: 'user2',
+ username: 'user2',
+ avatar: 'test_image',
+ }),
+ new ListAssignee({
+ id: 3,
+ name: 'user3',
+ username: 'user3',
+ avatar: 'test_image',
+ }),
+ new ListAssignee({
+ id: 4,
+ name: 'user4',
+ username: 'user4',
+ avatar: 'test_image',
+ })];
+
+ Vue.nextTick(() => done());
+ });
+
+ it('renders all four assignees', () => {
+ expect(component.$el.querySelectorAll('.card-assignee .avatar').length).toEqual(4);
+ });
+
+ describe('more than four assignees', () => {
+ beforeEach((done) => {
+ component.issue.assignees.push(new ListAssignee({
+ id: 5,
+ name: 'user5',
+ username: 'user5',
+ avatar: 'test_image',
+ }));
+
+ Vue.nextTick(() => done());
+ });
+
+ it('renders more avatar counter', () => {
+ expect(component.$el.querySelector('.card-assignee .avatar-counter').innerText).toEqual('+2');
+ });
+
+ it('renders three assignees', () => {
+ expect(component.$el.querySelectorAll('.card-assignee .avatar').length).toEqual(3);
+ });
+
+ it('renders 99+ avatar counter', (done) => {
+ for (let i = 5; i < 104; i += 1) {
+ const u = new ListAssignee({
+ id: i,
+ name: 'name',
+ username: 'username',
+ avatar: 'test_image',
+ });
+ component.issue.assignees.push(u);
+ }
+
+ Vue.nextTick(() => {
+ expect(component.$el.querySelector('.card-assignee .avatar-counter').innerText).toEqual('99+');
+ done();
+ });
+ });
+ });
});
describe('labels', () => {
@@ -159,9 +248,7 @@ describe('Issue card component', () => {
beforeEach((done) => {
component.issue.addLabel(label1);
- setTimeout(() => {
- done();
- }, 0);
+ Vue.nextTick(() => done());
});
it('does not render list label', () => {
diff --git a/spec/javascripts/boards/issue_spec.js b/spec/javascripts/boards/issue_spec.js
index c96dfe94a4a..cd1497bc5e6 100644
--- a/spec/javascripts/boards/issue_spec.js
+++ b/spec/javascripts/boards/issue_spec.js
@@ -2,14 +2,15 @@
/* global BoardService */
/* global ListIssue */
-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');
+import Vue from 'vue';
+import '~/lib/utils/url_utility';
+import '~/boards/models/issue';
+import '~/boards/models/label';
+import '~/boards/models/list';
+import '~/boards/models/assignee';
+import '~/boards/services/board_service';
+import '~/boards/stores/boards_store';
+import './mock_data';
describe('Issue model', () => {
let issue;
@@ -27,7 +28,13 @@ describe('Issue model', () => {
title: 'test',
color: 'red',
description: 'testing'
- }]
+ }],
+ assignees: [{
+ id: 1,
+ name: 'name',
+ username: 'username',
+ avatar_url: 'http://avatar_url',
+ }],
});
});
@@ -80,6 +87,33 @@ describe('Issue model', () => {
expect(issue.labels.length).toBe(0);
});
+ it('adds assignee', () => {
+ issue.addAssignee({
+ id: 2,
+ name: 'Bruce Wayne',
+ username: 'batman',
+ avatar_url: 'http://batman',
+ });
+
+ expect(issue.assignees.length).toBe(2);
+ });
+
+ it('finds assignee', () => {
+ const assignee = issue.findAssignee(issue.assignees[0]);
+ expect(assignee).toBeDefined();
+ });
+
+ it('removes assignee', () => {
+ const assignee = issue.findAssignee(issue.assignees[0]);
+ issue.removeAssignee(assignee);
+ expect(issue.assignees.length).toBe(0);
+ });
+
+ it('removes all assignees', () => {
+ issue.removeAllAssignees();
+ expect(issue.assignees.length).toBe(0);
+ });
+
it('sets position to infinity if no position is stored', () => {
expect(issue.position).toBe(Infinity);
});
@@ -90,9 +124,31 @@ describe('Issue model', () => {
iid: 1,
confidential: false,
relative_position: 1,
- labels: []
+ labels: [],
+ assignees: [],
});
expect(relativePositionIssue.position).toBe(1);
});
+
+ describe('update', () => {
+ it('passes assignee ids when there are assignees', (done) => {
+ spyOn(Vue.http, 'patch').and.callFake((url, data) => {
+ expect(data.issue.assignee_ids).toEqual([1]);
+ done();
+ });
+
+ issue.update('url');
+ });
+
+ it('passes assignee ids of [0] when there are no assignees', (done) => {
+ spyOn(Vue.http, 'patch').and.callFake((url, data) => {
+ expect(data.issue.assignee_ids).toEqual([0]);
+ done();
+ });
+
+ issue.removeAllAssignees();
+ issue.update('url');
+ });
+ });
});
diff --git a/spec/javascripts/boards/list_spec.js b/spec/javascripts/boards/list_spec.js
index 24a2da9f6b6..8e3d9fd77a0 100644
--- a/spec/javascripts/boards/list_spec.js
+++ b/spec/javascripts/boards/list_spec.js
@@ -8,14 +8,14 @@
import Vue from 'vue';
-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');
+import '~/lib/utils/url_utility';
+import '~/boards/models/issue';
+import '~/boards/models/label';
+import '~/boards/models/list';
+import '~/boards/models/assignee';
+import '~/boards/services/board_service';
+import '~/boards/stores/boards_store';
+import './mock_data';
describe('List model', () => {
let list;
@@ -94,7 +94,8 @@ describe('List model', () => {
title: 'Testing',
iid: _.random(10000),
confidential: false,
- labels: [list.label, listDup.label]
+ labels: [list.label, listDup.label],
+ assignees: [],
});
list.issues.push(issue);
@@ -119,7 +120,8 @@ describe('List model', () => {
title: 'Testing',
iid: _.random(10000) + i,
confidential: false,
- labels: [list.label]
+ labels: [list.label],
+ assignees: [],
}));
}
list.issuesSize = 50;
@@ -137,7 +139,8 @@ describe('List model', () => {
title: 'Testing',
iid: _.random(10000),
confidential: false,
- labels: [list.label]
+ labels: [list.label],
+ assignees: [],
}));
list.issuesSize = 2;
diff --git a/spec/javascripts/boards/mock_data.js b/spec/javascripts/boards/mock_data.js
index a4fa694eebe..a64c3964ee3 100644
--- a/spec/javascripts/boards/mock_data.js
+++ b/spec/javascripts/boards/mock_data.js
@@ -33,7 +33,8 @@ const BoardsMockData = {
title: 'Testing',
iid: 1,
confidential: false,
- labels: []
+ labels: [],
+ assignees: [],
}],
size: 1
}
diff --git a/spec/javascripts/boards/modal_store_spec.js b/spec/javascripts/boards/modal_store_spec.js
index 80db816aff8..32e6d04df9f 100644
--- a/spec/javascripts/boards/modal_store_spec.js
+++ b/spec/javascripts/boards/modal_store_spec.js
@@ -1,10 +1,10 @@
/* global ListIssue */
-require('~/boards/models/issue');
-require('~/boards/models/label');
-require('~/boards/models/list');
-require('~/boards/models/user');
-require('~/boards/stores/modal_store');
+import '~/boards/models/issue';
+import '~/boards/models/label';
+import '~/boards/models/list';
+import '~/boards/models/assignee';
+import '~/boards/stores/modal_store';
describe('Modal store', () => {
let issue;
@@ -21,12 +21,14 @@ describe('Modal store', () => {
iid: 1,
confidential: false,
labels: [],
+ assignees: [],
});
issue2 = new ListIssue({
title: 'Testing',
iid: 2,
confidential: false,
labels: [],
+ assignees: [],
});
Store.store.issues.push(issue);
Store.store.issues.push(issue2);
diff --git a/spec/javascripts/bootstrap_linked_tabs_spec.js b/spec/javascripts/bootstrap_linked_tabs_spec.js
index fa9f95e16cd..a27dc48b3fd 100644
--- a/spec/javascripts/bootstrap_linked_tabs_spec.js
+++ b/spec/javascripts/bootstrap_linked_tabs_spec.js
@@ -1,4 +1,4 @@
-require('~/lib/utils/bootstrap_linked_tabs');
+import LinkedTabs from '~/lib/utils/bootstrap_linked_tabs';
(() => {
// TODO: remove this hack!
@@ -25,7 +25,7 @@ require('~/lib/utils/bootstrap_linked_tabs');
});
it('should activate the tab correspondent to the given action', () => {
- const linkedTabs = new window.gl.LinkedTabs({ // eslint-disable-line
+ const linkedTabs = new LinkedTabs({ // eslint-disable-line
action: 'tab1',
defaultAction: 'tab1',
parentEl: '.linked-tabs',
@@ -35,7 +35,7 @@ require('~/lib/utils/bootstrap_linked_tabs');
});
it('should active the default tab action when the action is show', () => {
- const linkedTabs = new window.gl.LinkedTabs({ // eslint-disable-line
+ const linkedTabs = new LinkedTabs({ // eslint-disable-line
action: 'show',
defaultAction: 'tab1',
parentEl: '.linked-tabs',
@@ -49,7 +49,7 @@ require('~/lib/utils/bootstrap_linked_tabs');
it('should change the url according to the clicked tab', () => {
const historySpy = !phantomjs && spyOn(history, 'replaceState').and.callFake(() => {});
- const linkedTabs = new window.gl.LinkedTabs({ // eslint-disable-line
+ const linkedTabs = new LinkedTabs({
action: 'show',
defaultAction: 'tab1',
parentEl: '.linked-tabs',
diff --git a/spec/javascripts/ci_status_icon_spec.js b/spec/javascripts/ci_status_icon_spec.js
deleted file mode 100644
index c83416c15ef..00000000000
--- a/spec/javascripts/ci_status_icon_spec.js
+++ /dev/null
@@ -1,44 +0,0 @@
-import * as icons from '~/ci_status_icons';
-
-describe('CI status icons', () => {
- const statuses = [
- 'canceled',
- 'created',
- 'failed',
- 'manual',
- 'pending',
- 'running',
- 'skipped',
- 'success',
- 'warning',
- ];
-
- statuses.forEach((status) => {
- it(`should export a ${status} svg`, () => {
- const key = `${status.toUpperCase()}_SVG`;
-
- expect(Object.hasOwnProperty.call(icons, key)).toBe(true);
- expect(icons[key]).toMatch(/^<svg/);
- });
- });
-
- describe('default export map', () => {
- const entityIconNames = [
- 'icon_status_canceled',
- 'icon_status_created',
- 'icon_status_failed',
- 'icon_status_manual',
- 'icon_status_pending',
- 'icon_status_running',
- 'icon_status_skipped',
- 'icon_status_success',
- 'icon_status_warning',
- ];
-
- entityIconNames.forEach((iconName) => {
- it(`should have a '${iconName}' key`, () => {
- expect(Object.hasOwnProperty.call(icons.default, iconName)).toBe(true);
- });
- });
- });
-});
diff --git a/spec/javascripts/commit/pipelines/mock_data.js b/spec/javascripts/commit/pipelines/mock_data.js
deleted file mode 100644
index 82b00b4c1ec..00000000000
--- a/spec/javascripts/commit/pipelines/mock_data.js
+++ /dev/null
@@ -1,89 +0,0 @@
-export default {
- id: 73,
- user: {
- name: 'Administrator',
- username: 'root',
- id: 1,
- state: 'active',
- avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
- web_url: 'http://localhost:3000/root',
- },
- path: '/root/review-app/pipelines/73',
- details: {
- status: {
- icon: 'icon_status_failed',
- text: 'failed',
- label: 'failed',
- group: 'failed',
- has_details: true,
- details_path: '/root/review-app/pipelines/73',
- },
- duration: null,
- finished_at: '2017-01-25T00:00:17.130Z',
- stages: [{
- name: 'build',
- title: 'build: failed',
- status: {
- icon: 'icon_status_failed',
- text: 'failed',
- label: 'failed',
- group: 'failed',
- has_details: true,
- details_path: '/root/review-app/pipelines/73#build',
- },
- path: '/root/review-app/pipelines/73#build',
- dropdown_path: '/root/review-app/pipelines/73/stage.json?stage=build',
- }],
- artifacts: [],
- manual_actions: [
- {
- name: 'stop_review',
- path: '/root/review-app/builds/1463/play',
- },
- {
- name: 'name',
- path: '/root/review-app/builds/1490/play',
- },
- ],
- },
- flags: {
- latest: true,
- triggered: false,
- stuck: false,
- yaml_errors: false,
- retryable: true,
- cancelable: false,
- },
- ref:
- {
- name: 'master',
- path: '/root/review-app/tree/master',
- tag: false,
- branch: true,
- },
- commit: {
- id: 'fbd79f04fa98717641deaaeb092a4d417237c2e4',
- short_id: 'fbd79f04',
- title: 'Update .gitlab-ci.yml',
- author_name: 'Administrator',
- author_email: 'admin@example.com',
- created_at: '2017-01-16T12:13:57.000-05:00',
- committer_name: 'Administrator',
- committer_email: 'admin@example.com',
- message: 'Update .gitlab-ci.yml',
- author: {
- name: 'Administrator',
- username: 'root',
- id: 1,
- state: 'active',
- avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
- web_url: 'http://localhost:3000/root',
- },
- author_gravatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
- commit_url: 'http://localhost:3000/root/review-app/commit/fbd79f04fa98717641deaaeb092a4d417237c2e4',
- commit_path: '/root/review-app/commit/fbd79f04fa98717641deaaeb092a4d417237c2e4',
- },
- retry_path: '/root/review-app/pipelines/73/retry',
- created_at: '2017-01-16T17:13:59.800Z',
- updated_at: '2017-01-25T00:00:17.132Z',
-};
diff --git a/spec/javascripts/commit/pipelines/pipelines_spec.js b/spec/javascripts/commit/pipelines/pipelines_spec.js
index ad31448f81c..398c593eec2 100644
--- a/spec/javascripts/commit/pipelines/pipelines_spec.js
+++ b/spec/javascripts/commit/pipelines/pipelines_spec.js
@@ -1,12 +1,17 @@
import Vue from 'vue';
import PipelinesTable from '~/commit/pipelines/pipelines_table';
-import pipeline from './mock_data';
describe('Pipelines table in Commits and Merge requests', () => {
+ const jsonFixtureName = 'pipelines/pipelines.json';
+ let pipeline;
+
preloadFixtures('static/pipelines_table.html.raw');
+ preloadFixtures(jsonFixtureName);
beforeEach(() => {
loadFixtures('static/pipelines_table.html.raw');
+ const pipelines = getJSONFixture(jsonFixtureName).pipelines;
+ pipeline = pipelines.find(p => p.id === 1);
});
describe('successful request', () => {
diff --git a/spec/javascripts/cycle_analytics/limit_warning_component_spec.js b/spec/javascripts/cycle_analytics/limit_warning_component_spec.js
index 50000c5a5f5..2fb9eb0ca85 100644
--- a/spec/javascripts/cycle_analytics/limit_warning_component_spec.js
+++ b/spec/javascripts/cycle_analytics/limit_warning_component_spec.js
@@ -1,6 +1,9 @@
import Vue from 'vue';
+import Translate from '~/vue_shared/translate';
import limitWarningComp from '~/cycle_analytics/components/limit_warning_component';
+Vue.use(Translate);
+
describe('Limit warning component', () => {
let component;
let LimitWarningComponent;
diff --git a/spec/javascripts/deploy_keys/components/action_btn_spec.js b/spec/javascripts/deploy_keys/components/action_btn_spec.js
new file mode 100644
index 00000000000..5b93fbc5575
--- /dev/null
+++ b/spec/javascripts/deploy_keys/components/action_btn_spec.js
@@ -0,0 +1,70 @@
+import Vue from 'vue';
+import eventHub from '~/deploy_keys/eventhub';
+import actionBtn from '~/deploy_keys/components/action_btn.vue';
+
+describe('Deploy keys action btn', () => {
+ const data = getJSONFixture('deploy_keys/keys.json');
+ const deployKey = data.enabled_keys[0];
+ let vm;
+
+ beforeEach((done) => {
+ const ActionBtnComponent = Vue.extend(actionBtn);
+
+ vm = new ActionBtnComponent({
+ propsData: {
+ deployKey,
+ type: 'enable',
+ },
+ }).$mount();
+
+ setTimeout(done);
+ });
+
+ it('renders the type as uppercase', () => {
+ expect(
+ vm.$el.textContent.trim(),
+ ).toBe('Enable');
+ });
+
+ it('sends eventHub event with btn type', (done) => {
+ spyOn(eventHub, '$emit');
+
+ vm.$el.click();
+
+ setTimeout(() => {
+ expect(
+ eventHub.$emit,
+ ).toHaveBeenCalledWith('enable.key', deployKey);
+
+ done();
+ });
+ });
+
+ it('shows loading spinner after click', (done) => {
+ vm.$el.click();
+
+ setTimeout(() => {
+ expect(
+ vm.$el.querySelector('.fa'),
+ ).toBeDefined();
+
+ done();
+ });
+ });
+
+ it('disables button after click', (done) => {
+ vm.$el.click();
+
+ setTimeout(() => {
+ expect(
+ vm.$el.classList.contains('disabled'),
+ ).toBeTruthy();
+
+ expect(
+ vm.$el.getAttribute('disabled'),
+ ).toBe('disabled');
+
+ done();
+ });
+ });
+});
diff --git a/spec/javascripts/deploy_keys/components/app_spec.js b/spec/javascripts/deploy_keys/components/app_spec.js
new file mode 100644
index 00000000000..700897f50b0
--- /dev/null
+++ b/spec/javascripts/deploy_keys/components/app_spec.js
@@ -0,0 +1,142 @@
+import Vue from 'vue';
+import eventHub from '~/deploy_keys/eventhub';
+import deployKeysApp from '~/deploy_keys/components/app.vue';
+
+describe('Deploy keys app component', () => {
+ const data = getJSONFixture('deploy_keys/keys.json');
+ let vm;
+
+ const deployKeysResponse = (request, next) => {
+ next(request.respondWith(JSON.stringify(data), {
+ status: 200,
+ }));
+ };
+
+ beforeEach((done) => {
+ const Component = Vue.extend(deployKeysApp);
+
+ Vue.http.interceptors.push(deployKeysResponse);
+
+ vm = new Component({
+ propsData: {
+ endpoint: '/test',
+ },
+ }).$mount();
+
+ setTimeout(done);
+ });
+
+ afterEach(() => {
+ Vue.http.interceptors = _.without(Vue.http.interceptors, deployKeysResponse);
+ });
+
+ it('renders loading icon', (done) => {
+ vm.store.keys = {};
+ vm.isLoading = false;
+
+ Vue.nextTick(() => {
+ expect(
+ vm.$el.querySelectorAll('.deploy-keys-panel').length,
+ ).toBe(0);
+
+ expect(
+ vm.$el.querySelector('.fa-spinner'),
+ ).toBeDefined();
+
+ done();
+ });
+ });
+
+ it('renders keys panels', () => {
+ expect(
+ vm.$el.querySelectorAll('.deploy-keys-panel').length,
+ ).toBe(3);
+ });
+
+ it('does not render key panels when keys object is empty', (done) => {
+ vm.store.keys = {};
+
+ Vue.nextTick(() => {
+ expect(
+ vm.$el.querySelectorAll('.deploy-keys-panel').length,
+ ).toBe(0);
+
+ done();
+ });
+ });
+
+ it('does not render public panel when empty', (done) => {
+ vm.store.keys.public_keys = [];
+
+ Vue.nextTick(() => {
+ expect(
+ vm.$el.querySelectorAll('.deploy-keys-panel').length,
+ ).toBe(2);
+
+ done();
+ });
+ });
+
+ it('re-fetches deploy keys when enabling a key', (done) => {
+ const key = data.public_keys[0];
+
+ spyOn(vm.service, 'getKeys');
+ spyOn(vm.service, 'enableKey').and.callFake(() => new Promise((resolve) => {
+ resolve();
+
+ setTimeout(() => {
+ expect(vm.service.getKeys).toHaveBeenCalled();
+
+ done();
+ });
+ }));
+
+ eventHub.$emit('enable.key', key);
+
+ expect(vm.service.enableKey).toHaveBeenCalledWith(key.id);
+ });
+
+ it('re-fetches deploy keys when disabling a key', (done) => {
+ const key = data.public_keys[0];
+
+ spyOn(window, 'confirm').and.returnValue(true);
+ spyOn(vm.service, 'getKeys');
+ spyOn(vm.service, 'disableKey').and.callFake(() => new Promise((resolve) => {
+ resolve();
+
+ setTimeout(() => {
+ expect(vm.service.getKeys).toHaveBeenCalled();
+
+ done();
+ });
+ }));
+
+ eventHub.$emit('disable.key', key);
+
+ expect(vm.service.disableKey).toHaveBeenCalledWith(key.id);
+ });
+
+ it('calls disableKey when removing a key', (done) => {
+ const key = data.public_keys[0];
+
+ spyOn(window, 'confirm').and.returnValue(true);
+ spyOn(vm.service, 'getKeys');
+ spyOn(vm.service, 'disableKey').and.callFake(() => new Promise((resolve) => {
+ resolve();
+
+ setTimeout(() => {
+ expect(vm.service.getKeys).toHaveBeenCalled();
+
+ done();
+ });
+ }));
+
+ eventHub.$emit('remove.key', key);
+
+ expect(vm.service.disableKey).toHaveBeenCalledWith(key.id);
+ });
+
+ it('hasKeys returns true when there are keys', () => {
+ expect(vm.hasKeys).toEqual(3);
+ });
+});
diff --git a/spec/javascripts/deploy_keys/components/key_spec.js b/spec/javascripts/deploy_keys/components/key_spec.js
new file mode 100644
index 00000000000..793ab8c451d
--- /dev/null
+++ b/spec/javascripts/deploy_keys/components/key_spec.js
@@ -0,0 +1,92 @@
+import Vue from 'vue';
+import DeployKeysStore from '~/deploy_keys/store';
+import key from '~/deploy_keys/components/key.vue';
+
+describe('Deploy keys key', () => {
+ let vm;
+ const KeyComponent = Vue.extend(key);
+ const data = getJSONFixture('deploy_keys/keys.json');
+ const createComponent = (deployKey) => {
+ const store = new DeployKeysStore();
+ store.keys = data;
+
+ vm = new KeyComponent({
+ propsData: {
+ deployKey,
+ store,
+ },
+ }).$mount();
+ };
+
+ describe('enabled key', () => {
+ const deployKey = data.enabled_keys[0];
+
+ beforeEach((done) => {
+ createComponent(deployKey);
+
+ setTimeout(done);
+ });
+
+ it('renders the keys title', () => {
+ expect(
+ vm.$el.querySelector('.title').textContent.trim(),
+ ).toContain('My title');
+ });
+
+ it('renders human friendly formatted created date', () => {
+ expect(
+ vm.$el.querySelector('.key-created-at').textContent.trim(),
+ ).toBe(`created ${gl.utils.getTimeago().format(deployKey.created_at)}`);
+ });
+
+ it('shows remove button', () => {
+ expect(
+ vm.$el.querySelector('.btn').textContent.trim(),
+ ).toBe('Remove');
+ });
+
+ it('shows write access text when key has write access', (done) => {
+ vm.deployKey.can_push = true;
+
+ Vue.nextTick(() => {
+ expect(
+ vm.$el.querySelector('.write-access-allowed'),
+ ).not.toBeNull();
+
+ expect(
+ vm.$el.querySelector('.write-access-allowed').textContent.trim(),
+ ).toBe('Write access allowed');
+
+ done();
+ });
+ });
+ });
+
+ describe('public keys', () => {
+ const deployKey = data.public_keys[0];
+
+ beforeEach((done) => {
+ createComponent(deployKey);
+
+ setTimeout(done);
+ });
+
+ it('shows enable button', () => {
+ expect(
+ vm.$el.querySelector('.btn').textContent.trim(),
+ ).toBe('Enable');
+ });
+
+ it('shows disable button when key is enabled', (done) => {
+ vm.store.keys.enabled_keys.push(deployKey);
+
+ Vue.nextTick(() => {
+ expect(
+ vm.$el.querySelector('.btn').textContent.trim(),
+ ).toBe('Disable');
+
+ done();
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/deploy_keys/components/keys_panel_spec.js b/spec/javascripts/deploy_keys/components/keys_panel_spec.js
new file mode 100644
index 00000000000..a69b39c35c4
--- /dev/null
+++ b/spec/javascripts/deploy_keys/components/keys_panel_spec.js
@@ -0,0 +1,70 @@
+import Vue from 'vue';
+import DeployKeysStore from '~/deploy_keys/store';
+import deployKeysPanel from '~/deploy_keys/components/keys_panel.vue';
+
+describe('Deploy keys panel', () => {
+ const data = getJSONFixture('deploy_keys/keys.json');
+ let vm;
+
+ beforeEach((done) => {
+ const DeployKeysPanelComponent = Vue.extend(deployKeysPanel);
+ const store = new DeployKeysStore();
+ store.keys = data;
+
+ vm = new DeployKeysPanelComponent({
+ propsData: {
+ title: 'test',
+ keys: data.enabled_keys,
+ showHelpBox: true,
+ store,
+ },
+ }).$mount();
+
+ setTimeout(done);
+ });
+
+ it('renders the title with keys count', () => {
+ expect(
+ vm.$el.querySelector('h5').textContent.trim(),
+ ).toContain('test');
+
+ expect(
+ vm.$el.querySelector('h5').textContent.trim(),
+ ).toContain(`(${vm.keys.length})`);
+ });
+
+ it('renders list of keys', () => {
+ expect(
+ vm.$el.querySelectorAll('li').length,
+ ).toBe(vm.keys.length);
+ });
+
+ it('renders help box if keys are empty', (done) => {
+ vm.keys = [];
+
+ Vue.nextTick(() => {
+ expect(
+ vm.$el.querySelector('.settings-message'),
+ ).toBeDefined();
+
+ expect(
+ vm.$el.querySelector('.settings-message').textContent.trim(),
+ ).toBe('No deploy keys found. Create one with the form above.');
+
+ done();
+ });
+ });
+
+ it('does not render help box if keys are empty & showHelpBox is false', (done) => {
+ vm.keys = [];
+ vm.showHelpBox = false;
+
+ Vue.nextTick(() => {
+ expect(
+ vm.$el.querySelector('.settings-message'),
+ ).toBeNull();
+
+ done();
+ });
+ });
+});
diff --git a/spec/javascripts/droplab/constants_spec.js b/spec/javascripts/droplab/constants_spec.js
index fd153a49fcd..b9d28db74cc 100644
--- a/spec/javascripts/droplab/constants_spec.js
+++ b/spec/javascripts/droplab/constants_spec.js
@@ -27,6 +27,12 @@ describe('constants', function () {
});
});
+ describe('TEMPLATE_REGEX', function () {
+ it('should be a handlebars templating syntax regex', function() {
+ expect(constants.TEMPLATE_REGEX).toEqual(/\{\{(.+?)\}\}/g);
+ });
+ });
+
describe('IGNORE_CLASS', function () {
it('should be `droplab-item-ignore`', function() {
expect(constants.IGNORE_CLASS).toBe('droplab-item-ignore');
diff --git a/spec/javascripts/droplab/drop_down_spec.js b/spec/javascripts/droplab/drop_down_spec.js
index 7516b301917..2bbcebeeac0 100644
--- a/spec/javascripts/droplab/drop_down_spec.js
+++ b/spec/javascripts/droplab/drop_down_spec.js
@@ -1,5 +1,3 @@
-/* eslint-disable */
-
import DropDown from '~/droplab/drop_down';
import utils from '~/droplab/utils';
import { SELECTED_CLASS, IGNORE_CLASS } from '~/droplab/constants';
@@ -17,7 +15,7 @@ describe('DropDown', function () {
it('sets the .hidden property to true', function () {
expect(this.dropdown.hidden).toBe(true);
- })
+ });
it('sets the .list property', function () {
expect(this.dropdown.list).toBe(this.list);
@@ -152,7 +150,7 @@ describe('DropDown', function () {
it('should call addSelectedClass', function () {
expect(this.dropdown.addSelectedClass).toHaveBeenCalledWith(this.closestElement);
- })
+ });
it('should call .preventDefault', function () {
expect(this.event.preventDefault).toHaveBeenCalled();
@@ -293,36 +291,6 @@ describe('DropDown', function () {
});
});
- describe('toggle', function () {
- beforeEach(function () {
- this.dropdown = { hidden: true, show: () => {}, hide: () => {} };
-
- spyOn(this.dropdown, 'show');
- spyOn(this.dropdown, 'hide');
-
- DropDown.prototype.toggle.call(this.dropdown);
- });
-
- it('should call .show if hidden is true', function () {
- expect(this.dropdown.show).toHaveBeenCalled();
- });
-
- describe('if hidden is false', function () {
- beforeEach(function () {
- this.dropdown = { hidden: false, show: () => {}, hide: () => {} };
-
- spyOn(this.dropdown, 'show');
- spyOn(this.dropdown, 'hide');
-
- DropDown.prototype.toggle.call(this.dropdown);
- });
-
- it('should call .show if hidden is true', function () {
- expect(this.dropdown.hide).toHaveBeenCalled();
- });
- });
- });
-
describe('setData', function () {
beforeEach(function () {
this.dropdown = { render: () => {} };
@@ -399,7 +367,7 @@ describe('DropDown', function () {
expect(this.data.map).toHaveBeenCalledWith(jasmine.any(Function));
});
- it('should call .renderChildren for each data item', function() {
+ it('should call .renderChildren for each data item', function () {
expect(this.dropdown.renderChildren.calls.count()).toBe(this.data.length);
});
@@ -407,7 +375,7 @@ describe('DropDown', function () {
expect(this.renderableList.innerHTML).toBe('01');
});
- describe('if no data argument is passed' , function () {
+ describe('if no data argument is passed', function () {
beforeEach(function () {
this.data.map.calls.reset();
this.dropdown.renderChildren.calls.reset();
@@ -446,20 +414,20 @@ describe('DropDown', function () {
describe('renderChildren', function () {
beforeEach(function () {
this.templateString = 'templateString';
- this.dropdown = { setImagesSrc: () => {}, templateString: this.templateString };
+ this.dropdown = { templateString: this.templateString };
this.data = { droplab_hidden: true };
this.html = 'html';
this.template = { firstChild: { outerHTML: 'outerHTML', style: {} } };
- spyOn(utils, 't').and.returnValue(this.html);
+ spyOn(utils, 'template').and.returnValue(this.html);
spyOn(document, 'createElement').and.returnValue(this.template);
- spyOn(this.dropdown, 'setImagesSrc');
+ spyOn(DropDown, 'setImagesSrc');
this.renderChildren = DropDown.prototype.renderChildren.call(this.dropdown, this.data);
});
it('should call utils.t with .templateString and data', function () {
- expect(utils.t).toHaveBeenCalledWith(this.templateString, this.data);
+ expect(utils.template).toHaveBeenCalledWith(this.templateString, this.data);
});
it('should call document.createElement', function () {
@@ -471,7 +439,7 @@ describe('DropDown', function () {
});
it('should call .setImagesSrc with the template', function () {
- expect(this.dropdown.setImagesSrc).toHaveBeenCalledWith(this.template);
+ expect(DropDown.setImagesSrc).toHaveBeenCalledWith(this.template);
});
it('should set the template display to none', function () {
@@ -496,12 +464,11 @@ describe('DropDown', function () {
describe('setImagesSrc', function () {
beforeEach(function () {
- this.dropdown = {};
this.template = { querySelectorAll: () => {} };
spyOn(this.template, 'querySelectorAll').and.returnValue([]);
- DropDown.prototype.setImagesSrc.call(this.dropdown, this.template);
+ DropDown.setImagesSrc(this.template);
});
it('should call .querySelectorAll', function () {
@@ -562,7 +529,7 @@ describe('DropDown', function () {
describe('toggle', function () {
beforeEach(function () {
- this.hidden = true
+ this.hidden = true;
this.dropdown = { hidden: this.hidden, show: () => {}, hide: () => {} };
spyOn(this.dropdown, 'show');
@@ -577,7 +544,7 @@ describe('DropDown', function () {
describe('if .hidden is false', function () {
beforeEach(function () {
- this.hidden = false
+ this.hidden = false;
this.dropdown = { hidden: this.hidden, show: () => {}, hide: () => {} };
spyOn(this.dropdown, 'show');
diff --git a/spec/javascripts/droplab/hook_spec.js b/spec/javascripts/droplab/hook_spec.js
index 8ebdcdd1404..75bf5f3d611 100644
--- a/spec/javascripts/droplab/hook_spec.js
+++ b/spec/javascripts/droplab/hook_spec.js
@@ -1,5 +1,3 @@
-/* eslint-disable */
-
import Hook from '~/droplab/hook';
import * as dropdownSrc from '~/droplab/drop_down';
@@ -73,10 +71,4 @@ describe('Hook', function () {
});
});
});
-
- describe('addEvents', function () {
- it('should exist', function () {
- expect(Hook.prototype.hasOwnProperty('addEvents')).toBe(true);
- });
- });
});
diff --git a/spec/javascripts/environments/environment_actions_spec.js b/spec/javascripts/environments/environment_actions_spec.js
index 676bf61cfd9..596d812c724 100644
--- a/spec/javascripts/environments/environment_actions_spec.js
+++ b/spec/javascripts/environments/environment_actions_spec.js
@@ -4,7 +4,6 @@ import actionsComp from '~/environments/components/environment_actions.vue';
describe('Actions Component', () => {
let ActionsComponent;
let actionsMock;
- let spy;
let component;
beforeEach(() => {
@@ -26,13 +25,9 @@ describe('Actions Component', () => {
},
];
- spy = jasmine.createSpy('spy').and.returnValue(Promise.resolve());
component = new ActionsComponent({
propsData: {
actions: actionsMock,
- service: {
- postAction: spy,
- },
},
}).$mount();
});
@@ -48,13 +43,6 @@ describe('Actions Component', () => {
).toEqual(actionsMock.length);
});
- it('should call the service when an action is clicked', () => {
- component.$el.querySelector('.dropdown').click();
- component.$el.querySelector('.js-manual-action-link').click();
-
- expect(spy).toHaveBeenCalledWith(actionsMock[0].play_path);
- });
-
it('should render a disabled action when it\'s not playable', () => {
expect(
component.$el.querySelector('.dropdown-menu li:last-child button').getAttribute('disabled'),
diff --git a/spec/javascripts/environments/environment_rollback_spec.js b/spec/javascripts/environments/environment_rollback_spec.js
index 25397714a76..eb8e49d81fe 100644
--- a/spec/javascripts/environments/environment_rollback_spec.js
+++ b/spec/javascripts/environments/environment_rollback_spec.js
@@ -4,11 +4,9 @@ import rollbackComp from '~/environments/components/environment_rollback.vue';
describe('Rollback Component', () => {
const retryURL = 'https://gitlab.com/retry';
let RollbackComponent;
- let spy;
beforeEach(() => {
RollbackComponent = Vue.extend(rollbackComp);
- spy = jasmine.createSpy('spy').and.returnValue(Promise.resolve());
});
it('Should render Re-deploy label when isLastDeployment is true', () => {
@@ -17,9 +15,6 @@ describe('Rollback Component', () => {
propsData: {
retryUrl: retryURL,
isLastDeployment: true,
- service: {
- postAction: spy,
- },
},
}).$mount();
@@ -32,28 +27,9 @@ describe('Rollback Component', () => {
propsData: {
retryUrl: retryURL,
isLastDeployment: false,
- service: {
- postAction: spy,
- },
},
}).$mount();
expect(component.$el.querySelector('span').textContent).toContain('Rollback');
});
-
- it('should call the service when the button is clicked', () => {
- const component = new RollbackComponent({
- propsData: {
- retryUrl: retryURL,
- isLastDeployment: false,
- service: {
- postAction: spy,
- },
- },
- }).$mount();
-
- component.$el.click();
-
- expect(spy).toHaveBeenCalledWith(retryURL);
- });
});
diff --git a/spec/javascripts/environments/environment_spec.js b/spec/javascripts/environments/environment_spec.js
index 9762688af1a..1c54cc3054c 100644
--- a/spec/javascripts/environments/environment_spec.js
+++ b/spec/javascripts/environments/environment_spec.js
@@ -1,15 +1,18 @@
import Vue from 'vue';
import '~/flash';
-import EnvironmentsComponent from '~/environments/components/environment';
+import environmentsComponent from '~/environments/components/environment.vue';
import { environment, folder } from './mock_data';
describe('Environment', () => {
preloadFixtures('static/environments/environments.html.raw');
+ let EnvironmentsComponent;
let component;
beforeEach(() => {
loadFixtures('static/environments/environments.html.raw');
+
+ EnvironmentsComponent = Vue.extend(environmentsComponent);
});
describe('successfull request', () => {
diff --git a/spec/javascripts/environments/environment_stop_spec.js b/spec/javascripts/environments/environment_stop_spec.js
index 942e4aaabd4..8131f1e5b11 100644
--- a/spec/javascripts/environments/environment_stop_spec.js
+++ b/spec/javascripts/environments/environment_stop_spec.js
@@ -4,20 +4,15 @@ import stopComp from '~/environments/components/environment_stop.vue';
describe('Stop Component', () => {
let StopComponent;
let component;
- let spy;
const stopURL = '/stop';
beforeEach(() => {
StopComponent = Vue.extend(stopComp);
- spy = jasmine.createSpy('spy').and.returnValue(Promise.resolve());
spyOn(window, 'confirm').and.returnValue(true);
component = new StopComponent({
propsData: {
stopUrl: stopURL,
- service: {
- postAction: spy,
- },
},
}).$mount();
});
@@ -26,9 +21,4 @@ describe('Stop Component', () => {
expect(component.$el.tagName).toEqual('BUTTON');
expect(component.$el.getAttribute('title')).toEqual('Stop');
});
-
- it('should call the service when an action is clicked', () => {
- component.$el.click();
- expect(spy).toHaveBeenCalled();
- });
});
diff --git a/spec/javascripts/environments/folder/environments_folder_view_spec.js b/spec/javascripts/environments/folder/environments_folder_view_spec.js
index 72f3db29a66..350078ad5f5 100644
--- a/spec/javascripts/environments/folder/environments_folder_view_spec.js
+++ b/spec/javascripts/environments/folder/environments_folder_view_spec.js
@@ -1,13 +1,15 @@
import Vue from 'vue';
import '~/flash';
-import EnvironmentsFolderViewComponent from '~/environments/folder/environments_folder_view';
+import environmentsFolderViewComponent from '~/environments/folder/environments_folder_view.vue';
import { environmentsList } from '../mock_data';
describe('Environments Folder View', () => {
preloadFixtures('static/environments/environments_folder_view.html.raw');
+ let EnvironmentsFolderViewComponent;
beforeEach(() => {
loadFixtures('static/environments/environments_folder_view.html.raw');
+ EnvironmentsFolderViewComponent = Vue.extend(environmentsFolderViewComponent);
window.history.pushState({}, null, 'environments/folders/build');
});
diff --git a/spec/javascripts/filtered_search/components/recent_searches_dropdown_content_spec.js b/spec/javascripts/filtered_search/components/recent_searches_dropdown_content_spec.js
index 2722882375f..d0f09a561d5 100644
--- a/spec/javascripts/filtered_search/components/recent_searches_dropdown_content_spec.js
+++ b/spec/javascripts/filtered_search/components/recent_searches_dropdown_content_spec.js
@@ -76,6 +76,26 @@ describe('RecentSearchesDropdownContent', () => {
});
});
+ describe('if isLocalStorageAvailable is `false`', () => {
+ let el;
+
+ beforeEach(() => {
+ const props = Object.assign({ isLocalStorageAvailable: false }, propsDataWithItems);
+
+ vm = createComponent(props);
+ el = vm.$el;
+ });
+
+ it('should render an info note', () => {
+ const note = el.querySelector('.dropdown-info-note');
+ const items = el.querySelectorAll('.filtered-search-history-dropdown-item');
+
+ expect(note).toBeDefined();
+ expect(note.innerText.trim()).toBe('This feature requires local storage to be enabled');
+ expect(items.length).toEqual(propsDataWithoutItems.items.length);
+ });
+ });
+
describe('computed', () => {
describe('processedItems', () => {
it('with items', () => {
diff --git a/spec/javascripts/filtered_search/filtered_search_manager_spec.js b/spec/javascripts/filtered_search/filtered_search_manager_spec.js
index 6683489f63c..063d547d00c 100644
--- a/spec/javascripts/filtered_search/filtered_search_manager_spec.js
+++ b/spec/javascripts/filtered_search/filtered_search_manager_spec.js
@@ -1,3 +1,7 @@
+import * as recentSearchesStoreSrc from '~/filtered_search/stores/recent_searches_store';
+import RecentSearchesService from '~/filtered_search/services/recent_searches_service';
+import RecentSearchesServiceError from '~/filtered_search/services/recent_searches_service_error';
+
require('~/lib/utils/url_utility');
require('~/lib/utils/common_utils');
require('~/filtered_search/filtered_search_token_keys');
@@ -26,6 +30,10 @@ describe('Filtered Search Manager', () => {
element.dispatchEvent(event);
}
+ function getVisualTokens() {
+ return tokensContainer.querySelectorAll('.js-visual-token');
+ }
+
beforeEach(() => {
setFixtures(`
<div class="filtered-search-box">
@@ -56,6 +64,36 @@ describe('Filtered Search Manager', () => {
manager.cleanup();
});
+ describe('class constructor', () => {
+ const isLocalStorageAvailable = 'isLocalStorageAvailable';
+ let filteredSearchManager;
+
+ beforeEach(() => {
+ spyOn(RecentSearchesService, 'isAvailable').and.returnValue(isLocalStorageAvailable);
+ spyOn(recentSearchesStoreSrc, 'default');
+
+ filteredSearchManager = new gl.FilteredSearchManager();
+
+ return filteredSearchManager;
+ });
+
+ it('should instantiate RecentSearchesStore with isLocalStorageAvailable', () => {
+ expect(RecentSearchesService.isAvailable).toHaveBeenCalled();
+ expect(recentSearchesStoreSrc.default).toHaveBeenCalledWith({
+ isLocalStorageAvailable,
+ });
+ });
+
+ it('should not instantiate Flash if an RecentSearchesServiceError is caught', () => {
+ spyOn(RecentSearchesService.prototype, 'fetch').and.callFake(() => Promise.reject(new RecentSearchesServiceError()));
+ spyOn(window, 'Flash');
+
+ filteredSearchManager = new gl.FilteredSearchManager();
+
+ expect(window.Flash).not.toHaveBeenCalled();
+ });
+ });
+
describe('search', () => {
const defaultParams = '?scope=all&utf8=%E2%9C%93&state=opened';
@@ -170,11 +208,37 @@ describe('Filtered Search Manager', () => {
});
});
- describe('removeSelectedToken', () => {
- function getVisualTokens() {
- return tokensContainer.querySelectorAll('.js-visual-token');
- }
+ describe('removeToken', () => {
+ it('removes token even when it is already selected', () => {
+ tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
+ FilteredSearchSpecHelper.createFilterVisualTokenHTML('milestone', 'none', true),
+ );
+
+ tokensContainer.querySelector('.js-visual-token .remove-token').click();
+ expect(tokensContainer.querySelector('.js-visual-token')).toEqual(null);
+ });
+
+ describe('unselected token', () => {
+ beforeEach(() => {
+ spyOn(gl.FilteredSearchManager.prototype, 'removeSelectedToken').and.callThrough();
+
+ tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
+ FilteredSearchSpecHelper.createFilterVisualTokenHTML('milestone', 'none'),
+ );
+ tokensContainer.querySelector('.js-visual-token .remove-token').click();
+ });
+
+ it('removes token when remove button is selected', () => {
+ expect(tokensContainer.querySelector('.js-visual-token')).toEqual(null);
+ });
+
+ it('calls removeSelectedToken', () => {
+ expect(manager.removeSelectedToken).toHaveBeenCalled();
+ });
+ });
+ });
+ describe('removeSelectedTokenKeydown', () => {
beforeEach(() => {
tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
FilteredSearchSpecHelper.createFilterVisualTokenHTML('milestone', 'none', true),
@@ -224,6 +288,31 @@ describe('Filtered Search Manager', () => {
});
});
+ describe('removeSelectedToken', () => {
+ beforeEach(() => {
+ spyOn(gl.FilteredSearchVisualTokens, 'removeSelectedToken').and.callThrough();
+ spyOn(gl.FilteredSearchManager.prototype, 'handleInputPlaceholder').and.callThrough();
+ spyOn(gl.FilteredSearchManager.prototype, 'toggleClearSearchButton').and.callThrough();
+ manager.removeSelectedToken();
+ });
+
+ it('calls FilteredSearchVisualTokens.removeSelectedToken', () => {
+ expect(gl.FilteredSearchVisualTokens.removeSelectedToken).toHaveBeenCalled();
+ });
+
+ it('calls handleInputPlaceholder', () => {
+ expect(manager.handleInputPlaceholder).toHaveBeenCalled();
+ });
+
+ it('calls toggleClearSearchButton', () => {
+ expect(manager.toggleClearSearchButton).toHaveBeenCalled();
+ });
+
+ it('calls update dropdown offset', () => {
+ expect(manager.dropdownManager.updateDropdownOffset).toHaveBeenCalled();
+ });
+ });
+
describe('unselects token', () => {
beforeEach(() => {
tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(`
diff --git a/spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js b/spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js
index bbda1476fed..8b750561eb7 100644
--- a/spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js
+++ b/spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js
@@ -1,3 +1,5 @@
+import AjaxCache from '~/lib/utils/ajax_cache';
+
require('~/filtered_search/filtered_search_visual_tokens');
const FilteredSearchSpecHelper = require('../helpers/filtered_search_spec_helper');
@@ -214,8 +216,12 @@ describe('Filtered Search Visual Tokens', () => {
expect(tokenElement.querySelector('.name')).toEqual(jasmine.anything());
});
+ it('contains value container div', () => {
+ expect(tokenElement.querySelector('.value-container')).toEqual(jasmine.anything());
+ });
+
it('contains value div', () => {
- expect(tokenElement.querySelector('.value')).toEqual(jasmine.anything());
+ expect(tokenElement.querySelector('.value-container .value')).toEqual(jasmine.anything());
});
it('contains selectable class', () => {
@@ -225,6 +231,16 @@ describe('Filtered Search Visual Tokens', () => {
it('contains button role', () => {
expect(tokenElement.getAttribute('role')).toEqual('button');
});
+
+ describe('remove token', () => {
+ it('contains remove-token button', () => {
+ expect(tokenElement.querySelector('.value-container .remove-token')).toEqual(jasmine.anything());
+ });
+
+ it('contains fa-close icon', () => {
+ expect(tokenElement.querySelector('.remove-token .fa-close')).toEqual(jasmine.anything());
+ });
+ });
});
describe('addVisualTokenElement', () => {
@@ -597,4 +613,103 @@ describe('Filtered Search Visual Tokens', () => {
expect(token.querySelector('.value').innerText).toEqual('~bug');
});
});
+
+ describe('renderVisualTokenValue', () => {
+ let searchTokens;
+
+ beforeEach(() => {
+ tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(`
+ ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', 'none')}
+ ${FilteredSearchSpecHelper.createSearchVisualTokenHTML('search')}
+ ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('milestone', 'upcoming')}
+ `);
+
+ searchTokens = document.querySelectorAll('.filtered-search-token');
+ });
+
+ it('renders a token value element', () => {
+ spyOn(gl.FilteredSearchVisualTokens, 'updateLabelTokenColor');
+ const updateLabelTokenColorSpy = gl.FilteredSearchVisualTokens.updateLabelTokenColor;
+
+ expect(searchTokens.length).toBe(2);
+ Array.prototype.forEach.call(searchTokens, (token) => {
+ updateLabelTokenColorSpy.calls.reset();
+
+ const tokenName = token.querySelector('.name').innerText;
+ const tokenValue = 'new value';
+ gl.FilteredSearchVisualTokens.renderVisualTokenValue(token, tokenName, tokenValue);
+
+ const tokenValueElement = token.querySelector('.value');
+ expect(tokenValueElement.innerText).toBe(tokenValue);
+
+ if (tokenName.toLowerCase() === 'label') {
+ const tokenValueContainer = token.querySelector('.value-container');
+ expect(updateLabelTokenColorSpy.calls.count()).toBe(1);
+ const expectedArgs = [tokenValueContainer, tokenValue];
+ expect(updateLabelTokenColorSpy.calls.argsFor(0)).toEqual(expectedArgs);
+ } else {
+ expect(updateLabelTokenColorSpy.calls.count()).toBe(0);
+ }
+ });
+ });
+ });
+
+ describe('updateLabelTokenColor', () => {
+ const jsonFixtureName = 'labels/project_labels.json';
+ const dummyEndpoint = '/dummy/endpoint';
+
+ preloadFixtures(jsonFixtureName);
+ const labelData = getJSONFixture(jsonFixtureName);
+ const findLabel = tokenValue => labelData.find(
+ label => tokenValue === `~${gl.DropdownUtils.getEscapedText(label.title)}`,
+ );
+
+ const bugLabelToken = FilteredSearchSpecHelper.createFilterVisualToken('label', '~bug');
+ const missingLabelToken = FilteredSearchSpecHelper.createFilterVisualToken('label', '~doesnotexist');
+ const spaceLabelToken = FilteredSearchSpecHelper.createFilterVisualToken('label', '~"some space"');
+
+ const parseColor = (color) => {
+ const dummyElement = document.createElement('div');
+ dummyElement.style.color = color;
+ return dummyElement.style.color;
+ };
+
+ beforeEach(() => {
+ tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(`
+ ${bugLabelToken.outerHTML}
+ ${missingLabelToken.outerHTML}
+ ${spaceLabelToken.outerHTML}
+ `);
+
+ const filteredSearchInput = document.querySelector('.filtered-search');
+ filteredSearchInput.dataset.baseEndpoint = dummyEndpoint;
+
+ AjaxCache.internalStorage = { };
+ AjaxCache.internalStorage[`${dummyEndpoint}/labels.json`] = labelData;
+ });
+
+ const testCase = (token, done) => {
+ const tokenValueContainer = token.querySelector('.value-container');
+ const tokenValue = token.querySelector('.value').innerText;
+ const label = findLabel(tokenValue);
+
+ gl.FilteredSearchVisualTokens.updateLabelTokenColor(tokenValueContainer, tokenValue)
+ .then(() => {
+ if (label) {
+ expect(tokenValueContainer.getAttribute('style')).not.toBe(null);
+ expect(tokenValueContainer.style.backgroundColor).toBe(parseColor(label.color));
+ expect(tokenValueContainer.style.color).toBe(parseColor(label.text_color));
+ } else {
+ expect(token).toBe(missingLabelToken);
+ expect(tokenValueContainer.getAttribute('style')).toBe(null);
+ }
+ })
+ .then(done)
+ .catch(fail);
+ };
+
+ it('updates the color of a label token', done => testCase(bugLabelToken, done));
+ it('updates the color of a label token with spaces', done => testCase(spaceLabelToken, done));
+ it('does not change color of a missing label', done => testCase(missingLabelToken, done));
+ });
});
diff --git a/spec/javascripts/filtered_search/recent_searches_root_spec.js b/spec/javascripts/filtered_search/recent_searches_root_spec.js
new file mode 100644
index 00000000000..d8ba6de5f45
--- /dev/null
+++ b/spec/javascripts/filtered_search/recent_searches_root_spec.js
@@ -0,0 +1,31 @@
+import RecentSearchesRoot from '~/filtered_search/recent_searches_root';
+import * as vueSrc from 'vue';
+
+describe('RecentSearchesRoot', () => {
+ describe('render', () => {
+ let recentSearchesRoot;
+ let data;
+ let template;
+
+ beforeEach(() => {
+ recentSearchesRoot = {
+ store: {
+ state: 'state',
+ },
+ };
+
+ spyOn(vueSrc, 'default').and.callFake((options) => {
+ data = options.data;
+ template = options.template;
+ });
+
+ RecentSearchesRoot.prototype.render.call(recentSearchesRoot);
+ });
+
+ it('should instantiate Vue', () => {
+ expect(vueSrc.default).toHaveBeenCalled();
+ expect(data()).toBe(recentSearchesRoot.store.state);
+ expect(template).toContain(':is-local-storage-available="isLocalStorageAvailable"');
+ });
+ });
+});
diff --git a/spec/javascripts/filtered_search/services/recent_searches_service_error_spec.js b/spec/javascripts/filtered_search/services/recent_searches_service_error_spec.js
new file mode 100644
index 00000000000..ea7c146fa4f
--- /dev/null
+++ b/spec/javascripts/filtered_search/services/recent_searches_service_error_spec.js
@@ -0,0 +1,18 @@
+import RecentSearchesServiceError from '~/filtered_search/services/recent_searches_service_error';
+
+describe('RecentSearchesServiceError', () => {
+ let recentSearchesServiceError;
+
+ beforeEach(() => {
+ recentSearchesServiceError = new RecentSearchesServiceError();
+ });
+
+ it('instantiates an instance of RecentSearchesServiceError and not an Error', () => {
+ expect(recentSearchesServiceError).toEqual(jasmine.any(RecentSearchesServiceError));
+ expect(recentSearchesServiceError.name).toBe('RecentSearchesServiceError');
+ });
+
+ it('should set a default message', () => {
+ expect(recentSearchesServiceError.message).toBe('Recent Searches Service is unavailable');
+ });
+});
diff --git a/spec/javascripts/filtered_search/services/recent_searches_service_spec.js b/spec/javascripts/filtered_search/services/recent_searches_service_spec.js
index c255bf7c939..31fa478804a 100644
--- a/spec/javascripts/filtered_search/services/recent_searches_service_spec.js
+++ b/spec/javascripts/filtered_search/services/recent_searches_service_spec.js
@@ -1,6 +1,7 @@
/* eslint-disable promise/catch-or-return */
import RecentSearchesService from '~/filtered_search/services/recent_searches_service';
+import AccessorUtilities from '~/lib/utils/accessor';
describe('RecentSearchesService', () => {
let service;
@@ -11,6 +12,10 @@ describe('RecentSearchesService', () => {
});
describe('fetch', () => {
+ beforeEach(() => {
+ spyOn(RecentSearchesService, 'isAvailable').and.returnValue(true);
+ });
+
it('should default to empty array', (done) => {
const fetchItemsPromise = service.fetch();
@@ -29,11 +34,21 @@ describe('RecentSearchesService', () => {
const fetchItemsPromise = service.fetch();
fetchItemsPromise
- .catch(() => {
+ .catch((error) => {
+ expect(error).toEqual(jasmine.any(SyntaxError));
done();
});
});
+ it('should reject when service is unavailable', (done) => {
+ RecentSearchesService.isAvailable.and.returnValue(false);
+
+ service.fetch().catch((error) => {
+ expect(error).toEqual(jasmine.any(Error));
+ done();
+ });
+ });
+
it('should return items from localStorage', (done) => {
window.localStorage.setItem(service.localStorageKey, '["foo", "bar"]');
const fetchItemsPromise = service.fetch();
@@ -44,15 +59,89 @@ describe('RecentSearchesService', () => {
done();
});
});
+
+ describe('if .isAvailable returns `false`', () => {
+ beforeEach(() => {
+ RecentSearchesService.isAvailable.and.returnValue(false);
+
+ spyOn(window.localStorage, 'getItem');
+
+ RecentSearchesService.prototype.fetch();
+ });
+
+ it('should not call .getItem', () => {
+ expect(window.localStorage.getItem).not.toHaveBeenCalled();
+ });
+ });
});
describe('setRecentSearches', () => {
+ beforeEach(() => {
+ spyOn(RecentSearchesService, 'isAvailable').and.returnValue(true);
+ });
+
it('should save things in localStorage', () => {
const items = ['foo', 'bar'];
service.save(items);
- const newLocalStorageValue =
- window.localStorage.getItem(service.localStorageKey);
+ const newLocalStorageValue = window.localStorage.getItem(service.localStorageKey);
expect(JSON.parse(newLocalStorageValue)).toEqual(items);
});
});
+
+ describe('save', () => {
+ beforeEach(() => {
+ spyOn(window.localStorage, 'setItem');
+ spyOn(RecentSearchesService, 'isAvailable');
+ });
+
+ describe('if .isAvailable returns `true`', () => {
+ const searchesString = 'searchesString';
+ const localStorageKey = 'localStorageKey';
+ const recentSearchesService = {
+ localStorageKey,
+ };
+
+ beforeEach(() => {
+ RecentSearchesService.isAvailable.and.returnValue(true);
+
+ spyOn(JSON, 'stringify').and.returnValue(searchesString);
+
+ RecentSearchesService.prototype.save.call(recentSearchesService);
+ });
+
+ it('should call .setItem', () => {
+ expect(window.localStorage.setItem).toHaveBeenCalledWith(localStorageKey, searchesString);
+ });
+ });
+
+ describe('if .isAvailable returns `false`', () => {
+ beforeEach(() => {
+ RecentSearchesService.isAvailable.and.returnValue(false);
+
+ RecentSearchesService.prototype.save();
+ });
+
+ it('should not call .setItem', () => {
+ expect(window.localStorage.setItem).not.toHaveBeenCalled();
+ });
+ });
+ });
+
+ describe('isAvailable', () => {
+ let isAvailable;
+
+ beforeEach(() => {
+ spyOn(AccessorUtilities, 'isLocalStorageAccessSafe').and.callThrough();
+
+ isAvailable = RecentSearchesService.isAvailable();
+ });
+
+ it('should call .isLocalStorageAccessSafe', () => {
+ expect(AccessorUtilities.isLocalStorageAccessSafe).toHaveBeenCalled();
+ });
+
+ it('should return a boolean', () => {
+ expect(typeof isAvailable).toBe('boolean');
+ });
+ });
});
diff --git a/spec/javascripts/fixtures/balsamiq.rb b/spec/javascripts/fixtures/balsamiq.rb
new file mode 100644
index 00000000000..b5372821bf5
--- /dev/null
+++ b/spec/javascripts/fixtures/balsamiq.rb
@@ -0,0 +1,18 @@
+require 'spec_helper'
+
+describe 'Balsamiq file', '(JavaScript fixtures)', type: :controller do
+ include JavaScriptFixturesHelpers
+
+ let(:namespace) { create(:namespace, name: 'frontend-fixtures' )}
+ let(:project) { create(:project, namespace: namespace, path: 'balsamiq-project') }
+
+ before(:all) do
+ clean_frontend_fixtures('blob/balsamiq/')
+ end
+
+ it 'blob/balsamiq/test.bmpr' do |example|
+ blob = project.repository.blob_at('b89b56d79', 'files/images/balsamiq.bmpr')
+
+ store_frontend_fixture(blob.data.force_encoding('utf-8'), example.description)
+ end
+end
diff --git a/spec/javascripts/fixtures/balsamiq_viewer.html.haml b/spec/javascripts/fixtures/balsamiq_viewer.html.haml
new file mode 100644
index 00000000000..18166ba4901
--- /dev/null
+++ b/spec/javascripts/fixtures/balsamiq_viewer.html.haml
@@ -0,0 +1 @@
+.file-content.balsamiq-viewer#js-balsamiq-viewer{ data: { endpoint: '/test' } }
diff --git a/spec/javascripts/fixtures/blob.rb b/spec/javascripts/fixtures/blob.rb
new file mode 100644
index 00000000000..16490ad5039
--- /dev/null
+++ b/spec/javascripts/fixtures/blob.rb
@@ -0,0 +1,29 @@
+require 'spec_helper'
+
+describe Projects::BlobController, '(JavaScript fixtures)', type: :controller do
+ include JavaScriptFixturesHelpers
+
+ let(:admin) { create(:admin) }
+ let(:namespace) { create(:namespace, name: 'frontend-fixtures' )}
+ let(:project) { create(:project, :repository, namespace: namespace, path: 'branches-project') }
+
+ render_views
+
+ before(:all) do
+ clean_frontend_fixtures('blob/')
+ end
+
+ before(:each) do
+ sign_in(admin)
+ end
+
+ it 'blob/show.html.raw' do |example|
+ get(:show,
+ namespace_id: project.namespace,
+ project_id: project,
+ id: 'add-ipython-files/files/ipython/basic.ipynb')
+
+ expect(response).to be_success
+ store_frontend_fixture(response, example.description)
+ end
+end
diff --git a/spec/javascripts/fixtures/deploy_keys.rb b/spec/javascripts/fixtures/deploy_keys.rb
new file mode 100644
index 00000000000..16e598a4b29
--- /dev/null
+++ b/spec/javascripts/fixtures/deploy_keys.rb
@@ -0,0 +1,36 @@
+require 'spec_helper'
+
+describe Projects::DeployKeysController, '(JavaScript fixtures)', type: :controller do
+ include JavaScriptFixturesHelpers
+
+ let(:admin) { create(:admin) }
+ let(:namespace) { create(:namespace, name: 'frontend-fixtures' )}
+ let(:project) { create(:project_empty_repo, namespace: namespace, path: 'todos-project') }
+ let(:project2) { create(:empty_project, :internal)}
+
+ before(:all) do
+ clean_frontend_fixtures('deploy_keys/')
+ end
+
+ before(:each) do
+ sign_in(admin)
+ end
+
+ render_views
+
+ it 'deploy_keys/keys.json' do |example|
+ create(:deploy_key, public: true)
+ project_key = create(:deploy_key, key: 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQCdMHEHyhRjbhEZVddFn6lTWdgEy5Q6Bz4nwGB76xWZI5YT/1WJOMEW+sL5zYd31kk7sd3FJ5L9ft8zWMWrr/iWXQikC2cqZK24H1xy+ZUmrRuJD4qGAaIVoyyzBL+avL+lF8J5lg6YSw8gwJY/lX64/vnJHUlWw2n5BF8IFOWhiw== dummy@gitlab.com')
+ internal_key = create(:deploy_key, key: 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQDNd/UJWhPrpb+b/G5oL109y57yKuCxE+WUGJGYaj7WQKsYRJmLYh1mgjrl+KVyfsWpq4ylOxIfFSnN9xBBFN8mlb0Fma5DC7YsSsibJr3MZ19ZNBprwNcdogET7aW9I0In7Wu5f2KqI6e5W/spJHCy4JVxzVMUvk6Myab0LnJ2iQ== dummy@gitlab.com')
+ create(:deploy_keys_project, project: project, deploy_key: project_key)
+ create(:deploy_keys_project, project: project2, deploy_key: internal_key)
+
+ get :index,
+ namespace_id: project.namespace.to_param,
+ project_id: project,
+ format: :json
+
+ expect(response).to be_success
+ store_frontend_fixture(response, example.description)
+ end
+end
diff --git a/spec/javascripts/fixtures/environments.rb b/spec/javascripts/fixtures/environments.rb
new file mode 100644
index 00000000000..3474f4696ef
--- /dev/null
+++ b/spec/javascripts/fixtures/environments.rb
@@ -0,0 +1,30 @@
+require 'spec_helper'
+
+describe Projects::EnvironmentsController, '(JavaScript fixtures)', type: :controller do
+ include JavaScriptFixturesHelpers
+
+ let(:admin) { create(:admin) }
+ let(:namespace) { create(:namespace, name: 'frontend-fixtures' )}
+ let(:project) { create(:project_empty_repo, namespace: namespace, path: 'environments-project') }
+ let(:environment) { create(:environment, name: 'production', project: project) }
+
+ render_views
+
+ before(:all) do
+ clean_frontend_fixtures('environments/metrics')
+ end
+
+ before(:each) do
+ sign_in(admin)
+ end
+
+ it 'environments/metrics/metrics.html.raw' do |example|
+ get :metrics,
+ namespace_id: project.namespace,
+ project_id: project,
+ id: environment.id
+
+ expect(response).to be_success
+ store_frontend_fixture(response, example.description)
+ end
+end
diff --git a/spec/javascripts/fixtures/environments/metrics.html.haml b/spec/javascripts/fixtures/environments/metrics.html.haml
deleted file mode 100644
index e2dd9519898..00000000000
--- a/spec/javascripts/fixtures/environments/metrics.html.haml
+++ /dev/null
@@ -1,62 +0,0 @@
-.prometheus-container{ 'data-has-metrics': "false", 'data-doc-link': '/help/administration/monitoring/prometheus/index.md', 'data-prometheus-integration': '/root/hello-prometheus/services/prometheus/edit' }
- .top-area
- .row
- .col-sm-6
- %h3.page-title
- Metrics for environment
- .prometheus-state
- .js-getting-started.hidden
- .row
- .col-md-4.col-md-offset-4.state-svg
- %svg
- .row
- .col-md-6.col-md-offset-3
- %h4.text-center.state-title
- Get started with performance monitoring
- .row
- .col-md-6.col-md-offset-3
- .description-text.text-center.state-description
- Stay updated about the performance and health of your environment by configuring Prometheus to monitor your deployments. Learn more about performance monitoring
- .row.state-button-section
- .col-md-4.col-md-offset-4.text-center.state-button
- %a.btn.btn-success
- Configure Prometheus
- .js-loading.hidden
- .row
- .col-md-4.col-md-offset-4.state-svg
- %svg
- .row
- .col-md-6.col-md-offset-3
- %h4.text-center.state-title
- Waiting for performance data
- .row
- .col-md-6.col-md-offset-3
- .description-text.text-center.state-description
- Creating graphs uses the data from the Prometheus server. If this takes a long time, ensure that data is available.
- .row.state-button-section
- .col-md-4.col-md-offset-4.text-center.state-button
- %a.btn.btn-success
- View documentation
- .js-unable-to-connect.hidden
- .row
- .col-md-4.col-md-offset-4.state-svg
- %svg
- .row
- .col-md-6.col-md-offset-3
- %h4.text-center.state-title
- Unable to connect to Prometheus server
- .row
- .col-md-6.col-md-offset-3
- .description-text.text-center.state-description
- Ensure connectivity is available from the GitLab server to the Prometheus server
- .row.state-button-section
- .col-md-4.col-md-offset-4.text-center.state-button
- %a.btn.btn-success
- View documentation
- .prometheus-graphs
- .row
- .col-sm-12
- %svg.prometheus-graph{ 'graph-type' => 'cpu_values' }
- .row
- .col-sm-12
- %svg.prometheus-graph{ 'graph-type' => 'memory_values' }
diff --git a/spec/javascripts/fixtures/graph.html.haml b/spec/javascripts/fixtures/graph.html.haml
new file mode 100644
index 00000000000..4fedb0f1ded
--- /dev/null
+++ b/spec/javascripts/fixtures/graph.html.haml
@@ -0,0 +1 @@
+#js-pipeline-graph-vue{ data: { endpoint: "foo" } }
diff --git a/spec/javascripts/fixtures/labels.rb b/spec/javascripts/fixtures/labels.rb
new file mode 100644
index 00000000000..2e4811b64a4
--- /dev/null
+++ b/spec/javascripts/fixtures/labels.rb
@@ -0,0 +1,56 @@
+require 'spec_helper'
+
+describe 'Labels (JavaScript fixtures)' do
+ include JavaScriptFixturesHelpers
+
+ let(:admin) { create(:admin) }
+ let(:group) { create(:group, name: 'frontend-fixtures-group' )}
+ let(:project) { create(:project_empty_repo, namespace: group, path: 'labels-project') }
+
+ let!(:project_label_bug) { create(:label, project: project, title: 'bug', color: '#FF0000') }
+ let!(:project_label_enhancement) { create(:label, project: project, title: 'enhancement', color: '#00FF00') }
+ let!(:project_label_feature) { create(:label, project: project, title: 'feature', color: '#0000FF') }
+
+ let!(:group_label_roses) { create(:group_label, group: group, title: 'roses', color: '#FF0000') }
+ let!(:groub_label_space) { create(:group_label, group: group, title: 'some space', color: '#FFFFFF') }
+ let!(:groub_label_violets) { create(:group_label, group: group, title: 'violets', color: '#0000FF') }
+
+ before(:all) do
+ clean_frontend_fixtures('labels/')
+ end
+
+ describe Groups::LabelsController, '(JavaScript fixtures)', type: :controller do
+ render_views
+
+ before(:each) do
+ sign_in(admin)
+ end
+
+ it 'labels/group_labels.json' do |example|
+ get :index,
+ group_id: group,
+ format: 'json'
+
+ expect(response).to be_success
+ store_frontend_fixture(response, example.description)
+ end
+ end
+
+ describe Projects::LabelsController, '(JavaScript fixtures)', type: :controller do
+ render_views
+
+ before(:each) do
+ sign_in(admin)
+ end
+
+ it 'labels/project_labels.json' do |example|
+ get :index,
+ namespace_id: group,
+ project_id: project,
+ format: 'json'
+
+ expect(response).to be_success
+ store_frontend_fixture(response, example.description)
+ end
+ end
+end
diff --git a/spec/javascripts/fixtures/line_highlighter.html.haml b/spec/javascripts/fixtures/line_highlighter.html.haml
index 514877340e4..2782c50e298 100644
--- a/spec/javascripts/fixtures/line_highlighter.html.haml
+++ b/spec/javascripts/fixtures/line_highlighter.html.haml
@@ -1,4 +1,4 @@
-#blob-content-holder
+.file-holder
.file-content
.line-numbers
- 1.upto(25) do |i|
diff --git a/spec/javascripts/fixtures/merge_requests.rb b/spec/javascripts/fixtures/merge_requests.rb
index 47d904b865b..a746a776548 100644
--- a/spec/javascripts/fixtures/merge_requests.rb
+++ b/spec/javascripts/fixtures/merge_requests.rb
@@ -16,6 +16,16 @@ describe Projects::MergeRequestsController, '(JavaScript fixtures)', type: :cont
sha: merge_request.diff_head_sha
)
end
+ let(:path) { "files/ruby/popen.rb" }
+ let(:position) do
+ Gitlab::Diff::Position.new(
+ old_path: path,
+ new_path: path,
+ old_line: nil,
+ new_line: 14,
+ diff_refs: merge_request.diff_refs
+ )
+ end
render_views
@@ -39,6 +49,12 @@ describe Projects::MergeRequestsController, '(JavaScript fixtures)', type: :cont
render_merge_request(example.description, merged_merge_request)
end
+ it 'merge_requests/diff_comment.html.raw' do |example|
+ create(:diff_note_on_merge_request, project: project, author: admin, position: position, noteable: merge_request)
+ create(:note_on_merge_request, author: admin, project: project, noteable: merge_request)
+ render_merge_request(example.description, merge_request)
+ end
+
private
def render_merge_request(fixture_file_name, merge_request)
diff --git a/spec/javascripts/fixtures/mini_dropdown_graph.html.haml b/spec/javascripts/fixtures/mini_dropdown_graph.html.haml
index 29370b974af..b532b48a95b 100644
--- a/spec/javascripts/fixtures/mini_dropdown_graph.html.haml
+++ b/spec/javascripts/fixtures/mini_dropdown_graph.html.haml
@@ -3,7 +3,7 @@
Dropdown
%ul.dropdown-menu.mini-pipeline-graph-dropdown-menu.js-builds-dropdown-container
- .js-builds-dropdown-list.scrollable-menu
+ %li.js-builds-dropdown-list.scrollable-menu
- .js-builds-dropdown-loading.builds-dropdown-loading.hidden
- %span.fa.fa-spinner.fa-spin
+ %li.js-builds-dropdown-loading.hidden
+ %span.fa.fa-spinner
diff --git a/spec/javascripts/fixtures/pdf.rb b/spec/javascripts/fixtures/pdf.rb
new file mode 100644
index 00000000000..6b2422a7986
--- /dev/null
+++ b/spec/javascripts/fixtures/pdf.rb
@@ -0,0 +1,18 @@
+require 'spec_helper'
+
+describe 'PDF file', '(JavaScript fixtures)', type: :controller do
+ include JavaScriptFixturesHelpers
+
+ let(:namespace) { create(:namespace, name: 'frontend-fixtures' )}
+ let(:project) { create(:project, namespace: namespace, path: 'pdf-project') }
+
+ before(:all) do
+ clean_frontend_fixtures('blob/pdf/')
+ end
+
+ it 'blob/pdf/test.pdf' do |example|
+ blob = project.repository.blob_at('e774ebd33', 'files/pdf/test.pdf')
+
+ store_frontend_fixture(blob.data.force_encoding("utf-8"), example.description)
+ end
+end
diff --git a/spec/javascripts/fixtures/pipelines.rb b/spec/javascripts/fixtures/pipelines.rb
new file mode 100644
index 00000000000..daafbac86db
--- /dev/null
+++ b/spec/javascripts/fixtures/pipelines.rb
@@ -0,0 +1,35 @@
+require 'spec_helper'
+
+describe Projects::PipelinesController, '(JavaScript fixtures)', type: :controller do
+ include JavaScriptFixturesHelpers
+
+ let(:admin) { create(:admin) }
+ let(:namespace) { create(:namespace, name: 'frontend-fixtures' )}
+ let(:project) { create(:project, :repository, namespace: namespace, path: 'pipelines-project') }
+ let(:commit) { create(:commit, project: project) }
+ let(:commit_without_author) { RepoHelpers.another_sample_commit }
+ let!(:user) { create(:user, email: commit.author_email) }
+ let!(:pipeline) { create(:ci_pipeline, project: project, sha: commit.id, user: user) }
+ let!(:pipeline_without_author) { create(:ci_pipeline, project: project, sha: commit_without_author.id) }
+ let!(:pipeline_without_commit) { create(:ci_pipeline, project: project, sha: '0000') }
+
+ render_views
+
+ before(:all) do
+ clean_frontend_fixtures('pipelines/')
+ end
+
+ before(:each) do
+ sign_in(admin)
+ end
+
+ it 'pipelines/pipelines.json' do |example|
+ get :index,
+ namespace_id: namespace,
+ project_id: project,
+ format: :json
+
+ expect(response).to be_success
+ store_frontend_fixture(response, example.description)
+ end
+end
diff --git a/spec/javascripts/fixtures/raw.rb b/spec/javascripts/fixtures/raw.rb
new file mode 100644
index 00000000000..1ce622fc836
--- /dev/null
+++ b/spec/javascripts/fixtures/raw.rb
@@ -0,0 +1,24 @@
+require 'spec_helper'
+
+describe 'Raw files', '(JavaScript fixtures)', type: :controller do
+ include JavaScriptFixturesHelpers
+
+ let(:namespace) { create(:namespace, name: 'frontend-fixtures' )}
+ let(:project) { create(:project, namespace: namespace, path: 'raw-project') }
+
+ before(:all) do
+ clean_frontend_fixtures('blob/notebook/')
+ end
+
+ it 'blob/notebook/basic.json' do |example|
+ blob = project.repository.blob_at('6d85bb69', 'files/ipython/basic.ipynb')
+
+ store_frontend_fixture(blob.data, example.description)
+ end
+
+ it 'blob/notebook/worksheets.json' do |example|
+ blob = project.repository.blob_at('6d85bb69', 'files/ipython/worksheets.ipynb')
+
+ store_frontend_fixture(blob.data, example.description)
+ end
+end
diff --git a/spec/javascripts/gl_dropdown_spec.js b/spec/javascripts/gl_dropdown_spec.js
index c207fb00a47..8f90ed69e64 100644
--- a/spec/javascripts/gl_dropdown_spec.js
+++ b/spec/javascripts/gl_dropdown_spec.js
@@ -2,7 +2,6 @@
require('~/gl_dropdown');
require('~/lib/utils/common_utils');
-require('~/lib/utils/type_utility');
require('~/lib/utils/url_utility');
(() => {
@@ -44,21 +43,18 @@ require('~/lib/utils/url_utility');
preloadFixtures('static/gl_dropdown.html.raw');
loadJSONFixtures('projects.json');
- function initDropDown(hasRemote, isFilterable) {
- this.dropdownButtonElement = $('#js-project-dropdown', this.dropdownContainerElement).glDropdown({
+ function initDropDown(hasRemote, isFilterable, extraOpts = {}) {
+ const options = Object.assign({
selectable: true,
filterable: isFilterable,
data: hasRemote ? remoteMock.bind({}, this.projectsData) : this.projectsData,
search: {
fields: ['name']
},
- text: (project) => {
- (project.name_with_namespace || project.name);
- },
- id: (project) => {
- project.id;
- }
- });
+ text: project => (project.name_with_namespace || project.name),
+ id: project => project.id,
+ }, extraOpts);
+ this.dropdownButtonElement = $('#js-project-dropdown', this.dropdownContainerElement).glDropdown(options);
}
beforeEach(() => {
@@ -80,6 +76,37 @@ require('~/lib/utils/url_utility');
expect(this.dropdownContainerElement).toHaveClass('open');
});
+ it('escapes HTML as text', () => {
+ this.projectsData[0].name_with_namespace = '<script>alert("testing");</script>';
+
+ initDropDown.call(this, false);
+
+ this.dropdownButtonElement.click();
+
+ expect(
+ $('.dropdown-content li:first-child').text(),
+ ).toBe('<script>alert("testing");</script>');
+ });
+
+ it('should output HTML when highlighting', () => {
+ this.projectsData[0].name_with_namespace = 'testing';
+ $('.dropdown-input .dropdown-input-field').val('test');
+
+ initDropDown.call(this, false, true, {
+ highlight: true,
+ });
+
+ this.dropdownButtonElement.click();
+
+ expect(
+ $('.dropdown-content li:first-child').text(),
+ ).toBe('testing');
+
+ expect(
+ $('.dropdown-content li:first-child a').html(),
+ ).toBe('<b>t</b><b>e</b><b>s</b><b>t</b>ing');
+ });
+
describe('that is open', () => {
beforeEach(() => {
initDropDown.call(this, false, false);
diff --git a/spec/javascripts/gl_form_spec.js b/spec/javascripts/gl_form_spec.js
index 71d6e2a7e22..5ed18d0a76b 100644
--- a/spec/javascripts/gl_form_spec.js
+++ b/spec/javascripts/gl_form_spec.js
@@ -32,7 +32,7 @@ describe('GLForm', () => {
});
});
- describe('.setupAutosize', () => {
+ describe('setupAutosize', () => {
beforeEach((done) => {
this.glForm.setupAutosize();
setTimeout(() => {
@@ -59,7 +59,7 @@ describe('GLForm', () => {
});
});
- describe('.setHeightData', () => {
+ describe('setHeightData', () => {
beforeEach(() => {
spyOn($.prototype, 'data');
spyOn($.prototype, 'outerHeight').and.returnValue(200);
@@ -75,7 +75,7 @@ describe('GLForm', () => {
});
});
- describe('.destroyAutosize', () => {
+ describe('destroyAutosize', () => {
describe('when called', () => {
beforeEach(() => {
spyOn($.prototype, 'data');
diff --git a/spec/javascripts/helpers/class_spec_helper_spec.js b/spec/javascripts/helpers/class_spec_helper_spec.js
index 0a61e561640..a1cfc8ba820 100644
--- a/spec/javascripts/helpers/class_spec_helper_spec.js
+++ b/spec/javascripts/helpers/class_spec_helper_spec.js
@@ -3,7 +3,7 @@
require('./class_spec_helper');
describe('ClassSpecHelper', () => {
- describe('.itShouldBeAStaticMethod', function () {
+ describe('itShouldBeAStaticMethod', function () {
beforeEach(() => {
class TestClass {
instanceMethod() { this.prop = 'val'; }
diff --git a/spec/javascripts/helpers/filtered_search_spec_helper.js b/spec/javascripts/helpers/filtered_search_spec_helper.js
index ce83a256ddd..b8d4a93b1ab 100644
--- a/spec/javascripts/helpers/filtered_search_spec_helper.js
+++ b/spec/javascripts/helpers/filtered_search_spec_helper.js
@@ -10,7 +10,12 @@ class FilteredSearchSpecHelper {
li.innerHTML = `
<div class="selectable ${isSelected ? 'selected' : ''}" role="button">
<div class="name">${name}</div>
- <div class="value">${value}</div>
+ <div class="value-container">
+ <div class="value">${value}</div>
+ <div class="remove-token" role="button">
+ <i class="fa fa-close"></i>
+ </div>
+ </div>
</div>
`;
diff --git a/spec/javascripts/helpers/user_mock_data_helper.js b/spec/javascripts/helpers/user_mock_data_helper.js
new file mode 100644
index 00000000000..a9783ea065c
--- /dev/null
+++ b/spec/javascripts/helpers/user_mock_data_helper.js
@@ -0,0 +1,16 @@
+export default {
+ createNumberRandomUsers(numberUsers) {
+ const users = [];
+ for (let i = 0; i < numberUsers; i = i += 1) {
+ users.push(
+ {
+ avatar: 'http://gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ id: (i + 1),
+ name: `GitLab User ${i}`,
+ username: `gitlab${i}`,
+ },
+ );
+ }
+ return users;
+ },
+};
diff --git a/spec/javascripts/issuable_time_tracker_spec.js b/spec/javascripts/issuable_time_tracker_spec.js
index 0a830f25e29..8ff93c4f918 100644
--- a/spec/javascripts/issuable_time_tracker_spec.js
+++ b/spec/javascripts/issuable_time_tracker_spec.js
@@ -2,7 +2,7 @@
import Vue from 'vue';
-require('~/issuable/time_tracking/components/time_tracker');
+import timeTracker from '~/sidebar/components/time_tracking/time_tracker';
function initTimeTrackingComponent(opts) {
setFixtures(`
@@ -16,187 +16,185 @@ function initTimeTrackingComponent(opts) {
time_spent: opts.timeSpent,
human_time_estimate: opts.timeEstimateHumanReadable,
human_time_spent: opts.timeSpentHumanReadable,
- docsUrl: '/help/workflow/time_tracking.md',
+ rootPath: '/',
};
- const TimeTrackingComponent = Vue.component('issuable-time-tracker');
+ const TimeTrackingComponent = Vue.extend(timeTracker);
this.timeTracker = new TimeTrackingComponent({
el: '#mock-container',
propsData: this.initialData,
});
}
-((gl) => {
- describe('Issuable Time Tracker', function() {
- describe('Initialization', function() {
- beforeEach(function() {
- initTimeTrackingComponent.call(this, { timeEstimate: 100000, timeSpent: 5000, timeEstimateHumanReadable: '2h 46m', timeSpentHumanReadable: '1h 23m' });
- });
+describe('Issuable Time Tracker', function() {
+ describe('Initialization', function() {
+ beforeEach(function() {
+ initTimeTrackingComponent.call(this, { timeEstimate: 100000, timeSpent: 5000, timeEstimateHumanReadable: '2h 46m', timeSpentHumanReadable: '1h 23m' });
+ });
- it('should return something defined', function() {
- expect(this.timeTracker).toBeDefined();
- });
+ it('should return something defined', function() {
+ expect(this.timeTracker).toBeDefined();
+ });
- it ('should correctly set timeEstimate', function(done) {
- Vue.nextTick(() => {
- expect(this.timeTracker.timeEstimate).toBe(this.initialData.time_estimate);
- done();
- });
+ it ('should correctly set timeEstimate', function(done) {
+ Vue.nextTick(() => {
+ expect(this.timeTracker.timeEstimate).toBe(this.initialData.time_estimate);
+ done();
});
- it ('should correctly set time_spent', function(done) {
- Vue.nextTick(() => {
- expect(this.timeTracker.timeSpent).toBe(this.initialData.time_spent);
- done();
- });
+ });
+ it ('should correctly set time_spent', function(done) {
+ Vue.nextTick(() => {
+ expect(this.timeTracker.timeSpent).toBe(this.initialData.time_spent);
+ done();
});
});
+ });
- describe('Content Display', function() {
- describe('Panes', function() {
- describe('Comparison pane', function() {
- beforeEach(function() {
- initTimeTrackingComponent.call(this, { timeEstimate: 100000, timeSpent: 5000, timeEstimateHumanReadable: '', timeSpentHumanReadable: '' });
+ describe('Content Display', function() {
+ describe('Panes', function() {
+ describe('Comparison pane', function() {
+ beforeEach(function() {
+ initTimeTrackingComponent.call(this, { timeEstimate: 100000, timeSpent: 5000, timeEstimateHumanReadable: '', timeSpentHumanReadable: '' });
+ });
+
+ it('should show the "Comparison" pane when timeEstimate and time_spent are truthy', function(done) {
+ Vue.nextTick(() => {
+ const $comparisonPane = this.timeTracker.$el.querySelector('.time-tracking-comparison-pane');
+ expect(this.timeTracker.showComparisonState).toBe(true);
+ done();
});
+ });
- it('should show the "Comparison" pane when timeEstimate and time_spent are truthy', function(done) {
+ describe('Remaining meter', function() {
+ it('should display the remaining meter with the correct width', function(done) {
Vue.nextTick(() => {
- const $comparisonPane = this.timeTracker.$el.querySelector('.time-tracking-comparison-pane');
- expect(this.timeTracker.showComparisonState).toBe(true);
+ const meterWidth = this.timeTracker.$el.querySelector('.time-tracking-comparison-pane .meter-fill').style.width;
+ const correctWidth = '5%';
+
+ expect(meterWidth).toBe(correctWidth);
done();
- });
+ })
});
- describe('Remaining meter', function() {
- it('should display the remaining meter with the correct width', function(done) {
- Vue.nextTick(() => {
- const meterWidth = this.timeTracker.$el.querySelector('.time-tracking-comparison-pane .meter-fill').style.width;
- const correctWidth = '5%';
-
- expect(meterWidth).toBe(correctWidth);
- done();
- })
- });
-
- it('should display the remaining meter with the correct background color when within estimate', function(done) {
- Vue.nextTick(() => {
- const styledMeter = $(this.timeTracker.$el).find('.time-tracking-comparison-pane .within_estimate .meter-fill');
- expect(styledMeter.length).toBe(1);
- done()
- });
+ it('should display the remaining meter with the correct background color when within estimate', function(done) {
+ Vue.nextTick(() => {
+ const styledMeter = $(this.timeTracker.$el).find('.time-tracking-comparison-pane .within_estimate .meter-fill');
+ expect(styledMeter.length).toBe(1);
+ done()
});
+ });
- it('should display the remaining meter with the correct background color when over estimate', function(done) {
- this.timeTracker.time_estimate = 100000;
- this.timeTracker.time_spent = 20000000;
- Vue.nextTick(() => {
- const styledMeter = $(this.timeTracker.$el).find('.time-tracking-comparison-pane .over_estimate .meter-fill');
- expect(styledMeter.length).toBe(1);
- done();
- });
+ it('should display the remaining meter with the correct background color when over estimate', function(done) {
+ this.timeTracker.time_estimate = 100000;
+ this.timeTracker.time_spent = 20000000;
+ Vue.nextTick(() => {
+ const styledMeter = $(this.timeTracker.$el).find('.time-tracking-comparison-pane .over_estimate .meter-fill');
+ expect(styledMeter.length).toBe(1);
+ done();
});
});
});
+ });
- describe("Estimate only pane", function() {
- beforeEach(function() {
- initTimeTrackingComponent.call(this, { timeEstimate: 100000, timeSpent: 0, timeEstimateHumanReadable: '2h 46m', timeSpentHumanReadable: '' });
- });
+ describe("Estimate only pane", function() {
+ beforeEach(function() {
+ initTimeTrackingComponent.call(this, { timeEstimate: 100000, timeSpent: 0, timeEstimateHumanReadable: '2h 46m', timeSpentHumanReadable: '' });
+ });
- it('should display the human readable version of time estimated', function(done) {
- Vue.nextTick(() => {
- const estimateText = this.timeTracker.$el.querySelector('.time-tracking-estimate-only-pane').innerText;
- const correctText = 'Estimated: 2h 46m';
+ it('should display the human readable version of time estimated', function(done) {
+ Vue.nextTick(() => {
+ const estimateText = this.timeTracker.$el.querySelector('.time-tracking-estimate-only-pane').innerText;
+ const correctText = 'Estimated: 2h 46m';
- expect(estimateText).toBe(correctText);
- done();
- });
+ expect(estimateText).toBe(correctText);
+ done();
});
});
+ });
- describe('Spent only pane', function() {
- beforeEach(function() {
- initTimeTrackingComponent.call(this, { timeEstimate: 0, timeSpent: 5000, timeEstimateHumanReadable: '2h 46m', timeSpentHumanReadable: '1h 23m' });
- });
+ describe('Spent only pane', function() {
+ beforeEach(function() {
+ initTimeTrackingComponent.call(this, { timeEstimate: 0, timeSpent: 5000, timeEstimateHumanReadable: '2h 46m', timeSpentHumanReadable: '1h 23m' });
+ });
- it('should display the human readable version of time spent', function(done) {
- Vue.nextTick(() => {
- const spentText = this.timeTracker.$el.querySelector('.time-tracking-spend-only-pane').innerText;
- const correctText = 'Spent: 1h 23m';
+ it('should display the human readable version of time spent', function(done) {
+ Vue.nextTick(() => {
+ const spentText = this.timeTracker.$el.querySelector('.time-tracking-spend-only-pane').innerText;
+ const correctText = 'Spent: 1h 23m';
- expect(spentText).toBe(correctText);
- done();
- });
+ expect(spentText).toBe(correctText);
+ done();
});
});
+ });
- describe('No time tracking pane', function() {
- beforeEach(function() {
- initTimeTrackingComponent.call(this, { timeEstimate: 0, timeSpent: 0, timeEstimateHumanReadable: 0, timeSpentHumanReadable: 0 });
- });
+ describe('No time tracking pane', function() {
+ beforeEach(function() {
+ initTimeTrackingComponent.call(this, { timeEstimate: 0, timeSpent: 0, timeEstimateHumanReadable: '', timeSpentHumanReadable: '' });
+ });
- it('should only show the "No time tracking" pane when both timeEstimate and time_spent are falsey', function(done) {
- Vue.nextTick(() => {
- const $noTrackingPane = this.timeTracker.$el.querySelector('.time-tracking-no-tracking-pane');
- const noTrackingText =$noTrackingPane.innerText;
- const correctText = 'No estimate or time spent';
+ it('should only show the "No time tracking" pane when both timeEstimate and time_spent are falsey', function(done) {
+ Vue.nextTick(() => {
+ const $noTrackingPane = this.timeTracker.$el.querySelector('.time-tracking-no-tracking-pane');
+ const noTrackingText =$noTrackingPane.innerText;
+ const correctText = 'No estimate or time spent';
- expect(this.timeTracker.showNoTimeTrackingState).toBe(true);
- expect($noTrackingPane).toBeVisible();
- expect(noTrackingText).toBe(correctText);
- done();
- });
+ expect(this.timeTracker.showNoTimeTrackingState).toBe(true);
+ expect($noTrackingPane).toBeVisible();
+ expect(noTrackingText).toBe(correctText);
+ done();
});
});
+ });
- describe("Help pane", function() {
- beforeEach(function() {
- initTimeTrackingComponent.call(this, { timeEstimate: 0, timeSpent: 0 });
- });
+ describe("Help pane", function() {
+ beforeEach(function() {
+ initTimeTrackingComponent.call(this, { timeEstimate: 0, timeSpent: 0 });
+ });
- it('should not show the "Help" pane by default', function(done) {
- Vue.nextTick(() => {
- const $helpPane = this.timeTracker.$el.querySelector('.time-tracking-help-state');
+ it('should not show the "Help" pane by default', function(done) {
+ Vue.nextTick(() => {
+ const $helpPane = this.timeTracker.$el.querySelector('.time-tracking-help-state');
- expect(this.timeTracker.showHelpState).toBe(false);
- expect($helpPane).toBeNull();
- done();
- });
+ expect(this.timeTracker.showHelpState).toBe(false);
+ expect($helpPane).toBeNull();
+ done();
});
+ });
- it('should show the "Help" pane when help button is clicked', function(done) {
- Vue.nextTick(() => {
- $(this.timeTracker.$el).find('.help-button').click();
+ it('should show the "Help" pane when help button is clicked', function(done) {
+ Vue.nextTick(() => {
+ $(this.timeTracker.$el).find('.help-button').click();
- setTimeout(() => {
- const $helpPane = this.timeTracker.$el.querySelector('.time-tracking-help-state');
- expect(this.timeTracker.showHelpState).toBe(true);
- expect($helpPane).toBeVisible();
- done();
- }, 10);
- });
+ setTimeout(() => {
+ const $helpPane = this.timeTracker.$el.querySelector('.time-tracking-help-state');
+ expect(this.timeTracker.showHelpState).toBe(true);
+ expect($helpPane).toBeVisible();
+ done();
+ }, 10);
});
+ });
- it('should not show the "Help" pane when help button is clicked and then closed', function(done) {
- Vue.nextTick(() => {
- $(this.timeTracker.$el).find('.help-button').click();
+ it('should not show the "Help" pane when help button is clicked and then closed', function(done) {
+ Vue.nextTick(() => {
+ $(this.timeTracker.$el).find('.help-button').click();
- setTimeout(() => {
+ setTimeout(() => {
- $(this.timeTracker.$el).find('.close-help-button').click();
+ $(this.timeTracker.$el).find('.close-help-button').click();
- setTimeout(() => {
- const $helpPane = this.timeTracker.$el.querySelector('.time-tracking-help-state');
+ setTimeout(() => {
+ const $helpPane = this.timeTracker.$el.querySelector('.time-tracking-help-state');
- expect(this.timeTracker.showHelpState).toBe(false);
- expect($helpPane).toBeNull();
+ expect(this.timeTracker.showHelpState).toBe(false);
+ expect($helpPane).toBeNull();
- done();
- }, 1000);
+ done();
}, 1000);
- });
+ }, 1000);
});
});
});
});
});
-})(window.gl || (window.gl = {}));
+});
diff --git a/spec/javascripts/issue_show/components/app_spec.js b/spec/javascripts/issue_show/components/app_spec.js
new file mode 100644
index 00000000000..09bca2c3680
--- /dev/null
+++ b/spec/javascripts/issue_show/components/app_spec.js
@@ -0,0 +1,60 @@
+import Vue from 'vue';
+import '~/render_math';
+import '~/render_gfm';
+import issuableApp from '~/issue_show/components/app.vue';
+import issueShowData from '../mock_data';
+
+const issueShowInterceptor = data => (request, next) => {
+ next(request.respondWith(JSON.stringify(data), {
+ status: 200,
+ headers: {
+ 'POLL-INTERVAL': 1,
+ },
+ }));
+};
+
+describe('Issuable output', () => {
+ document.body.innerHTML = '<span id="task_status"></span>';
+
+ let vm;
+
+ beforeEach(() => {
+ const IssuableDescriptionComponent = Vue.extend(issuableApp);
+ Vue.http.interceptors.push(issueShowInterceptor(issueShowData.initialRequest));
+
+ vm = new IssuableDescriptionComponent({
+ propsData: {
+ canUpdate: true,
+ endpoint: '/gitlab-org/gitlab-shell/issues/9/rendered_title',
+ issuableRef: '#1',
+ initialTitle: '',
+ initialDescriptionHtml: '',
+ initialDescriptionText: '',
+ },
+ }).$mount();
+ });
+
+ afterEach(() => {
+ Vue.http.interceptors = _.without(Vue.http.interceptors, issueShowInterceptor);
+ });
+
+ it('should render a title/description and update title/description on update', (done) => {
+ setTimeout(() => {
+ expect(document.querySelector('title').innerText).toContain('this is a title (#1)');
+ expect(vm.$el.querySelector('.title').innerHTML).toContain('<p>this is a title</p>');
+ expect(vm.$el.querySelector('.wiki').innerHTML).toContain('<p>this is a description!</p>');
+ expect(vm.$el.querySelector('.js-task-list-field').value).toContain('this is a description');
+
+ Vue.http.interceptors.push(issueShowInterceptor(issueShowData.secondRequest));
+
+ setTimeout(() => {
+ expect(document.querySelector('title').innerText).toContain('2 (#1)');
+ expect(vm.$el.querySelector('.title').innerHTML).toContain('<p>2</p>');
+ expect(vm.$el.querySelector('.wiki').innerHTML).toContain('<p>42</p>');
+ expect(vm.$el.querySelector('.js-task-list-field').value).toContain('42');
+
+ done();
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/issue_show/components/description_spec.js b/spec/javascripts/issue_show/components/description_spec.js
new file mode 100644
index 00000000000..408349cc42d
--- /dev/null
+++ b/spec/javascripts/issue_show/components/description_spec.js
@@ -0,0 +1,99 @@
+import Vue from 'vue';
+import descriptionComponent from '~/issue_show/components/description.vue';
+
+describe('Description component', () => {
+ let vm;
+
+ beforeEach(() => {
+ const Component = Vue.extend(descriptionComponent);
+
+ if (!document.querySelector('.issuable-meta')) {
+ const metaData = document.createElement('div');
+ metaData.classList.add('issuable-meta');
+ metaData.innerHTML = '<span id="task_status"></span><span id="task_status_short"></span>';
+
+ document.body.appendChild(metaData);
+ }
+
+ vm = new Component({
+ propsData: {
+ canUpdate: true,
+ descriptionHtml: 'test',
+ descriptionText: 'test',
+ updatedAt: new Date().toString(),
+ taskStatus: '',
+ },
+ }).$mount();
+ });
+
+ it('animates description changes', (done) => {
+ vm.descriptionHtml = 'changed';
+
+ Vue.nextTick(() => {
+ expect(
+ vm.$el.querySelector('.wiki').classList.contains('issue-realtime-pre-pulse'),
+ ).toBeTruthy();
+
+ setTimeout(() => {
+ expect(
+ vm.$el.querySelector('.wiki').classList.contains('issue-realtime-trigger-pulse'),
+ ).toBeTruthy();
+
+ done();
+ });
+ });
+ });
+
+ it('re-inits the TaskList when description changed', (done) => {
+ spyOn(gl, 'TaskList');
+ vm.descriptionHtml = 'changed';
+
+ setTimeout(() => {
+ expect(
+ gl.TaskList,
+ ).toHaveBeenCalled();
+
+ done();
+ });
+ });
+
+ it('does not re-init the TaskList when canUpdate is false', (done) => {
+ spyOn(gl, 'TaskList');
+ vm.canUpdate = false;
+ vm.descriptionHtml = 'changed';
+
+ setTimeout(() => {
+ expect(
+ gl.TaskList,
+ ).not.toHaveBeenCalled();
+
+ done();
+ });
+ });
+
+ describe('taskStatus', () => {
+ it('adds full taskStatus', (done) => {
+ vm.taskStatus = '1 of 1';
+
+ setTimeout(() => {
+ expect(
+ document.querySelector('.issuable-meta #task_status').textContent.trim(),
+ ).toBe('1 of 1');
+
+ done();
+ });
+ });
+
+ it('adds short taskStatus', (done) => {
+ vm.taskStatus = '1 of 1';
+
+ setTimeout(() => {
+ expect(
+ document.querySelector('.issuable-meta #task_status_short').textContent.trim(),
+ ).toBe('1/1 task');
+
+ done();
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/issue_show/components/title_spec.js b/spec/javascripts/issue_show/components/title_spec.js
new file mode 100644
index 00000000000..2f953e7e92e
--- /dev/null
+++ b/spec/javascripts/issue_show/components/title_spec.js
@@ -0,0 +1,67 @@
+import Vue from 'vue';
+import titleComponent from '~/issue_show/components/title.vue';
+
+describe('Title component', () => {
+ let vm;
+
+ beforeEach(() => {
+ const Component = Vue.extend(titleComponent);
+ vm = new Component({
+ propsData: {
+ issuableRef: '#1',
+ titleHtml: 'Testing <img />',
+ titleText: 'Testing',
+ },
+ }).$mount();
+ });
+
+ it('renders title HTML', () => {
+ expect(
+ vm.$el.innerHTML.trim(),
+ ).toBe('Testing <img>');
+ });
+
+ it('updates page title when changing titleHtml', (done) => {
+ spyOn(vm, 'setPageTitle');
+ vm.titleHtml = 'test';
+
+ Vue.nextTick(() => {
+ expect(
+ vm.setPageTitle,
+ ).toHaveBeenCalled();
+
+ done();
+ });
+ });
+
+ it('animates title changes', (done) => {
+ vm.titleHtml = 'test';
+
+ Vue.nextTick(() => {
+ expect(
+ vm.$el.classList.contains('issue-realtime-pre-pulse'),
+ ).toBeTruthy();
+
+ setTimeout(() => {
+ expect(
+ vm.$el.classList.contains('issue-realtime-trigger-pulse'),
+ ).toBeTruthy();
+
+ done();
+ });
+ });
+ });
+
+ it('updates page title after changing title', (done) => {
+ vm.titleHtml = 'changed';
+ vm.titleText = 'changed';
+
+ Vue.nextTick(() => {
+ expect(
+ document.querySelector('title').textContent.trim(),
+ ).toContain('changed');
+
+ done();
+ });
+ });
+});
diff --git a/spec/javascripts/issue_show/issue_title_spec.js b/spec/javascripts/issue_show/issue_title_spec.js
deleted file mode 100644
index 03edbf9f947..00000000000
--- a/spec/javascripts/issue_show/issue_title_spec.js
+++ /dev/null
@@ -1,22 +0,0 @@
-import Vue from 'vue';
-import issueTitle from '~/issue_show/issue_title.vue';
-
-describe('Issue Title', () => {
- let IssueTitleComponent;
-
- beforeEach(() => {
- IssueTitleComponent = Vue.extend(issueTitle);
- });
-
- it('should render a title', () => {
- const component = new IssueTitleComponent({
- propsData: {
- initialTitle: 'wow',
- endpoint: '/gitlab-org/gitlab-shell/issues/9/rendered_title',
- },
- }).$mount();
-
- expect(component.$el.classList).toContain('title');
- expect(component.$el.innerHTML).toContain('wow');
- });
-});
diff --git a/spec/javascripts/issue_show/mock_data.js b/spec/javascripts/issue_show/mock_data.js
new file mode 100644
index 00000000000..6683d581bc5
--- /dev/null
+++ b/spec/javascripts/issue_show/mock_data.js
@@ -0,0 +1,26 @@
+export default {
+ initialRequest: {
+ title: '<p>this is a title</p>',
+ title_text: 'this is a title',
+ description: '<p>this is a description!</p>',
+ description_text: 'this is a description',
+ task_status: '2 of 4 completed',
+ updated_at: new Date().toString(),
+ },
+ secondRequest: {
+ title: '<p>2</p>',
+ title_text: '2',
+ description: '<p>42</p>',
+ description_text: '42',
+ task_status: '0 of 0 completed',
+ updated_at: new Date().toString(),
+ },
+ issueSpecRequest: {
+ title: '<p>this is a title</p>',
+ title_text: 'this is a title',
+ description: '<li class="task-list-item enabled"><input type="checkbox" class="task-list-item-checkbox">Task List Item</li>',
+ description_text: '- [ ] Task List Item',
+ task_status: '0 of 1 completed',
+ updated_at: new Date().toString(),
+ },
+};
diff --git a/spec/javascripts/issue_spec.js b/spec/javascripts/issue_spec.js
index 9a2570ef7e9..763f5ee9e50 100644
--- a/spec/javascripts/issue_spec.js
+++ b/spec/javascripts/issue_spec.js
@@ -81,12 +81,6 @@ describe('Issue', function() {
this.issue = new Issue();
});
- it('modifies the Markdown field', function() {
- 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', function() {
spyOn(jQuery, 'ajax').and.callFake(function(req) {
expect(req.type).toBe('PATCH');
@@ -108,8 +102,8 @@ describe('Issue', function() {
expect(this.$triggeredButton).toHaveProp('disabled', true);
expectNewBranchButtonState(true, false);
return this.issueStateDeferred;
- } else if (req.url === Issue.$btnNewBranch.data('path')) {
- expect(req.type).toBe('get');
+ } else if (req.url === Issue.createMrDropdownWrap.dataset.canCreatePath) {
+ expect(req.type).toBe('GET');
expectNewBranchButtonState(true, false);
return this.canCreateBranchDeferred;
}
diff --git a/spec/javascripts/labels_issue_sidebar_spec.js b/spec/javascripts/labels_issue_sidebar_spec.js
index 37e038c16da..53aba191b19 100644
--- a/spec/javascripts/labels_issue_sidebar_spec.js
+++ b/spec/javascripts/labels_issue_sidebar_spec.js
@@ -2,7 +2,6 @@
/* global IssuableContext */
/* global LabelsSelect */
-require('~/lib/utils/type_utility');
require('~/gl_dropdown');
require('select2');
require('vendor/jquery.nicescroll');
diff --git a/spec/javascripts/landing_spec.js b/spec/javascripts/landing_spec.js
new file mode 100644
index 00000000000..7916073190a
--- /dev/null
+++ b/spec/javascripts/landing_spec.js
@@ -0,0 +1,160 @@
+import Landing from '~/landing';
+import Cookies from 'js-cookie';
+
+describe('Landing', function () {
+ describe('class constructor', function () {
+ beforeEach(function () {
+ this.landingElement = {};
+ this.dismissButton = {};
+ this.cookieName = 'cookie_name';
+
+ this.landing = new Landing(this.landingElement, this.dismissButton, this.cookieName);
+ });
+
+ it('should set .landing', function () {
+ expect(this.landing.landingElement).toBe(this.landingElement);
+ });
+
+ it('should set .cookieName', function () {
+ expect(this.landing.cookieName).toBe(this.cookieName);
+ });
+
+ it('should set .dismissButton', function () {
+ expect(this.landing.dismissButton).toBe(this.dismissButton);
+ });
+
+ it('should set .eventWrapper', function () {
+ expect(this.landing.eventWrapper).toEqual({});
+ });
+ });
+
+ describe('toggle', function () {
+ beforeEach(function () {
+ this.isDismissed = false;
+ this.landingElement = { classList: jasmine.createSpyObj('classList', ['toggle']) };
+ this.landing = {
+ isDismissed: () => {},
+ addEvents: () => {},
+ landingElement: this.landingElement,
+ };
+
+ spyOn(this.landing, 'isDismissed').and.returnValue(this.isDismissed);
+ spyOn(this.landing, 'addEvents');
+
+ Landing.prototype.toggle.call(this.landing);
+ });
+
+ it('should call .isDismissed', function () {
+ expect(this.landing.isDismissed).toHaveBeenCalled();
+ });
+
+ it('should call .classList.toggle', function () {
+ expect(this.landingElement.classList.toggle).toHaveBeenCalledWith('hidden', this.isDismissed);
+ });
+
+ it('should call .addEvents', function () {
+ expect(this.landing.addEvents).toHaveBeenCalled();
+ });
+
+ describe('if isDismissed is true', function () {
+ beforeEach(function () {
+ this.isDismissed = true;
+ this.landingElement = { classList: jasmine.createSpyObj('classList', ['toggle']) };
+ this.landing = {
+ isDismissed: () => {},
+ addEvents: () => {},
+ landingElement: this.landingElement,
+ };
+
+ spyOn(this.landing, 'isDismissed').and.returnValue(this.isDismissed);
+ spyOn(this.landing, 'addEvents');
+
+ this.landing.isDismissed.calls.reset();
+
+ Landing.prototype.toggle.call(this.landing);
+ });
+
+ it('should not call .addEvents', function () {
+ expect(this.landing.addEvents).not.toHaveBeenCalled();
+ });
+ });
+ });
+
+ describe('addEvents', function () {
+ beforeEach(function () {
+ this.dismissButton = jasmine.createSpyObj('dismissButton', ['addEventListener']);
+ this.eventWrapper = {};
+ this.landing = {
+ eventWrapper: this.eventWrapper,
+ dismissButton: this.dismissButton,
+ dismissLanding: () => {},
+ };
+
+ Landing.prototype.addEvents.call(this.landing);
+ });
+
+ it('should set .eventWrapper.dismissLanding', function () {
+ expect(this.eventWrapper.dismissLanding).toEqual(jasmine.any(Function));
+ });
+
+ it('should call .addEventListener', function () {
+ expect(this.dismissButton.addEventListener).toHaveBeenCalledWith('click', this.eventWrapper.dismissLanding);
+ });
+ });
+
+ describe('removeEvents', function () {
+ beforeEach(function () {
+ this.dismissButton = jasmine.createSpyObj('dismissButton', ['removeEventListener']);
+ this.eventWrapper = { dismissLanding: () => {} };
+ this.landing = {
+ eventWrapper: this.eventWrapper,
+ dismissButton: this.dismissButton,
+ };
+
+ Landing.prototype.removeEvents.call(this.landing);
+ });
+
+ it('should call .removeEventListener', function () {
+ expect(this.dismissButton.removeEventListener).toHaveBeenCalledWith('click', this.eventWrapper.dismissLanding);
+ });
+ });
+
+ describe('dismissLanding', function () {
+ beforeEach(function () {
+ this.landingElement = { classList: jasmine.createSpyObj('classList', ['add']) };
+ this.cookieName = 'cookie_name';
+ this.landing = { landingElement: this.landingElement, cookieName: this.cookieName };
+
+ spyOn(Cookies, 'set');
+
+ Landing.prototype.dismissLanding.call(this.landing);
+ });
+
+ it('should call .classList.add', function () {
+ expect(this.landingElement.classList.add).toHaveBeenCalledWith('hidden');
+ });
+
+ it('should call Cookies.set', function () {
+ expect(Cookies.set).toHaveBeenCalledWith(this.cookieName, 'true', { expires: 365 });
+ });
+ });
+
+ describe('isDismissed', function () {
+ beforeEach(function () {
+ this.cookieName = 'cookie_name';
+ this.landing = { cookieName: this.cookieName };
+
+ spyOn(Cookies, 'get').and.returnValue('true');
+
+ this.isDismissed = Landing.prototype.isDismissed.call(this.landing);
+ });
+
+ it('should call Cookies.get', function () {
+ expect(Cookies.get).toHaveBeenCalledWith(this.cookieName);
+ });
+
+ it('should return a boolean', function () {
+ expect(typeof this.isDismissed).toEqual('boolean');
+ });
+ });
+});
diff --git a/spec/javascripts/lib/utils/accessor_spec.js b/spec/javascripts/lib/utils/accessor_spec.js
new file mode 100644
index 00000000000..b768d6f2a68
--- /dev/null
+++ b/spec/javascripts/lib/utils/accessor_spec.js
@@ -0,0 +1,78 @@
+import AccessorUtilities from '~/lib/utils/accessor';
+
+describe('AccessorUtilities', () => {
+ const testError = new Error('test error');
+
+ describe('isPropertyAccessSafe', () => {
+ let base;
+
+ it('should return `true` if access is safe', () => {
+ base = { testProp: 'testProp' };
+
+ expect(AccessorUtilities.isPropertyAccessSafe(base, 'testProp')).toBe(true);
+ });
+
+ it('should return `false` if access throws an error', () => {
+ base = { get testProp() { throw testError; } };
+
+ expect(AccessorUtilities.isPropertyAccessSafe(base, 'testProp')).toBe(false);
+ });
+
+ it('should return `false` if property is undefined', () => {
+ base = {};
+
+ expect(AccessorUtilities.isPropertyAccessSafe(base, 'testProp')).toBe(false);
+ });
+ });
+
+ describe('isFunctionCallSafe', () => {
+ const base = {};
+
+ it('should return `true` if calling is safe', () => {
+ base.func = () => {};
+
+ expect(AccessorUtilities.isFunctionCallSafe(base, 'func')).toBe(true);
+ });
+
+ it('should return `false` if calling throws an error', () => {
+ base.func = () => { throw new Error('test error'); };
+
+ expect(AccessorUtilities.isFunctionCallSafe(base, 'func')).toBe(false);
+ });
+
+ it('should return `false` if function is undefined', () => {
+ base.func = undefined;
+
+ expect(AccessorUtilities.isFunctionCallSafe(base, 'func')).toBe(false);
+ });
+ });
+
+ describe('isLocalStorageAccessSafe', () => {
+ beforeEach(() => {
+ spyOn(window.localStorage, 'setItem');
+ spyOn(window.localStorage, 'removeItem');
+ });
+
+ it('should return `true` if access is safe', () => {
+ expect(AccessorUtilities.isLocalStorageAccessSafe()).toBe(true);
+ });
+
+ it('should return `false` if access to .setItem isnt safe', () => {
+ window.localStorage.setItem.and.callFake(() => { throw testError; });
+
+ expect(AccessorUtilities.isLocalStorageAccessSafe()).toBe(false);
+ });
+
+ it('should set a test item if access is safe', () => {
+ AccessorUtilities.isLocalStorageAccessSafe();
+
+ expect(window.localStorage.setItem).toHaveBeenCalledWith('isLocalStorageAccessSafe', 'true');
+ });
+
+ it('should remove the test item if access is safe', () => {
+ AccessorUtilities.isLocalStorageAccessSafe();
+
+ expect(window.localStorage.removeItem).toHaveBeenCalledWith('isLocalStorageAccessSafe');
+ });
+ });
+});
diff --git a/spec/javascripts/lib/utils/ajax_cache_spec.js b/spec/javascripts/lib/utils/ajax_cache_spec.js
new file mode 100644
index 00000000000..e1747a82329
--- /dev/null
+++ b/spec/javascripts/lib/utils/ajax_cache_spec.js
@@ -0,0 +1,158 @@
+import AjaxCache from '~/lib/utils/ajax_cache';
+
+describe('AjaxCache', () => {
+ const dummyEndpoint = '/AjaxCache/dummyEndpoint';
+ const dummyResponse = {
+ important: 'dummy data',
+ };
+
+ beforeEach(() => {
+ AjaxCache.internalStorage = { };
+ AjaxCache.pendingRequests = { };
+ });
+
+ describe('get', () => {
+ it('returns undefined if cache is empty', () => {
+ const data = AjaxCache.get(dummyEndpoint);
+
+ expect(data).toBe(undefined);
+ });
+
+ it('returns undefined if cache contains no matching data', () => {
+ AjaxCache.internalStorage['not matching'] = dummyResponse;
+
+ const data = AjaxCache.get(dummyEndpoint);
+
+ expect(data).toBe(undefined);
+ });
+
+ it('returns matching data', () => {
+ AjaxCache.internalStorage[dummyEndpoint] = dummyResponse;
+
+ const data = AjaxCache.get(dummyEndpoint);
+
+ expect(data).toBe(dummyResponse);
+ });
+ });
+
+ describe('hasData', () => {
+ it('returns false if cache is empty', () => {
+ expect(AjaxCache.hasData(dummyEndpoint)).toBe(false);
+ });
+
+ it('returns false if cache contains no matching data', () => {
+ AjaxCache.internalStorage['not matching'] = dummyResponse;
+
+ expect(AjaxCache.hasData(dummyEndpoint)).toBe(false);
+ });
+
+ it('returns true if data is available', () => {
+ AjaxCache.internalStorage[dummyEndpoint] = dummyResponse;
+
+ expect(AjaxCache.hasData(dummyEndpoint)).toBe(true);
+ });
+ });
+
+ describe('remove', () => {
+ it('does nothing if cache is empty', () => {
+ AjaxCache.remove(dummyEndpoint);
+
+ expect(AjaxCache.internalStorage).toEqual({ });
+ });
+
+ it('does nothing if cache contains no matching data', () => {
+ AjaxCache.internalStorage['not matching'] = dummyResponse;
+
+ AjaxCache.remove(dummyEndpoint);
+
+ expect(AjaxCache.internalStorage['not matching']).toBe(dummyResponse);
+ });
+
+ it('removes matching data', () => {
+ AjaxCache.internalStorage[dummyEndpoint] = dummyResponse;
+
+ AjaxCache.remove(dummyEndpoint);
+
+ expect(AjaxCache.internalStorage).toEqual({ });
+ });
+ });
+
+ describe('retrieve', () => {
+ let ajaxSpy;
+
+ beforeEach(() => {
+ spyOn(jQuery, 'ajax').and.callFake(url => ajaxSpy(url));
+ });
+
+ it('stores and returns data from Ajax call if cache is empty', (done) => {
+ ajaxSpy = (url) => {
+ expect(url).toBe(dummyEndpoint);
+ const deferred = $.Deferred();
+ deferred.resolve(dummyResponse);
+ return deferred.promise();
+ };
+
+ AjaxCache.retrieve(dummyEndpoint)
+ .then((data) => {
+ expect(data).toBe(dummyResponse);
+ expect(AjaxCache.internalStorage[dummyEndpoint]).toBe(dummyResponse);
+ })
+ .then(done)
+ .catch(fail);
+ });
+
+ it('makes no Ajax call if request is pending', () => {
+ const responseDeferred = $.Deferred();
+
+ ajaxSpy = (url) => {
+ expect(url).toBe(dummyEndpoint);
+ // neither reject nor resolve to keep request pending
+ return responseDeferred.promise();
+ };
+
+ const unexpectedResponse = data => fail(`Did not expect response: ${data}`);
+
+ AjaxCache.retrieve(dummyEndpoint)
+ .then(unexpectedResponse)
+ .catch(fail);
+
+ AjaxCache.retrieve(dummyEndpoint)
+ .then(unexpectedResponse)
+ .catch(fail);
+
+ expect($.ajax.calls.count()).toBe(1);
+ });
+
+ it('returns undefined if Ajax call fails and cache is empty', (done) => {
+ const dummyStatusText = 'exploded';
+ const dummyErrorMessage = 'server exploded';
+ ajaxSpy = (url) => {
+ expect(url).toBe(dummyEndpoint);
+ const deferred = $.Deferred();
+ deferred.reject(null, dummyStatusText, dummyErrorMessage);
+ return deferred.promise();
+ };
+
+ AjaxCache.retrieve(dummyEndpoint)
+ .then(data => fail(`Received unexpected data: ${JSON.stringify(data)}`))
+ .catch((error) => {
+ expect(error.message).toBe(`${dummyEndpoint}: ${dummyErrorMessage}`);
+ expect(error.textStatus).toBe(dummyStatusText);
+ done();
+ })
+ .catch(fail);
+ });
+
+ it('makes no Ajax call if matching data exists', (done) => {
+ AjaxCache.internalStorage[dummyEndpoint] = dummyResponse;
+ ajaxSpy = () => fail(new Error('expected no Ajax call!'));
+
+ AjaxCache.retrieve(dummyEndpoint)
+ .then((data) => {
+ expect(data).toBe(dummyResponse);
+ })
+ .then(done)
+ .catch(fail);
+ });
+ });
+});
diff --git a/spec/javascripts/lib/utils/common_utils_spec.js b/spec/javascripts/lib/utils/common_utils_spec.js
index a00efa10119..5eb147ed888 100644
--- a/spec/javascripts/lib/utils/common_utils_spec.js
+++ b/spec/javascripts/lib/utils/common_utils_spec.js
@@ -362,5 +362,16 @@ require('~/lib/utils/common_utils');
gl.utils.setCiStatusFavicon(BUILD_URL);
});
});
+
+ describe('gl.utils.ajaxPost', () => {
+ it('should perform `$.ajax` call and do `POST` request', () => {
+ const requestURL = '/some/random/api';
+ const data = { keyname: 'value' };
+ const ajaxSpy = spyOn($, 'ajax').and.callFake(() => {});
+
+ gl.utils.ajaxPost(requestURL, data);
+ expect(ajaxSpy.calls.allArgs()[0][0].type).toEqual('POST');
+ });
+ });
});
})();
diff --git a/spec/javascripts/lib/utils/poll_spec.js b/spec/javascripts/lib/utils/poll_spec.js
index 918b6d32c43..22f30191ab9 100644
--- a/spec/javascripts/lib/utils/poll_spec.js
+++ b/spec/javascripts/lib/utils/poll_spec.js
@@ -1,9 +1,5 @@
-import Vue from 'vue';
-import VueResource from 'vue-resource';
import Poll from '~/lib/utils/poll';
-Vue.use(VueResource);
-
const waitForAllCallsToFinish = (service, waitForCount, successCallback) => {
const timer = () => {
setTimeout(() => {
@@ -12,45 +8,33 @@ const waitForAllCallsToFinish = (service, waitForCount, successCallback) => {
} else {
timer();
}
- }, 5);
+ }, 0);
};
timer();
};
-class ServiceMock {
- constructor(endpoint) {
- this.service = Vue.resource(endpoint);
- }
+function mockServiceCall(service, response, shouldFail = false) {
+ const action = shouldFail ? Promise.reject : Promise.resolve;
+ const responseObject = response;
+
+ if (!responseObject.headers) responseObject.headers = {};
- fetch() {
- return this.service.get();
- }
+ service.fetch.and.callFake(action.bind(Promise, responseObject));
}
describe('Poll', () => {
- let callbacks;
- let service;
+ const service = jasmine.createSpyObj('service', ['fetch']);
+ const callbacks = jasmine.createSpyObj('callbacks', ['success', 'error']);
- beforeEach(() => {
- callbacks = {
- success: () => {},
- error: () => {},
- };
-
- service = new ServiceMock('endpoint');
-
- spyOn(callbacks, 'success');
- spyOn(callbacks, 'error');
- spyOn(service, 'fetch').and.callThrough();
+ afterEach(() => {
+ callbacks.success.calls.reset();
+ callbacks.error.calls.reset();
+ service.fetch.calls.reset();
});
it('calls the success callback when no header for interval is provided', (done) => {
- const successInterceptor = (request, next) => {
- next(request.respondWith(JSON.stringify([]), { status: 200 }));
- };
-
- Vue.http.interceptors.push(successInterceptor);
+ mockServiceCall(service, { status: 200 });
new Poll({
resource: service,
@@ -63,18 +47,12 @@ describe('Poll', () => {
expect(callbacks.success).toHaveBeenCalled();
expect(callbacks.error).not.toHaveBeenCalled();
- Vue.http.interceptors = _.without(Vue.http.interceptors, successInterceptor);
-
done();
- }, 0);
+ });
});
it('calls the error callback whe the http request returns an error', (done) => {
- const errorInterceptor = (request, next) => {
- next(request.respondWith(JSON.stringify([]), { status: 500 }));
- };
-
- Vue.http.interceptors.push(errorInterceptor);
+ mockServiceCall(service, { status: 500 }, true);
new Poll({
resource: service,
@@ -86,42 +64,29 @@ describe('Poll', () => {
waitForAllCallsToFinish(service, 1, () => {
expect(callbacks.success).not.toHaveBeenCalled();
expect(callbacks.error).toHaveBeenCalled();
- Vue.http.interceptors = _.without(Vue.http.interceptors, errorInterceptor);
done();
});
});
it('should call the success callback when the interval header is -1', (done) => {
- const intervalInterceptor = (request, next) => {
- next(request.respondWith(JSON.stringify([]), { status: 200, headers: { 'poll-interval': -1 } }));
- };
-
- Vue.http.interceptors.push(intervalInterceptor);
+ mockServiceCall(service, { status: 200, headers: { 'poll-interval': -1 } });
new Poll({
resource: service,
method: 'fetch',
successCallback: callbacks.success,
errorCallback: callbacks.error,
- }).makeRequest();
-
- setTimeout(() => {
+ }).makeRequest().then(() => {
expect(callbacks.success).toHaveBeenCalled();
expect(callbacks.error).not.toHaveBeenCalled();
- Vue.http.interceptors = _.without(Vue.http.interceptors, intervalInterceptor);
-
done();
- }, 0);
+ }).catch(done.fail);
});
it('starts polling when http status is 200 and interval header is provided', (done) => {
- const pollInterceptor = (request, next) => {
- next(request.respondWith(JSON.stringify([]), { status: 200, headers: { 'poll-interval': 2 } }));
- };
-
- Vue.http.interceptors.push(pollInterceptor);
+ mockServiceCall(service, { status: 200, headers: { 'poll-interval': 1 } });
const Polling = new Poll({
resource: service,
@@ -141,19 +106,13 @@ describe('Poll', () => {
expect(callbacks.success).toHaveBeenCalled();
expect(callbacks.error).not.toHaveBeenCalled();
- Vue.http.interceptors = _.without(Vue.http.interceptors, pollInterceptor);
-
done();
});
});
describe('stop', () => {
it('stops polling when method is called', (done) => {
- const pollInterceptor = (request, next) => {
- next(request.respondWith(JSON.stringify([]), { status: 200, headers: { 'poll-interval': 2 } }));
- };
-
- Vue.http.interceptors.push(pollInterceptor);
+ mockServiceCall(service, { status: 200, headers: { 'poll-interval': 1 } });
const Polling = new Poll({
resource: service,
@@ -174,8 +133,6 @@ describe('Poll', () => {
expect(service.fetch).toHaveBeenCalledWith({ page: 1 });
expect(Polling.stop).toHaveBeenCalled();
- Vue.http.interceptors = _.without(Vue.http.interceptors, pollInterceptor);
-
done();
});
});
@@ -183,11 +140,7 @@ describe('Poll', () => {
describe('restart', () => {
it('should restart polling when its called', (done) => {
- const pollInterceptor = (request, next) => {
- next(request.respondWith(JSON.stringify([]), { status: 200, headers: { 'poll-interval': 2 } }));
- };
-
- Vue.http.interceptors.push(pollInterceptor);
+ mockServiceCall(service, { status: 200, headers: { 'poll-interval': 1 } });
const Polling = new Poll({
resource: service,
@@ -215,8 +168,6 @@ describe('Poll', () => {
expect(Polling.stop).toHaveBeenCalled();
expect(Polling.restart).toHaveBeenCalled();
- Vue.http.interceptors = _.without(Vue.http.interceptors, pollInterceptor);
-
done();
});
});
diff --git a/spec/javascripts/line_highlighter_spec.js b/spec/javascripts/line_highlighter_spec.js
index a1fd2d38968..b1b08989028 100644
--- a/spec/javascripts/line_highlighter_spec.js
+++ b/spec/javascripts/line_highlighter_spec.js
@@ -58,7 +58,7 @@ require('~/line_highlighter');
return expect(func).not.toThrow();
});
});
- describe('#clickHandler', function() {
+ describe('clickHandler', function() {
it('handles clicking on a child icon element', function() {
var spy;
spy = spyOn(this["class"], 'setHash').and.callThrough();
@@ -176,7 +176,7 @@ require('~/line_highlighter');
});
});
});
- describe('#hashToRange', function() {
+ describe('hashToRange', function() {
beforeEach(function() {
return this.subject = this["class"].hashToRange;
});
@@ -190,7 +190,7 @@ require('~/line_highlighter');
return expect(this.subject('#foo')).toEqual([null, null]);
});
});
- describe('#highlightLine', function() {
+ describe('highlightLine', function() {
beforeEach(function() {
return this.subject = this["class"].highlightLine;
});
@@ -203,7 +203,7 @@ require('~/line_highlighter');
return expect($('#LC13')).toHaveClass(this.css);
});
});
- return describe('#setHash', function() {
+ return describe('setHash', function() {
beforeEach(function() {
return this.subject = this["class"].setHash;
});
diff --git a/spec/javascripts/merge_request_notes_spec.js b/spec/javascripts/merge_request_notes_spec.js
new file mode 100644
index 00000000000..e54acfa8e44
--- /dev/null
+++ b/spec/javascripts/merge_request_notes_spec.js
@@ -0,0 +1,61 @@
+/* global Notes */
+
+import 'vendor/autosize';
+import '~/gl_form';
+import '~/lib/utils/text_utility';
+import '~/render_gfm';
+import '~/render_math';
+import '~/notes';
+
+describe('Merge request notes', () => {
+ window.gon = window.gon || {};
+ window.gl = window.gl || {};
+ gl.utils = gl.utils || {};
+
+ const fixture = 'merge_requests/diff_comment.html.raw';
+ preloadFixtures(fixture);
+
+ beforeEach(() => {
+ loadFixtures(fixture);
+ gl.utils.disableButtonIfEmptyField = _.noop;
+ window.project_uploads_path = 'http://test.host/uploads';
+ $('body').data('page', 'projects:merge_requests:show');
+ window.gon.current_user_id = $('.note:last').data('author-id');
+
+ return new Notes('', []);
+ });
+
+ describe('up arrow', () => {
+ it('edits last comment when triggered in main form', () => {
+ const upArrowEvent = $.Event('keydown');
+ upArrowEvent.which = 38;
+
+ spyOnEvent('.note:last .js-note-edit', 'click');
+
+ $('.js-note-text').trigger(upArrowEvent);
+
+ expect('click').toHaveBeenTriggeredOn('.note:last .js-note-edit');
+ });
+
+ it('edits last comment in discussion when triggered in discussion form', (done) => {
+ const upArrowEvent = $.Event('keydown');
+ upArrowEvent.which = 38;
+
+ spyOnEvent('.note-discussion .js-note-edit', 'click');
+
+ $('.js-discussion-reply-button').click();
+
+ setTimeout(() => {
+ expect(
+ $('.note-discussion .js-note-text'),
+ ).toExist();
+
+ $('.note-discussion .js-note-text').trigger(upArrowEvent);
+
+ expect('click').toHaveBeenTriggeredOn('.note-discussion .js-note-edit');
+
+ done();
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/merge_request_tabs_spec.js b/spec/javascripts/merge_request_tabs_spec.js
index e437333d522..254a41db160 100644
--- a/spec/javascripts/merge_request_tabs_spec.js
+++ b/spec/javascripts/merge_request_tabs_spec.js
@@ -47,7 +47,7 @@ require('vendor/jquery.scrollTo');
this.class.destroyPipelinesView();
});
- describe('#activateTab', function () {
+ describe('activateTab', function () {
beforeEach(function () {
spyOn($, 'ajax').and.callFake(function () {});
loadFixtures('merge_requests/merge_request_with_task_list.html.raw');
@@ -71,7 +71,7 @@ require('vendor/jquery.scrollTo');
});
});
- describe('#opensInNewTab', function () {
+ describe('opensInNewTab', function () {
var tabUrl;
var windowTarget = '_blank';
@@ -152,7 +152,7 @@ require('vendor/jquery.scrollTo');
});
});
- describe('#setCurrentAction', function () {
+ describe('setCurrentAction', function () {
beforeEach(function () {
spyOn($, 'ajax').and.callFake(function () {});
this.subject = this.class.setCurrentAction;
@@ -221,7 +221,7 @@ require('vendor/jquery.scrollTo');
});
});
- describe('#tabShown', () => {
+ describe('tabShown', () => {
beforeEach(function () {
spyOn($, 'ajax').and.callFake(function (options) {
options.success({ html: '' });
@@ -281,7 +281,7 @@ require('vendor/jquery.scrollTo');
});
});
- describe('#loadDiff', function () {
+ describe('loadDiff', function () {
it('requires an absolute pathname', function () {
spyOn($, 'ajax').and.callFake(function (options) {
expect(options.url).toEqual('/foo/bar/merge_requests/1/diffs.json');
diff --git a/spec/javascripts/merge_request_widget_spec.js b/spec/javascripts/merge_request_widget_spec.js
deleted file mode 100644
index 88dae8c3e06..00000000000
--- a/spec/javascripts/merge_request_widget_spec.js
+++ /dev/null
@@ -1,199 +0,0 @@
-/* eslint-disable space-before-function-paren, quotes, comma-dangle, dot-notation, quote-props, no-var, max-len */
-
-require('~/merge_request_widget');
-require('~/smart_interval');
-require('~/lib/utils/datetime_utility');
-
-(function() {
- describe('MergeRequestWidget', function() {
- beforeEach(function() {
- window.notifyPermissions = function() {};
- window.notify = function() {};
- this.opts = {
- ci_status_url: "http://sampledomain.local/ci/getstatus",
- ci_environments_status_url: "http://sampledomain.local/ci/getenvironmentsstatus",
- 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",
- ci_pipeline: 80,
- ci_sha: "12a34bc5",
- builds_path: "http://sampledomain.local/sampleBuildsPath",
- commits_path: "http://sampledomain.local/commits",
- pipeline_path: "http://sampledomain.local/pipelines"
- };
- this["class"] = new window.gl.MergeRequestWidget(this.opts);
- });
-
- describe('getCIEnvironmentsStatus', function() {
- beforeEach(function() {
- this.ciEnvironmentsStatusData = [{
- created_at: '2016-09-12T13:38:30.636Z',
- environment_id: 1,
- environment_name: 'env1',
- external_url: 'https://test-url.com',
- external_url_formatted: 'test-url.com'
- }];
-
- spyOn(jQuery, 'getJSON').and.callFake(function(req, cb) {
- cb(this.ciEnvironmentsStatusData);
- }.bind(this));
- });
-
- it('should call renderEnvironments when the environments property is set', function() {
- const spy = spyOn(this.class, 'renderEnvironments').and.stub();
- this.class.getCIEnvironmentsStatus();
- expect(spy).toHaveBeenCalledWith(this.ciEnvironmentsStatusData);
- });
-
- it('should not call renderEnvironments when the environments property is not set', function() {
- this.ciEnvironmentsStatusData = null;
- const spy = spyOn(this.class, 'renderEnvironments').and.stub();
- this.class.getCIEnvironmentsStatus();
- expect(spy).not.toHaveBeenCalled();
- });
- });
-
- describe('renderEnvironments', function() {
- describe('should render correct timeago', function() {
- beforeEach(function() {
- this.environments = [{
- id: 'test-environment-id',
- url: 'testurl',
- deployed_at: new Date().toISOString(),
- deployed_at_formatted: true
- }];
- });
-
- function getTimeagoText(template) {
- var el = document.createElement('html');
- el.innerHTML = template;
- return el.querySelector('.js-environment-timeago').innerText.trim();
- }
-
- it('should render less than a minute ago text', function() {
- spyOn(this.class.$widgetBody, 'before').and.callFake(function(template) {
- expect(getTimeagoText(template)).toBe('less than a minute ago.');
- });
-
- this.class.renderEnvironments(this.environments);
- });
-
- it('should render about an hour ago text', function() {
- var oneHourAgo = new Date();
- oneHourAgo.setHours(oneHourAgo.getHours() - 1);
-
- this.environments[0].deployed_at = oneHourAgo.toISOString();
- spyOn(this.class.$widgetBody, 'before').and.callFake(function(template) {
- expect(getTimeagoText(template)).toBe('about an hour ago.');
- });
-
- this.class.renderEnvironments(this.environments);
- });
-
- it('should render about 2 hours ago text', function() {
- var twoHoursAgo = new Date();
- twoHoursAgo.setHours(twoHoursAgo.getHours() - 2);
-
- this.environments[0].deployed_at = twoHoursAgo.toISOString();
- spyOn(this.class.$widgetBody, 'before').and.callFake(function(template) {
- expect(getTimeagoText(template)).toBe('about 2 hours ago.');
- });
-
- this.class.renderEnvironments(this.environments);
- });
- });
- });
-
- describe('mergeInProgress', function() {
- it('should display error with h4 tag', function() {
- spyOn(this.class.$widgetBody, 'html').and.callFake(function(html) {
- expect(html).toBe('<h4>Sorry, something went wrong.</h4>');
- });
- spyOn($, 'ajax').and.callFake(function(e) {
- e.success({ merge_error: 'Sorry, something went wrong.' });
- });
- this.class.mergeInProgress(null);
- });
- });
-
- describe('getCIStatus', function() {
- beforeEach(function() {
- this.ciStatusData = {
- "title": "Sample MR title",
- "pipeline": 80,
- "sha": "12a34bc5",
- "status": "success",
- "coverage": 98
- };
-
- 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();
- spyOn(gl.utils, 'setCiStatusFavicon').and.callFake(() => {});
- 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();
- spyOn(gl.utils, 'setCiStatusFavicon').and.callFake(() => {});
- 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();
- spyOn(gl.utils, 'setCiStatusFavicon').and.callFake(() => {});
- 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();
- spyOn(gl.utils, 'setCiStatusFavicon').and.callFake(() => {});
- this["class"].getCIStatus(false);
- return expect(spy).not.toHaveBeenCalled();
- });
- it('should not display a notification on the first check after the widget has been created', function() {
- var spy;
- spy = spyOn(window, 'notify');
- spyOn(gl.utils, 'setCiStatusFavicon').and.callFake(() => {});
- this["class"] = new window.gl.MergeRequestWidget(this.opts);
- this["class"].getCIStatus(true);
- return expect(spy).not.toHaveBeenCalled();
- });
- it('should update the pipeline URL when the pipeline changes', function() {
- var spy;
- spy = spyOn(this["class"], 'updatePipelineUrls').and.stub();
- spyOn(gl.utils, 'setCiStatusFavicon').and.callFake(() => {});
- this["class"].getCIStatus(false);
- this.ciStatusData.pipeline += 1;
- this["class"].getCIStatus(false);
- return expect(spy).toHaveBeenCalled();
- });
- it('should update the commit URL when the sha changes', function() {
- var spy;
- spy = spyOn(this["class"], 'updateCommitUrls').and.stub();
- spyOn(gl.utils, 'setCiStatusFavicon').and.callFake(() => {});
- this["class"].getCIStatus(false);
- this.ciStatusData.sha = "9b50b99a";
- this["class"].getCIStatus(false);
- return expect(spy).toHaveBeenCalled();
- });
- });
- });
-}).call(window);
diff --git a/spec/javascripts/merged_buttons_spec.js b/spec/javascripts/merged_buttons_spec.js
deleted file mode 100644
index b5c5e60dd97..00000000000
--- a/spec/javascripts/merged_buttons_spec.js
+++ /dev/null
@@ -1,44 +0,0 @@
-/* global MergedButtons */
-
-import '~/merged_buttons';
-
-describe('MergedButtons', () => {
- const fixturesPath = 'merge_requests/merged_merge_request.html.raw';
- preloadFixtures(fixturesPath);
-
- beforeEach(() => {
- loadFixtures(fixturesPath);
- this.mergedButtons = new MergedButtons();
- this.$removeBranchWidget = $('.remove_source_branch_widget:not(.failed)');
- this.$removeBranchProgress = $('.remove_source_branch_in_progress');
- this.$removeBranchFailed = $('.remove_source_branch_widget.failed');
- this.$removeBranchButton = $('.remove_source_branch');
- });
-
- describe('removeSourceBranch', () => {
- it('shows loader', () => {
- $('.remove_source_branch').trigger('click');
- expect(this.$removeBranchProgress).toBeVisible();
- expect(this.$removeBranchWidget).not.toBeVisible();
- });
- });
-
- describe('removeBranchSuccess', () => {
- it('refreshes page when branch removed', () => {
- spyOn(gl.utils, 'refreshCurrentPage').and.stub();
- const response = { status: 200 };
- this.$removeBranchButton.trigger('ajax:success', response, 'xhr');
- expect(gl.utils.refreshCurrentPage).toHaveBeenCalled();
- });
- });
-
- describe('removeBranchError', () => {
- it('shows error message', () => {
- const response = { status: 500 };
- this.$removeBranchButton.trigger('ajax:error', response, 'xhr');
- expect(this.$removeBranchFailed).toBeVisible();
- expect(this.$removeBranchProgress).not.toBeVisible();
- expect(this.$removeBranchWidget).not.toBeVisible();
- });
- });
-});
diff --git a/spec/javascripts/mini_pipeline_graph_dropdown_spec.js b/spec/javascripts/mini_pipeline_graph_dropdown_spec.js
index e504d41d4d4..481b46c3ac6 100644
--- a/spec/javascripts/mini_pipeline_graph_dropdown_spec.js
+++ b/spec/javascripts/mini_pipeline_graph_dropdown_spec.js
@@ -3,70 +3,84 @@
import MiniPipelineGraph from '~/mini_pipeline_graph_dropdown';
import '~/flash';
-(() => {
- describe('Mini Pipeline Graph Dropdown', () => {
- preloadFixtures('static/mini_dropdown_graph.html.raw');
+describe('Mini Pipeline Graph Dropdown', () => {
+ preloadFixtures('static/mini_dropdown_graph.html.raw');
- beforeEach(() => {
- loadFixtures('static/mini_dropdown_graph.html.raw');
- });
+ beforeEach(() => {
+ loadFixtures('static/mini_dropdown_graph.html.raw');
+ });
- describe('When is initialized', () => {
- it('should initialize without errors when no options are given', () => {
- const miniPipelineGraph = new MiniPipelineGraph();
+ describe('When is initialized', () => {
+ it('should initialize without errors when no options are given', () => {
+ const miniPipelineGraph = new MiniPipelineGraph();
- expect(miniPipelineGraph.dropdownListSelector).toEqual('.js-builds-dropdown-container');
- });
+ expect(miniPipelineGraph.dropdownListSelector).toEqual('.js-builds-dropdown-container');
+ });
- it('should set the container as the given prop', () => {
- const container = '.foo';
+ it('should set the container as the given prop', () => {
+ const container = '.foo';
- const miniPipelineGraph = new MiniPipelineGraph({ container });
+ const miniPipelineGraph = new MiniPipelineGraph({ container });
- expect(miniPipelineGraph.container).toEqual(container);
- });
+ expect(miniPipelineGraph.container).toEqual(container);
});
+ });
- describe('When dropdown is clicked', () => {
- it('should call getBuildsList', () => {
- const getBuildsListSpy = spyOn(MiniPipelineGraph.prototype, 'getBuildsList').and.callFake(function () {});
+ describe('When dropdown is clicked', () => {
+ it('should call getBuildsList', () => {
+ const getBuildsListSpy = spyOn(
+ MiniPipelineGraph.prototype,
+ 'getBuildsList',
+ ).and.callFake(function () {});
- new MiniPipelineGraph({ container: '.js-builds-dropdown-tests' }).bindEvents();
+ new MiniPipelineGraph({ container: '.js-builds-dropdown-tests' }).bindEvents();
- document.querySelector('.js-builds-dropdown-button').click();
+ document.querySelector('.js-builds-dropdown-button').click();
- expect(getBuildsListSpy).toHaveBeenCalled();
- });
+ expect(getBuildsListSpy).toHaveBeenCalled();
+ });
- it('should make a request to the endpoint provided in the html', () => {
- const ajaxSpy = spyOn($, 'ajax').and.callFake(function () {});
+ it('should make a request to the endpoint provided in the html', () => {
+ const ajaxSpy = spyOn($, 'ajax').and.callFake(function () {});
- new MiniPipelineGraph({ container: '.js-builds-dropdown-tests' }).bindEvents();
+ new MiniPipelineGraph({ container: '.js-builds-dropdown-tests' }).bindEvents();
- document.querySelector('.js-builds-dropdown-button').click();
- expect(ajaxSpy.calls.allArgs()[0][0].url).toEqual('foobar');
- });
+ document.querySelector('.js-builds-dropdown-button').click();
+ expect(ajaxSpy.calls.allArgs()[0][0].url).toEqual('foobar');
+ });
- it('should not close when user uses cmd/ctrl + click', () => {
- spyOn($, 'ajax').and.callFake(function (params) {
- params.success({
- html: `<li>
- <a class="mini-pipeline-graph-dropdown-item" href="#">
- <span class="ci-status-icon ci-status-icon-failed"></span>
- <span class="ci-build-text">build</span>
- </a>
- <a class="ci-action-icon-wrapper js-ci-action-icon" href="#"></a>
- </li>`,
- });
+ it('should not close when user uses cmd/ctrl + click', () => {
+ spyOn($, 'ajax').and.callFake(function (params) {
+ params.success({
+ html: `<li>
+ <a class="mini-pipeline-graph-dropdown-item" href="#">
+ <span class="ci-status-icon ci-status-icon-failed"></span>
+ <span class="ci-build-text">build</span>
+ </a>
+ <a class="ci-action-icon-wrapper js-ci-action-icon" href="#"></a>
+ </li>`,
});
- new MiniPipelineGraph({ container: '.js-builds-dropdown-tests' }).bindEvents();
+ });
+ new MiniPipelineGraph({ container: '.js-builds-dropdown-tests' }).bindEvents();
- document.querySelector('.js-builds-dropdown-button').click();
+ document.querySelector('.js-builds-dropdown-button').click();
- document.querySelector('a.mini-pipeline-graph-dropdown-item').click();
+ document.querySelector('a.mini-pipeline-graph-dropdown-item').click();
- expect($('.js-builds-dropdown-list').is(':visible')).toEqual(true);
- });
+ expect($('.js-builds-dropdown-list').is(':visible')).toEqual(true);
});
});
-})();
+
+ it('should close the dropdown when request returns an error', (done) => {
+ spyOn($, 'ajax').and.callFake(options => options.error());
+
+ new MiniPipelineGraph({ container: '.js-builds-dropdown-tests' }).bindEvents();
+
+ document.querySelector('.js-builds-dropdown-button').click();
+
+ setTimeout(() => {
+ expect($('.js-builds-dropdown-tests .dropdown').hasClass('open')).toEqual(false);
+ done();
+ }, 0);
+ });
+});
diff --git a/spec/javascripts/monitoring/deployments_spec.js b/spec/javascripts/monitoring/deployments_spec.js
new file mode 100644
index 00000000000..19bc11d0f24
--- /dev/null
+++ b/spec/javascripts/monitoring/deployments_spec.js
@@ -0,0 +1,133 @@
+import d3 from 'd3';
+import PrometheusGraph from '~/monitoring/prometheus_graph';
+import Deployments from '~/monitoring/deployments';
+import { prometheusMockData } from './prometheus_mock_data';
+
+describe('Metrics deployments', () => {
+ const fixtureName = 'environments/metrics/metrics.html.raw';
+ let deployment;
+ let prometheusGraph;
+
+ const graphElement = () => document.querySelector('.prometheus-graph');
+
+ preloadFixtures(fixtureName);
+
+ beforeEach((done) => {
+ // Setup the view
+ loadFixtures(fixtureName);
+
+ d3.selectAll('.prometheus-graph')
+ .append('g')
+ .attr('class', 'graph-container');
+
+ prometheusGraph = new PrometheusGraph();
+ deployment = new Deployments(1000, 500);
+
+ spyOn(prometheusGraph, 'init');
+ spyOn($, 'ajax').and.callFake(() => {
+ const d = $.Deferred();
+ d.resolve({
+ deployments: [{
+ id: 1,
+ created_at: deployment.chartData[10].time,
+ sha: 'testing',
+ tag: false,
+ ref: {
+ name: 'testing',
+ },
+ }, {
+ id: 2,
+ created_at: deployment.chartData[15].time,
+ sha: '',
+ tag: true,
+ ref: {
+ name: 'tag',
+ },
+ }],
+ });
+
+ setTimeout(done);
+
+ return d.promise();
+ });
+
+ prometheusGraph.configureGraph();
+ prometheusGraph.transformData(prometheusMockData.metrics);
+
+ deployment.init(prometheusGraph.graphSpecificProperties.memory_values.data);
+ });
+
+ it('creates line on graph for deploment', () => {
+ expect(
+ graphElement().querySelectorAll('.deployment-line').length,
+ ).toBe(2);
+ });
+
+ it('creates hidden deploy boxes', () => {
+ expect(
+ graphElement().querySelectorAll('.prometheus-graph .js-deploy-info-box').length,
+ ).toBe(2);
+ });
+
+ it('hides the info boxes by default', () => {
+ expect(
+ graphElement().querySelectorAll('.prometheus-graph .js-deploy-info-box.hidden').length,
+ ).toBe(2);
+ });
+
+ it('shows sha short code when tag is false', () => {
+ expect(
+ graphElement().querySelector('.deploy-info-1-cpu_values .js-deploy-info-box').textContent.trim(),
+ ).toContain('testin');
+ });
+
+ it('shows ref name when tag is true', () => {
+ expect(
+ graphElement().querySelector('.deploy-info-2-cpu_values .js-deploy-info-box').textContent.trim(),
+ ).toContain('tag');
+ });
+
+ it('shows info box when moving mouse over line', () => {
+ deployment.mouseOverDeployInfo(deployment.data[0].xPos, 'cpu_values');
+
+ expect(
+ graphElement().querySelectorAll('.prometheus-graph .js-deploy-info-box.hidden').length,
+ ).toBe(1);
+
+ expect(
+ graphElement().querySelector('.deploy-info-1-cpu_values .js-deploy-info-box.hidden'),
+ ).toBeNull();
+ });
+
+ it('hides previously visible info box when moving mouse away', () => {
+ deployment.mouseOverDeployInfo(500, 'cpu_values');
+
+ expect(
+ graphElement().querySelectorAll('.prometheus-graph .js-deploy-info-box.hidden').length,
+ ).toBe(2);
+
+ expect(
+ graphElement().querySelector('.deploy-info-1-cpu_values .js-deploy-info-box.hidden'),
+ ).not.toBeNull();
+ });
+
+ describe('refText', () => {
+ it('returns shortened SHA', () => {
+ expect(
+ Deployments.refText({
+ tag: false,
+ sha: '123456789',
+ }),
+ ).toBe('123456');
+ });
+
+ it('returns tag name', () => {
+ expect(
+ Deployments.refText({
+ tag: true,
+ ref: 'v1.0',
+ }),
+ ).toBe('v1.0');
+ });
+ });
+});
diff --git a/spec/javascripts/monitoring/prometheus_graph_spec.js b/spec/javascripts/monitoring/prometheus_graph_spec.js
index 4b904fc2960..25578bf1c6e 100644
--- a/spec/javascripts/monitoring/prometheus_graph_spec.js
+++ b/spec/javascripts/monitoring/prometheus_graph_spec.js
@@ -3,7 +3,7 @@ import PrometheusGraph from '~/monitoring/prometheus_graph';
import { prometheusMockData } from './prometheus_mock_data';
describe('PrometheusGraph', () => {
- const fixtureName = 'static/environments/metrics.html.raw';
+ const fixtureName = 'environments/metrics/metrics.html.raw';
const prometheusGraphContainer = '.prometheus-graph';
const prometheusGraphContents = `${prometheusGraphContainer}[graph-type=cpu_values]`;
@@ -77,7 +77,7 @@ describe('PrometheusGraph', () => {
});
describe('PrometheusGraphs UX states', () => {
- const fixtureName = 'static/environments/metrics.html.raw';
+ const fixtureName = 'environments/metrics/metrics.html.raw';
preloadFixtures(fixtureName);
beforeEach(() => {
diff --git a/spec/javascripts/notebook/cells/code_spec.js b/spec/javascripts/notebook/cells/code_spec.js
new file mode 100644
index 00000000000..0c432d73f67
--- /dev/null
+++ b/spec/javascripts/notebook/cells/code_spec.js
@@ -0,0 +1,55 @@
+import Vue from 'vue';
+import CodeComponent from '~/notebook/cells/code.vue';
+
+const Component = Vue.extend(CodeComponent);
+
+describe('Code component', () => {
+ let vm;
+ let json;
+
+ beforeEach(() => {
+ json = getJSONFixture('blob/notebook/basic.json');
+ });
+
+ describe('without output', () => {
+ beforeEach((done) => {
+ vm = new Component({
+ propsData: {
+ cell: json.cells[0],
+ },
+ });
+ vm.$mount();
+
+ setTimeout(() => {
+ done();
+ });
+ });
+
+ it('does not render output prompt', () => {
+ expect(vm.$el.querySelectorAll('.prompt').length).toBe(1);
+ });
+ });
+
+ describe('with output', () => {
+ beforeEach((done) => {
+ vm = new Component({
+ propsData: {
+ cell: json.cells[2],
+ },
+ });
+ vm.$mount();
+
+ setTimeout(() => {
+ done();
+ });
+ });
+
+ it('does not render output prompt', () => {
+ expect(vm.$el.querySelectorAll('.prompt').length).toBe(2);
+ });
+
+ it('renders output cell', () => {
+ expect(vm.$el.querySelector('.output')).toBeDefined();
+ });
+ });
+});
diff --git a/spec/javascripts/notebook/cells/markdown_spec.js b/spec/javascripts/notebook/cells/markdown_spec.js
new file mode 100644
index 00000000000..38c976f38d8
--- /dev/null
+++ b/spec/javascripts/notebook/cells/markdown_spec.js
@@ -0,0 +1,41 @@
+import Vue from 'vue';
+import MarkdownComponent from '~/notebook/cells/markdown.vue';
+
+const Component = Vue.extend(MarkdownComponent);
+
+describe('Markdown component', () => {
+ let vm;
+ let cell;
+ let json;
+
+ beforeEach((done) => {
+ json = getJSONFixture('blob/notebook/basic.json');
+
+ cell = json.cells[1];
+
+ vm = new Component({
+ propsData: {
+ cell,
+ },
+ });
+ vm.$mount();
+
+ setTimeout(() => {
+ done();
+ });
+ });
+
+ it('does not render promot', () => {
+ expect(vm.$el.querySelector('.prompt span')).toBeNull();
+ });
+
+ it('does not render the markdown text', () => {
+ expect(
+ vm.$el.querySelector('.markdown').innerHTML.trim(),
+ ).not.toEqual(cell.source.join(''));
+ });
+
+ it('renders the markdown HTML', () => {
+ expect(vm.$el.querySelector('.markdown h1')).not.toBeNull();
+ });
+});
diff --git a/spec/javascripts/notebook/cells/output/index_spec.js b/spec/javascripts/notebook/cells/output/index_spec.js
new file mode 100644
index 00000000000..dbf79f85c7c
--- /dev/null
+++ b/spec/javascripts/notebook/cells/output/index_spec.js
@@ -0,0 +1,126 @@
+import Vue from 'vue';
+import CodeComponent from '~/notebook/cells/output/index.vue';
+
+const Component = Vue.extend(CodeComponent);
+
+describe('Output component', () => {
+ let vm;
+ let json;
+
+ const createComponent = (output) => {
+ vm = new Component({
+ propsData: {
+ output,
+ count: 1,
+ },
+ });
+ vm.$mount();
+ };
+
+ beforeEach(() => {
+ json = getJSONFixture('blob/notebook/basic.json');
+ });
+
+ describe('text output', () => {
+ beforeEach((done) => {
+ createComponent(json.cells[2].outputs[0]);
+
+ setTimeout(() => {
+ done();
+ });
+ });
+
+ it('renders as plain text', () => {
+ expect(vm.$el.querySelector('pre')).not.toBeNull();
+ });
+
+ it('renders promot', () => {
+ expect(vm.$el.querySelector('.prompt span')).not.toBeNull();
+ });
+ });
+
+ describe('image output', () => {
+ beforeEach((done) => {
+ createComponent(json.cells[3].outputs[0]);
+
+ setTimeout(() => {
+ done();
+ });
+ });
+
+ it('renders as an image', () => {
+ expect(vm.$el.querySelector('img')).not.toBeNull();
+ });
+
+ it('does not render the prompt', () => {
+ expect(vm.$el.querySelector('.prompt span')).toBeNull();
+ });
+ });
+
+ describe('html output', () => {
+ beforeEach((done) => {
+ createComponent(json.cells[4].outputs[0]);
+
+ setTimeout(() => {
+ done();
+ });
+ });
+
+ it('renders raw HTML', () => {
+ expect(vm.$el.querySelector('p')).not.toBeNull();
+ expect(vm.$el.textContent.trim()).toBe('test');
+ });
+
+ it('does not render the prompt', () => {
+ expect(vm.$el.querySelector('.prompt span')).toBeNull();
+ });
+ });
+
+ describe('svg output', () => {
+ beforeEach((done) => {
+ createComponent(json.cells[5].outputs[0]);
+
+ setTimeout(() => {
+ done();
+ });
+ });
+
+ it('renders as an svg', () => {
+ expect(vm.$el.querySelector('svg')).not.toBeNull();
+ });
+
+ it('does not render the prompt', () => {
+ expect(vm.$el.querySelector('.prompt span')).toBeNull();
+ });
+ });
+
+ describe('default to plain text', () => {
+ beforeEach((done) => {
+ createComponent(json.cells[6].outputs[0]);
+
+ setTimeout(() => {
+ done();
+ });
+ });
+
+ it('renders as plain text', () => {
+ expect(vm.$el.querySelector('pre')).not.toBeNull();
+ expect(vm.$el.textContent.trim()).toContain('testing');
+ });
+
+ it('renders promot', () => {
+ expect(vm.$el.querySelector('.prompt span')).not.toBeNull();
+ });
+
+ it('renders as plain text when doesn\'t recognise other types', (done) => {
+ createComponent(json.cells[7].outputs[0]);
+
+ setTimeout(() => {
+ expect(vm.$el.querySelector('pre')).not.toBeNull();
+ expect(vm.$el.textContent.trim()).toContain('testing');
+
+ done();
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/notebook/cells/prompt_spec.js b/spec/javascripts/notebook/cells/prompt_spec.js
new file mode 100644
index 00000000000..207fa433a59
--- /dev/null
+++ b/spec/javascripts/notebook/cells/prompt_spec.js
@@ -0,0 +1,56 @@
+import Vue from 'vue';
+import PromptComponent from '~/notebook/cells/prompt.vue';
+
+const Component = Vue.extend(PromptComponent);
+
+describe('Prompt component', () => {
+ let vm;
+
+ describe('input', () => {
+ beforeEach((done) => {
+ vm = new Component({
+ propsData: {
+ type: 'In',
+ count: 1,
+ },
+ });
+ vm.$mount();
+
+ setTimeout(() => {
+ done();
+ });
+ });
+
+ it('renders in label', () => {
+ expect(vm.$el.textContent.trim()).toContain('In');
+ });
+
+ it('renders count', () => {
+ expect(vm.$el.textContent.trim()).toContain('1');
+ });
+ });
+
+ describe('output', () => {
+ beforeEach((done) => {
+ vm = new Component({
+ propsData: {
+ type: 'Out',
+ count: 1,
+ },
+ });
+ vm.$mount();
+
+ setTimeout(() => {
+ done();
+ });
+ });
+
+ it('renders in label', () => {
+ expect(vm.$el.textContent.trim()).toContain('Out');
+ });
+
+ it('renders count', () => {
+ expect(vm.$el.textContent.trim()).toContain('1');
+ });
+ });
+});
diff --git a/spec/javascripts/notebook/index_spec.js b/spec/javascripts/notebook/index_spec.js
new file mode 100644
index 00000000000..bd63ab35426
--- /dev/null
+++ b/spec/javascripts/notebook/index_spec.js
@@ -0,0 +1,98 @@
+import Vue from 'vue';
+import Notebook from '~/notebook/index.vue';
+
+const Component = Vue.extend(Notebook);
+
+describe('Notebook component', () => {
+ let vm;
+ let json;
+ let jsonWithWorksheet;
+
+ beforeEach(() => {
+ json = getJSONFixture('blob/notebook/basic.json');
+ jsonWithWorksheet = getJSONFixture('blob/notebook/worksheets.json');
+ });
+
+ describe('without JSON', () => {
+ beforeEach((done) => {
+ vm = new Component({
+ propsData: {
+ notebook: {},
+ },
+ });
+ vm.$mount();
+
+ setTimeout(() => {
+ done();
+ });
+ });
+
+ it('does not render', () => {
+ expect(vm.$el.tagName).toBeUndefined();
+ });
+ });
+
+ describe('with JSON', () => {
+ beforeEach((done) => {
+ vm = new Component({
+ propsData: {
+ notebook: json,
+ codeCssClass: 'js-code-class',
+ },
+ });
+ vm.$mount();
+
+ setTimeout(() => {
+ done();
+ });
+ });
+
+ it('renders cells', () => {
+ expect(vm.$el.querySelectorAll('.cell').length).toBe(json.cells.length);
+ });
+
+ it('renders markdown cell', () => {
+ expect(vm.$el.querySelector('.markdown')).not.toBeNull();
+ });
+
+ it('renders code cell', () => {
+ expect(vm.$el.querySelector('pre')).not.toBeNull();
+ });
+
+ it('add code class to code blocks', () => {
+ expect(vm.$el.querySelector('.js-code-class')).not.toBeNull();
+ });
+ });
+
+ describe('with worksheets', () => {
+ beforeEach((done) => {
+ vm = new Component({
+ propsData: {
+ notebook: jsonWithWorksheet,
+ codeCssClass: 'js-code-class',
+ },
+ });
+ vm.$mount();
+
+ setTimeout(() => {
+ done();
+ });
+ });
+
+ it('renders cells', () => {
+ expect(vm.$el.querySelectorAll('.cell').length).toBe(jsonWithWorksheet.worksheets[0].cells.length);
+ });
+
+ it('renders markdown cell', () => {
+ expect(vm.$el.querySelector('.markdown')).not.toBeNull();
+ });
+
+ it('renders code cell', () => {
+ expect(vm.$el.querySelector('pre')).not.toBeNull();
+ });
+
+ it('add code class to code blocks', () => {
+ expect(vm.$el.querySelector('.js-code-class')).not.toBeNull();
+ });
+ });
+});
diff --git a/spec/javascripts/notebook/lib/highlight_spec.js b/spec/javascripts/notebook/lib/highlight_spec.js
new file mode 100644
index 00000000000..d71c5718858
--- /dev/null
+++ b/spec/javascripts/notebook/lib/highlight_spec.js
@@ -0,0 +1,15 @@
+import Prism from '~/notebook/lib/highlight';
+
+describe('Highlight library', () => {
+ it('imports python language', () => {
+ expect(Prism.languages.python).toBeDefined();
+ });
+
+ it('uses custom CSS classes', () => {
+ const el = document.createElement('div');
+ el.innerHTML = Prism.highlight('console.log("a");', Prism.languages.javascript);
+
+ expect(el.querySelector('.s')).not.toBeNull();
+ expect(el.querySelector('.nf')).not.toBeNull();
+ });
+});
diff --git a/spec/javascripts/notes_spec.js b/spec/javascripts/notes_spec.js
index ca8ee04d955..be4605a5b89 100644
--- a/spec/javascripts/notes_spec.js
+++ b/spec/javascripts/notes_spec.js
@@ -1,10 +1,12 @@
/* eslint-disable space-before-function-paren, no-unused-expressions, no-var, object-shorthand, comma-dangle, max-len */
/* global Notes */
-require('~/notes');
-require('vendor/autosize');
-require('~/gl_form');
-require('~/lib/utils/text_utility');
+import 'vendor/autosize';
+import '~/gl_form';
+import '~/lib/utils/text_utility';
+import '~/render_gfm';
+import '~/render_math';
+import '~/notes';
(function() {
window.gon || (window.gon = {});
@@ -24,10 +26,10 @@ require('~/lib/utils/text_utility');
describe('task lists', function() {
beforeEach(function() {
- $('form').on('submit', function(e) {
+ $('.js-comment-button').on('click', function(e) {
e.preventDefault();
});
- this.notes = new Notes();
+ this.notes = new Notes('', []);
});
it('modifies the Markdown field', function() {
@@ -49,7 +51,7 @@ require('~/lib/utils/text_utility');
var textarea = '.js-note-text';
beforeEach(function() {
- this.notes = new Notes();
+ this.notes = new Notes('', []);
this.autoSizeSpy = spyOnEvent($(textarea), 'autosize:update');
spyOn(this.notes, 'renderNote').and.stub();
@@ -58,9 +60,12 @@ require('~/lib/utils/text_utility');
reset: function() {}
});
- $('form').on('submit', function(e) {
+ $('.js-comment-button').on('click', (e) => {
+ const $form = $(this);
e.preventDefault();
- $('.js-main-target-form').trigger('ajax:success');
+ this.notes.addNote($form);
+ this.notes.reenableTargetFormSubmitButton(e);
+ this.notes.resetMainTargetForm(e);
});
});
@@ -80,35 +85,78 @@ require('~/lib/utils/text_utility');
beforeEach(() => {
note = {
+ id: 1,
discussion_html: null,
valid: true,
- html: '<div></div>',
+ note: 'heya',
+ html: '<div>heya</div>',
};
- $notesList = jasmine.createSpyObj('$notesList', ['find']);
+ $notesList = jasmine.createSpyObj('$notesList', [
+ 'find',
+ 'append',
+ ]);
notes = jasmine.createSpyObj('notes', [
'refresh',
'isNewNote',
+ 'isUpdatedNote',
'collapseLongCommitList',
'updateNotesCount',
+ 'putConflictEditWarningInPlace'
]);
notes.taskList = jasmine.createSpyObj('tasklist', ['init']);
notes.note_ids = [];
+ notes.updatedNotesTrackingMap = {};
- spyOn(window, '$').and.returnValue($notesList);
spyOn(gl.utils, 'localTimeAgo');
- spyOn(Notes, 'animateAppendNote');
- notes.isNewNote.and.returnValue(true);
-
- Notes.prototype.renderNote.call(notes, note);
+ spyOn(Notes, 'animateAppendNote').and.callThrough();
+ spyOn(Notes, 'animateUpdateNote').and.callThrough();
});
- it('should query for the notes list', () => {
- expect(window.$).toHaveBeenCalledWith('ul.main-notes-list');
+ describe('when adding note', () => {
+ it('should call .animateAppendNote', () => {
+ notes.isNewNote.and.returnValue(true);
+ Notes.prototype.renderNote.call(notes, note, null, $notesList);
+
+ expect(Notes.animateAppendNote).toHaveBeenCalledWith(note.html, $notesList);
+ });
});
- it('should call .animateAppendNote', () => {
- expect(Notes.animateAppendNote).toHaveBeenCalledWith(note.html, $notesList);
+ describe('when note was edited', () => {
+ it('should call .animateUpdateNote', () => {
+ notes.isUpdatedNote.and.returnValue(true);
+ const $note = $('<div>');
+ $notesList.find.and.returnValue($note);
+ Notes.prototype.renderNote.call(notes, note, null, $notesList);
+
+ expect(Notes.animateUpdateNote).toHaveBeenCalledWith(note.html, $note);
+ });
+
+ describe('while editing', () => {
+ it('should update textarea if nothing has been touched', () => {
+ notes.isUpdatedNote.and.returnValue(true);
+ const $note = $(`<div class="is-editing">
+ <div class="original-note-content">initial</div>
+ <textarea class="js-note-text">initial</textarea>
+ </div>`);
+ $notesList.find.and.returnValue($note);
+ Notes.prototype.renderNote.call(notes, note, null, $notesList);
+
+ expect($note.find('.js-note-text').val()).toEqual(note.note);
+ });
+
+ it('should call .putConflictEditWarningInPlace', () => {
+ notes.isUpdatedNote.and.returnValue(true);
+ const $note = $(`<div class="is-editing">
+ <div class="original-note-content">initial</div>
+ <textarea class="js-note-text">different</textarea>
+ </div>`);
+ $notesList.find.and.returnValue($note);
+ Notes.prototype.renderNote.call(notes, note, null, $notesList);
+
+ expect(notes.putConflictEditWarningInPlace).toHaveBeenCalledWith(note, $note);
+ });
+ });
});
});
@@ -147,14 +195,12 @@ require('~/lib/utils/text_utility');
});
describe('Discussion root note', () => {
- let $notesList;
let body;
beforeEach(() => {
body = jasmine.createSpyObj('body', ['attr']);
discussionContainer = { length: 0 };
- spyOn(window, '$').and.returnValues(discussionContainer, body, $notesList);
$form.closest.and.returnValues(row, $form);
$form.find.and.returnValues(discussionContainer);
body.attr.and.returnValue('');
@@ -162,12 +208,8 @@ require('~/lib/utils/text_utility');
Notes.prototype.renderDiscussionNote.call(notes, note, $form);
});
- it('should query for the notes list', () => {
- expect(window.$.calls.argsFor(2)).toEqual(['ul.main-notes-list']);
- });
-
it('should call Notes.animateAppendNote', () => {
- expect(Notes.animateAppendNote).toHaveBeenCalledWith(note.discussion_html, $notesList);
+ expect(Notes.animateAppendNote).toHaveBeenCalledWith(note.discussion_html, $('.main-notes-list'));
});
});
@@ -175,16 +217,12 @@ require('~/lib/utils/text_utility');
beforeEach(() => {
discussionContainer = { length: 1 };
- spyOn(window, '$').and.returnValues(discussionContainer);
- $form.closest.and.returnValues(row);
+ $form.closest.and.returnValues(row, $form);
+ $form.find.and.returnValues(discussionContainer);
Notes.prototype.renderDiscussionNote.call(notes, note, $form);
});
- it('should query foor the discussion container', () => {
- expect(window.$).toHaveBeenCalledWith(`.notes[data-discussion-id="${note.discussion_id}"]`);
- });
-
it('should call Notes.animateAppendNote', () => {
expect(Notes.animateAppendNote).toHaveBeenCalledWith(note.html, discussionContainer);
});
@@ -193,35 +231,233 @@ require('~/lib/utils/text_utility');
describe('animateAppendNote', () => {
let noteHTML;
- let $note;
let $notesList;
+ let $resultantNote;
beforeEach(() => {
noteHTML = '<div></div>';
- $note = jasmine.createSpyObj('$note', ['addClass', 'renderGFM', 'removeClass']);
$notesList = jasmine.createSpyObj('$notesList', ['append']);
- spyOn(window, '$').and.returnValue($note);
- spyOn(window, 'setTimeout').and.callThrough();
- $note.addClass.and.returnValue($note);
- $note.renderGFM.and.returnValue($note);
+ $resultantNote = Notes.animateAppendNote(noteHTML, $notesList);
+ });
- Notes.animateAppendNote(noteHTML, $notesList);
+ it('should have `fade-in-full` class', () => {
+ expect($resultantNote.hasClass('fade-in-full')).toEqual(true);
});
- it('should init the note jquery object', () => {
- expect(window.$).toHaveBeenCalledWith(noteHTML);
+ it('should append note to the notes list', () => {
+ expect($notesList.append).toHaveBeenCalledWith($resultantNote);
});
+ });
+
+ describe('animateUpdateNote', () => {
+ let noteHTML;
+ let $note;
+ let $updatedNote;
- it('should call addClass', () => {
- expect($note.addClass).toHaveBeenCalledWith('fade-in');
+ beforeEach(() => {
+ noteHTML = '<div></div>';
+ $note = jasmine.createSpyObj('$note', [
+ 'replaceWith'
+ ]);
+
+ $updatedNote = Notes.animateUpdateNote(noteHTML, $note);
});
- it('should call renderGFM', () => {
- expect($note.renderGFM).toHaveBeenCalledWith();
+
+ it('should have `fade-in` class', () => {
+ expect($updatedNote.hasClass('fade-in')).toEqual(true);
});
- it('should append note to the notes list', () => {
- expect($notesList.append).toHaveBeenCalledWith($note);
+ it('should call replaceWith on $note', () => {
+ expect($note.replaceWith).toHaveBeenCalledWith($updatedNote);
+ });
+ });
+
+ describe('postComment & updateComment', () => {
+ const sampleComment = 'foo';
+ const updatedComment = 'bar';
+ const note = {
+ id: 1234,
+ html: `<li class="note note-row-1234 timeline-entry" id="note_1234">
+ <div class="note-text">${sampleComment}</div>
+ </li>`,
+ note: sampleComment,
+ valid: true
+ };
+ let $form;
+ let $notesContainer;
+
+ beforeEach(() => {
+ this.notes = new Notes('', []);
+ window.gon.current_username = 'root';
+ window.gon.current_user_fullname = 'Administrator';
+ $form = $('form.js-main-target-form');
+ $notesContainer = $('ul.main-notes-list');
+ $form.find('textarea.js-note-text').val(sampleComment);
+ });
+
+ it('should show placeholder note while new comment is being posted', () => {
+ $('.js-comment-button').click();
+ expect($notesContainer.find('.note.being-posted').length > 0).toEqual(true);
+ });
+
+ it('should remove placeholder note when new comment is done posting', () => {
+ const deferred = $.Deferred();
+ spyOn($, 'ajax').and.returnValue(deferred.promise());
+ $('.js-comment-button').click();
+
+ deferred.resolve(note);
+ expect($notesContainer.find('.note.being-posted').length).toEqual(0);
+ });
+
+ it('should show actual note element when new comment is done posting', () => {
+ const deferred = $.Deferred();
+ spyOn($, 'ajax').and.returnValue(deferred.promise());
+ $('.js-comment-button').click();
+
+ deferred.resolve(note);
+ expect($notesContainer.find(`#note_${note.id}`).length > 0).toEqual(true);
+ });
+
+ it('should reset Form when new comment is done posting', () => {
+ const deferred = $.Deferred();
+ spyOn($, 'ajax').and.returnValue(deferred.promise());
+ $('.js-comment-button').click();
+
+ deferred.resolve(note);
+ expect($form.find('textarea.js-note-text').val()).toEqual('');
+ });
+
+ it('should show flash error message when new comment failed to be posted', () => {
+ const deferred = $.Deferred();
+ spyOn($, 'ajax').and.returnValue(deferred.promise());
+ $('.js-comment-button').click();
+
+ deferred.reject();
+ expect($notesContainer.parent().find('.flash-container .flash-text').is(':visible')).toEqual(true);
+ });
+
+ it('should show flash error message when comment failed to be updated', () => {
+ const deferred = $.Deferred();
+ spyOn($, 'ajax').and.returnValue(deferred.promise());
+ $('.js-comment-button').click();
+
+ deferred.resolve(note);
+ const $noteEl = $notesContainer.find(`#note_${note.id}`);
+ $noteEl.find('.js-note-edit').click();
+ $noteEl.find('textarea.js-note-text').val(updatedComment);
+ $noteEl.find('.js-comment-save-button').click();
+
+ deferred.reject();
+ const $updatedNoteEl = $notesContainer.find(`#note_${note.id}`);
+ expect($updatedNoteEl.hasClass('.being-posted')).toEqual(false); // Remove being-posted visuals
+ expect($updatedNoteEl.find('.note-text').text().trim()).toEqual(sampleComment); // See if comment reverted back to original
+ expect($('.flash-container').is(':visible')).toEqual(true); // Flash error message shown
+ });
+ });
+
+ describe('getFormData', () => {
+ it('should return form metadata object from form reference', () => {
+ this.notes = new Notes('', []);
+
+ const $form = $('form');
+ const sampleComment = 'foobar';
+ $form.find('textarea.js-note-text').val(sampleComment);
+ const { formData, formContent, formAction } = this.notes.getFormData($form);
+
+ expect(formData.indexOf(sampleComment) > -1).toBe(true);
+ expect(formContent).toEqual(sampleComment);
+ expect(formAction).toEqual($form.attr('action'));
+ });
+ });
+
+ describe('hasSlashCommands', () => {
+ beforeEach(() => {
+ this.notes = new Notes('', []);
+ });
+
+ it('should return true when comment begins with a slash command', () => {
+ const sampleComment = '/wip \n/milestone %1.0 \n/merge \n/unassign Merging this';
+ const hasSlashCommands = this.notes.hasSlashCommands(sampleComment);
+
+ expect(hasSlashCommands).toBeTruthy();
+ });
+
+ it('should return false when comment does NOT begin with a slash command', () => {
+ const sampleComment = 'Hey, /unassign Merging this';
+ const hasSlashCommands = this.notes.hasSlashCommands(sampleComment);
+
+ expect(hasSlashCommands).toBeFalsy();
+ });
+
+ it('should return false when comment does NOT have any slash commands', () => {
+ const sampleComment = 'Looking good, Awesome!';
+ const hasSlashCommands = this.notes.hasSlashCommands(sampleComment);
+
+ expect(hasSlashCommands).toBeFalsy();
+ });
+ });
+
+ describe('stripSlashCommands', () => {
+ it('should strip slash commands from the comment which begins with a slash command', () => {
+ this.notes = new Notes();
+ const sampleComment = '/wip \n/milestone %1.0 \n/merge \n/unassign Merging this';
+ const stripedComment = this.notes.stripSlashCommands(sampleComment);
+
+ expect(stripedComment).not.toBe(sampleComment);
+ });
+
+ it('should NOT strip string that has slashes within', () => {
+ this.notes = new Notes();
+ const sampleComment = 'http://127.0.0.1:3000/root/gitlab-shell/issues/1';
+ const stripedComment = this.notes.stripSlashCommands(sampleComment);
+
+ expect(stripedComment).toBe(sampleComment);
+ });
+ });
+
+ describe('createPlaceholderNote', () => {
+ const sampleComment = 'foobar';
+ const uniqueId = 'b1234-a4567';
+ const currentUsername = 'root';
+ const currentUserFullname = 'Administrator';
+
+ beforeEach(() => {
+ this.notes = new Notes('', []);
+ });
+
+ it('should return constructed placeholder element for regular note based on form contents', () => {
+ const $tempNote = this.notes.createPlaceholderNote({
+ formContent: sampleComment,
+ uniqueId,
+ isDiscussionNote: false,
+ currentUsername,
+ currentUserFullname
+ });
+ const $tempNoteHeader = $tempNote.find('.note-header');
+
+ expect($tempNote.prop('nodeName')).toEqual('LI');
+ expect($tempNote.attr('id')).toEqual(uniqueId);
+ $tempNote.find('.timeline-icon > a, .note-header-info > a').each(function() {
+ expect($(this).attr('href')).toEqual(`/${currentUsername}`);
+ });
+ expect($tempNote.find('.timeline-content').hasClass('discussion')).toBeFalsy();
+ expect($tempNoteHeader.find('.hidden-xs').text().trim()).toEqual(currentUserFullname);
+ expect($tempNoteHeader.find('.note-headline-light').text().trim()).toEqual(`@${currentUsername}`);
+ expect($tempNote.find('.note-body .note-text').text().trim()).toEqual(sampleComment);
+ });
+
+ it('should return constructed placeholder element for discussion note based on form contents', () => {
+ const $tempNote = this.notes.createPlaceholderNote({
+ formContent: sampleComment,
+ uniqueId,
+ isDiscussionNote: true,
+ currentUsername,
+ currentUserFullname
+ });
+
+ expect($tempNote.prop('nodeName')).toEqual('LI');
+ expect($tempNote.find('.timeline-content').hasClass('discussion')).toBeTruthy();
});
});
});
diff --git a/spec/javascripts/pdf/index_spec.js b/spec/javascripts/pdf/index_spec.js
new file mode 100644
index 00000000000..f661fae5fe2
--- /dev/null
+++ b/spec/javascripts/pdf/index_spec.js
@@ -0,0 +1,61 @@
+/* eslint-disable import/no-unresolved */
+
+import Vue from 'vue';
+import { PDFJS } from 'pdfjs-dist';
+import workerSrc from 'vendor/pdf.worker';
+
+import PDFLab from '~/pdf/index.vue';
+import pdf from '../fixtures/blob/pdf/test.pdf';
+
+PDFJS.workerSrc = workerSrc;
+const Component = Vue.extend(PDFLab);
+
+describe('PDF component', () => {
+ let vm;
+
+ const checkLoaded = (done) => {
+ if (vm.loading) {
+ setTimeout(() => {
+ checkLoaded(done);
+ }, 100);
+ } else {
+ done();
+ }
+ };
+
+ describe('without PDF data', () => {
+ beforeEach((done) => {
+ vm = new Component({
+ propsData: {
+ pdf: '',
+ },
+ });
+
+ vm.$mount();
+
+ checkLoaded(done);
+ });
+
+ it('does not render', () => {
+ expect(vm.$el.tagName).toBeUndefined();
+ });
+ });
+
+ describe('with PDF data', () => {
+ beforeEach((done) => {
+ vm = new Component({
+ propsData: {
+ pdf,
+ },
+ });
+
+ vm.$mount();
+
+ checkLoaded(done);
+ });
+
+ it('renders pdf component', () => {
+ expect(vm.$el.tagName).toBeDefined();
+ });
+ });
+});
diff --git a/spec/javascripts/pdf/page_spec.js b/spec/javascripts/pdf/page_spec.js
new file mode 100644
index 00000000000..ac76ebbfbe6
--- /dev/null
+++ b/spec/javascripts/pdf/page_spec.js
@@ -0,0 +1,57 @@
+/* eslint-disable import/no-unresolved */
+
+import Vue from 'vue';
+import pdfjsLib from 'pdfjs-dist';
+import workerSrc from 'vendor/pdf.worker';
+
+import PageComponent from '~/pdf/page/index.vue';
+import testPDF from '../fixtures/blob/pdf/test.pdf';
+
+const Component = Vue.extend(PageComponent);
+
+describe('Page component', () => {
+ let vm;
+ let testPage;
+ pdfjsLib.PDFJS.workerSrc = workerSrc;
+
+ const checkRendered = (done) => {
+ if (vm.rendering) {
+ setTimeout(() => {
+ checkRendered(done);
+ }, 100);
+ } else {
+ done();
+ }
+ };
+
+ beforeEach((done) => {
+ pdfjsLib.getDocument(testPDF)
+ .then(pdf => pdf.getPage(1))
+ .then((page) => {
+ testPage = page;
+ done();
+ })
+ .catch((error) => {
+ console.error(error);
+ });
+ });
+
+ describe('render', () => {
+ beforeEach((done) => {
+ vm = new Component({
+ propsData: {
+ page: testPage,
+ number: 1,
+ },
+ });
+
+ vm.$mount();
+
+ checkRendered(done);
+ });
+
+ it('renders first page', () => {
+ expect(vm.$el.tagName).toBeDefined();
+ });
+ });
+});
diff --git a/spec/javascripts/pipeline_schedules/interval_pattern_input_spec.js b/spec/javascripts/pipeline_schedules/interval_pattern_input_spec.js
new file mode 100644
index 00000000000..08fa6ca9057
--- /dev/null
+++ b/spec/javascripts/pipeline_schedules/interval_pattern_input_spec.js
@@ -0,0 +1,217 @@
+import Vue from 'vue';
+import IntervalPatternInput from '~/pipeline_schedules/components/interval_pattern_input';
+
+const IntervalPatternInputComponent = Vue.extend(IntervalPatternInput);
+const inputNameAttribute = 'schedule[cron]';
+
+const cronIntervalPresets = {
+ everyDay: '0 4 * * *',
+ everyWeek: '0 4 * * 0',
+ everyMonth: '0 4 1 * *',
+};
+
+window.gl = window.gl || {};
+
+window.gl.pipelineScheduleFieldErrors = {
+ updateFormValidityState: () => {},
+};
+
+describe('Interval Pattern Input Component', function () {
+ describe('when prop initialCronInterval is passed (edit)', function () {
+ describe('when prop initialCronInterval is custom', function () {
+ beforeEach(function () {
+ this.initialCronInterval = '1 2 3 4 5';
+ this.intervalPatternComponent = new IntervalPatternInputComponent({
+ propsData: {
+ initialCronInterval: this.initialCronInterval,
+ },
+ }).$mount();
+ });
+
+ it('is initialized as a Vue component', function () {
+ expect(this.intervalPatternComponent).toBeDefined();
+ });
+
+ it('prop initialCronInterval is set', function () {
+ expect(this.intervalPatternComponent.initialCronInterval).toBe(this.initialCronInterval);
+ });
+
+ it('sets showUnsetWarning to false', function (done) {
+ Vue.nextTick(() => {
+ expect(this.intervalPatternComponent.showUnsetWarning).toBe(false);
+ done();
+ });
+ });
+
+ it('does not render showUnsetWarning', function (done) {
+ Vue.nextTick(() => {
+ expect(this.intervalPatternComponent.$el.outerHTML).not.toContain('Schedule not yet set');
+ done();
+ });
+ });
+
+ it('sets isEditable to true', function (done) {
+ Vue.nextTick(() => {
+ expect(this.intervalPatternComponent.isEditable).toBe(true);
+ done();
+ });
+ });
+ });
+
+ describe('when prop initialCronInterval is preset', function () {
+ beforeEach(function () {
+ this.intervalPatternComponent = new IntervalPatternInputComponent({
+ propsData: {
+ inputNameAttribute,
+ initialCronInterval: '0 4 * * *',
+ },
+ }).$mount();
+ });
+
+ it('is initialized as a Vue component', function () {
+ expect(this.intervalPatternComponent).toBeDefined();
+ });
+
+ it('sets showUnsetWarning to false', function (done) {
+ Vue.nextTick(() => {
+ expect(this.intervalPatternComponent.showUnsetWarning).toBe(false);
+ done();
+ });
+ });
+
+ it('does not render showUnsetWarning', function (done) {
+ Vue.nextTick(() => {
+ expect(this.intervalPatternComponent.$el.outerHTML).not.toContain('Schedule not yet set');
+ done();
+ });
+ });
+
+ it('sets isEditable to false', function (done) {
+ Vue.nextTick(() => {
+ expect(this.intervalPatternComponent.isEditable).toBe(false);
+ done();
+ });
+ });
+ });
+ });
+
+ describe('when prop initialCronInterval is not passed (new)', function () {
+ beforeEach(function () {
+ this.intervalPatternComponent = new IntervalPatternInputComponent({
+ propsData: {
+ inputNameAttribute,
+ },
+ }).$mount();
+ });
+
+ it('is initialized as a Vue component', function () {
+ expect(this.intervalPatternComponent).toBeDefined();
+ });
+
+ it('prop initialCronInterval is set', function () {
+ const defaultInitialCronInterval = '';
+ expect(this.intervalPatternComponent.initialCronInterval).toBe(defaultInitialCronInterval);
+ });
+
+ it('sets showUnsetWarning to true', function (done) {
+ Vue.nextTick(() => {
+ expect(this.intervalPatternComponent.showUnsetWarning).toBe(true);
+ done();
+ });
+ });
+
+ it('renders showUnsetWarning to true', function (done) {
+ Vue.nextTick(() => {
+ expect(this.intervalPatternComponent.$el.outerHTML).toContain('Schedule not yet set');
+ done();
+ });
+ });
+
+ it('sets isEditable to true', function (done) {
+ Vue.nextTick(() => {
+ expect(this.intervalPatternComponent.isEditable).toBe(true);
+ done();
+ });
+ });
+ });
+
+ describe('User Actions', function () {
+ beforeEach(function () {
+ // For an unknown reason, Phantom.js doesn't trigger click events
+ // on radio buttons in a way Vue can register. So, we have to mount
+ // to a fixture.
+ setFixtures('<div id="my-mount"></div>');
+
+ this.initialCronInterval = '1 2 3 4 5';
+ this.intervalPatternComponent = new IntervalPatternInputComponent({
+ propsData: {
+ initialCronInterval: this.initialCronInterval,
+ },
+ }).$mount('#my-mount');
+ });
+
+ it('cronInterval is updated when everyday preset interval is selected', function (done) {
+ this.intervalPatternComponent.$el.querySelector('#every-day').click();
+
+ Vue.nextTick(() => {
+ expect(this.intervalPatternComponent.cronInterval).toBe(cronIntervalPresets.everyDay);
+ expect(this.intervalPatternComponent.$el.querySelector('.cron-interval-input').value).toBe(cronIntervalPresets.everyDay);
+ done();
+ });
+ });
+
+ it('cronInterval is updated when everyweek preset interval is selected', function (done) {
+ this.intervalPatternComponent.$el.querySelector('#every-week').click();
+
+ Vue.nextTick(() => {
+ expect(this.intervalPatternComponent.cronInterval).toBe(cronIntervalPresets.everyWeek);
+ expect(this.intervalPatternComponent.$el.querySelector('.cron-interval-input').value).toBe(cronIntervalPresets.everyWeek);
+
+ done();
+ });
+ });
+
+ it('cronInterval is updated when everymonth preset interval is selected', function (done) {
+ this.intervalPatternComponent.$el.querySelector('#every-month').click();
+
+ Vue.nextTick(() => {
+ expect(this.intervalPatternComponent.cronInterval).toBe(cronIntervalPresets.everyMonth);
+ expect(this.intervalPatternComponent.$el.querySelector('.cron-interval-input').value).toBe(cronIntervalPresets.everyMonth);
+ done();
+ });
+ });
+
+ it('only a space is added to cronInterval (trimmed later) when custom radio is selected', function (done) {
+ this.intervalPatternComponent.$el.querySelector('#every-month').click();
+ this.intervalPatternComponent.$el.querySelector('#custom').click();
+
+ Vue.nextTick(() => {
+ const intervalWithSpaceAppended = `${cronIntervalPresets.everyMonth} `;
+ expect(this.intervalPatternComponent.cronInterval).toBe(intervalWithSpaceAppended);
+ expect(this.intervalPatternComponent.$el.querySelector('.cron-interval-input').value).toBe(intervalWithSpaceAppended);
+ done();
+ });
+ });
+
+ it('text input is disabled when preset interval is selected', function (done) {
+ this.intervalPatternComponent.$el.querySelector('#every-month').click();
+
+ Vue.nextTick(() => {
+ expect(this.intervalPatternComponent.isEditable).toBe(false);
+ expect(this.intervalPatternComponent.$el.querySelector('.cron-interval-input').disabled).toBe(true);
+ done();
+ });
+ });
+
+ it('text input is enabled when custom is selected', function (done) {
+ this.intervalPatternComponent.$el.querySelector('#every-month').click();
+ this.intervalPatternComponent.$el.querySelector('#custom').click();
+
+ Vue.nextTick(() => {
+ expect(this.intervalPatternComponent.isEditable).toBe(true);
+ expect(this.intervalPatternComponent.$el.querySelector('.cron-interval-input').disabled).toBe(false);
+ done();
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/pipeline_schedules/pipeline_schedule_callout_spec.js b/spec/javascripts/pipeline_schedules/pipeline_schedule_callout_spec.js
new file mode 100644
index 00000000000..6120d224ac0
--- /dev/null
+++ b/spec/javascripts/pipeline_schedules/pipeline_schedule_callout_spec.js
@@ -0,0 +1,106 @@
+import Vue from 'vue';
+import Cookies from 'js-cookie';
+import PipelineSchedulesCallout from '~/pipeline_schedules/components/pipeline_schedules_callout';
+
+const PipelineSchedulesCalloutComponent = Vue.extend(PipelineSchedulesCallout);
+const cookieKey = 'pipeline_schedules_callout_dismissed';
+const docsUrl = 'help/ci/scheduled_pipelines';
+
+describe('Pipeline Schedule Callout', () => {
+ beforeEach(() => {
+ setFixtures(`
+ <div id='pipeline-schedules-callout' data-docs-url=${docsUrl}></div>
+ `);
+ });
+
+ describe('independent of cookies', () => {
+ beforeEach(() => {
+ this.calloutComponent = new PipelineSchedulesCalloutComponent().$mount();
+ });
+
+ it('the component can be initialized', () => {
+ expect(this.calloutComponent).toBeDefined();
+ });
+
+ it('correctly sets illustrationSvg', () => {
+ expect(this.calloutComponent.illustrationSvg).toContain('<svg');
+ });
+
+ it('correctly sets docsUrl', () => {
+ expect(this.calloutComponent.docsUrl).toContain(docsUrl);
+ });
+ });
+
+ describe(`when ${cookieKey} cookie is set`, () => {
+ beforeEach(() => {
+ Cookies.set(cookieKey, true);
+ this.calloutComponent = new PipelineSchedulesCalloutComponent().$mount();
+ });
+
+ it('correctly sets calloutDismissed to true', () => {
+ expect(this.calloutComponent.calloutDismissed).toBe(true);
+ });
+
+ it('does not render the callout', () => {
+ expect(this.calloutComponent.$el.childNodes.length).toBe(0);
+ });
+ });
+
+ describe('when cookie is not set', () => {
+ beforeEach(() => {
+ Cookies.remove(cookieKey);
+ this.calloutComponent = new PipelineSchedulesCalloutComponent().$mount();
+ });
+
+ it('correctly sets calloutDismissed to false', () => {
+ expect(this.calloutComponent.calloutDismissed).toBe(false);
+ });
+
+ it('renders the callout container', () => {
+ expect(this.calloutComponent.$el.querySelector('.bordered-box')).not.toBeNull();
+ });
+
+ it('renders the callout svg', () => {
+ expect(this.calloutComponent.$el.outerHTML).toContain('<svg');
+ });
+
+ it('renders the callout title', () => {
+ expect(this.calloutComponent.$el.outerHTML).toContain('Scheduling Pipelines');
+ });
+
+ it('renders the callout text', () => {
+ expect(this.calloutComponent.$el.outerHTML).toContain('runs pipelines in the future');
+ });
+
+ it('renders the documentation url', () => {
+ expect(this.calloutComponent.$el.outerHTML).toContain(docsUrl);
+ });
+
+ it('updates calloutDismissed when close button is clicked', (done) => {
+ this.calloutComponent.$el.querySelector('#dismiss-callout-btn').click();
+
+ Vue.nextTick(() => {
+ expect(this.calloutComponent.calloutDismissed).toBe(true);
+ done();
+ });
+ });
+
+ it('#dismissCallout updates calloutDismissed', (done) => {
+ this.calloutComponent.dismissCallout();
+
+ Vue.nextTick(() => {
+ expect(this.calloutComponent.calloutDismissed).toBe(true);
+ done();
+ });
+ });
+
+ it('is hidden when close button is clicked', (done) => {
+ this.calloutComponent.$el.querySelector('#dismiss-callout-btn').click();
+
+ Vue.nextTick(() => {
+ expect(this.calloutComponent.$el.childNodes.length).toBe(0);
+ done();
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/pipelines/graph/action_component_spec.js b/spec/javascripts/pipelines/graph/action_component_spec.js
new file mode 100644
index 00000000000..f033956c071
--- /dev/null
+++ b/spec/javascripts/pipelines/graph/action_component_spec.js
@@ -0,0 +1,40 @@
+import Vue from 'vue';
+import actionComponent from '~/pipelines/components/graph/action_component.vue';
+
+describe('pipeline graph action component', () => {
+ let component;
+
+ beforeEach(() => {
+ const ActionComponent = Vue.extend(actionComponent);
+ component = new ActionComponent({
+ propsData: {
+ tooltipText: 'bar',
+ link: 'foo',
+ actionMethod: 'post',
+ actionIcon: 'icon_action_cancel',
+ },
+ }).$mount();
+ });
+
+ it('should render a link', () => {
+ expect(component.$el.getAttribute('href')).toEqual('foo');
+ });
+
+ it('should render the provided title as a bootstrap tooltip', () => {
+ expect(component.$el.getAttribute('data-original-title')).toEqual('bar');
+ });
+
+ it('should update bootstrap tooltip when title changes', (done) => {
+ component.tooltipText = 'changed';
+
+ Vue.nextTick(() => {
+ expect(component.$el.getAttribute('data-original-title')).toBe('changed');
+ done();
+ });
+ });
+
+ it('should render an svg', () => {
+ expect(component.$el.querySelector('.ci-action-icon-wrapper')).toBeDefined();
+ expect(component.$el.querySelector('svg')).toBeDefined();
+ });
+});
diff --git a/spec/javascripts/pipelines/graph/dropdown_action_component_spec.js b/spec/javascripts/pipelines/graph/dropdown_action_component_spec.js
new file mode 100644
index 00000000000..14ff1b0d25c
--- /dev/null
+++ b/spec/javascripts/pipelines/graph/dropdown_action_component_spec.js
@@ -0,0 +1,30 @@
+import Vue from 'vue';
+import dropdownActionComponent from '~/pipelines/components/graph/dropdown_action_component.vue';
+
+describe('action component', () => {
+ let component;
+
+ beforeEach(() => {
+ const DropdownActionComponent = Vue.extend(dropdownActionComponent);
+ component = new DropdownActionComponent({
+ propsData: {
+ tooltipText: 'bar',
+ link: 'foo',
+ actionMethod: 'post',
+ actionIcon: 'icon_action_cancel',
+ },
+ }).$mount();
+ });
+
+ it('should render a link', () => {
+ expect(component.$el.getAttribute('href')).toEqual('foo');
+ });
+
+ it('should render the provided title as a bootstrap tooltip', () => {
+ expect(component.$el.getAttribute('data-original-title')).toEqual('bar');
+ });
+
+ it('should render an svg', () => {
+ expect(component.$el.querySelector('svg')).toBeDefined();
+ });
+});
diff --git a/spec/javascripts/pipelines/graph/graph_component_spec.js b/spec/javascripts/pipelines/graph/graph_component_spec.js
new file mode 100644
index 00000000000..6bd0eb86263
--- /dev/null
+++ b/spec/javascripts/pipelines/graph/graph_component_spec.js
@@ -0,0 +1,62 @@
+import Vue from 'vue';
+import graphComponent from '~/pipelines/components/graph/graph_component.vue';
+import graphJSON from './mock_data';
+
+describe('graph component', () => {
+ preloadFixtures('static/graph.html.raw');
+
+ let GraphComponent;
+
+ beforeEach(() => {
+ loadFixtures('static/graph.html.raw');
+ GraphComponent = Vue.extend(graphComponent);
+ });
+
+ describe('while is loading', () => {
+ it('should render a loading icon', () => {
+ const component = new GraphComponent().$mount('#js-pipeline-graph-vue');
+ expect(component.$el.querySelector('.loading-icon')).toBeDefined();
+ });
+ });
+
+ describe('with a successfull response', () => {
+ const interceptor = (request, next) => {
+ next(request.respondWith(JSON.stringify(graphJSON), {
+ status: 200,
+ }));
+ };
+
+ beforeEach(() => {
+ Vue.http.interceptors.push(interceptor);
+ });
+
+ afterEach(() => {
+ Vue.http.interceptors = _.without(Vue.http.interceptors, interceptor);
+ });
+
+ it('should render the graph', (done) => {
+ const component = new GraphComponent().$mount('#js-pipeline-graph-vue');
+
+ setTimeout(() => {
+ expect(component.$el.classList.contains('js-pipeline-graph')).toEqual(true);
+
+ expect(
+ component.$el.querySelector('.stage-column:first-child').classList.contains('no-margin'),
+ ).toEqual(true);
+
+ expect(
+ component.$el.querySelector('.stage-column:nth-child(2)').classList.contains('left-margin'),
+ ).toEqual(true);
+
+ expect(
+ component.$el.querySelector('.stage-column:nth-child(2) .build:nth-child(1)').classList.contains('left-connector'),
+ ).toEqual(true);
+
+ expect(component.$el.querySelector('loading-icon')).toBe(null);
+
+ expect(component.$el.querySelector('.stage-column-list')).toBeDefined();
+ done();
+ }, 0);
+ });
+ });
+});
diff --git a/spec/javascripts/pipelines/graph/job_component_spec.js b/spec/javascripts/pipelines/graph/job_component_spec.js
new file mode 100644
index 00000000000..63986b6c0db
--- /dev/null
+++ b/spec/javascripts/pipelines/graph/job_component_spec.js
@@ -0,0 +1,117 @@
+import Vue from 'vue';
+import jobComponent from '~/pipelines/components/graph/job_component.vue';
+
+describe('pipeline graph job component', () => {
+ let JobComponent;
+
+ const mockJob = {
+ id: 4256,
+ name: 'test',
+ status: {
+ icon: 'icon_status_success',
+ text: 'passed',
+ label: 'passed',
+ group: 'success',
+ details_path: '/root/ci-mock/builds/4256',
+ action: {
+ icon: 'icon_action_retry',
+ title: 'Retry',
+ path: '/root/ci-mock/builds/4256/retry',
+ method: 'post',
+ },
+ },
+ };
+
+ beforeEach(() => {
+ JobComponent = Vue.extend(jobComponent);
+ });
+
+ describe('name with link', () => {
+ it('should render the job name and status with a link', () => {
+ const component = new JobComponent({
+ propsData: {
+ job: mockJob,
+ },
+ }).$mount();
+
+ const link = component.$el.querySelector('a');
+
+ expect(link.getAttribute('href')).toEqual(mockJob.status.details_path);
+
+ expect(
+ link.getAttribute('data-original-title'),
+ ).toEqual(`${mockJob.name} - ${mockJob.status.label}`);
+
+ expect(component.$el.querySelector('.js-status-icon-success')).toBeDefined();
+
+ expect(
+ component.$el.querySelector('.ci-status-text').textContent.trim(),
+ ).toEqual(mockJob.name);
+ });
+ });
+
+ describe('name without link', () => {
+ it('it should render status and name', () => {
+ const component = new JobComponent({
+ propsData: {
+ job: {
+ id: 4256,
+ name: 'test',
+ status: {
+ icon: 'icon_status_success',
+ text: 'passed',
+ label: 'passed',
+ group: 'success',
+ details_path: '/root/ci-mock/builds/4256',
+ },
+ },
+ },
+ }).$mount();
+
+ expect(component.$el.querySelector('.js-status-icon-success')).toBeDefined();
+
+ expect(
+ component.$el.querySelector('.ci-status-text').textContent.trim(),
+ ).toEqual(mockJob.name);
+ });
+ });
+
+ describe('action icon', () => {
+ it('it should render the action icon', () => {
+ const component = new JobComponent({
+ propsData: {
+ job: mockJob,
+ },
+ }).$mount();
+
+ expect(component.$el.querySelector('a.ci-action-icon-container')).toBeDefined();
+ expect(component.$el.querySelector('i.ci-action-icon-wrapper')).toBeDefined();
+ });
+ });
+
+ describe('dropdown', () => {
+ it('should render the dropdown action icon', () => {
+ const component = new JobComponent({
+ propsData: {
+ job: mockJob,
+ isDropdown: true,
+ },
+ }).$mount();
+
+ expect(component.$el.querySelector('a.ci-action-icon-wrapper')).toBeDefined();
+ });
+ });
+
+ it('should render provided class name', () => {
+ const component = new JobComponent({
+ propsData: {
+ job: mockJob,
+ cssClassJobName: 'css-class-job-name',
+ },
+ }).$mount();
+
+ expect(
+ component.$el.querySelector('a').classList.contains('css-class-job-name'),
+ ).toBe(true);
+ });
+});
diff --git a/spec/javascripts/pipelines/graph/job_name_component_spec.js b/spec/javascripts/pipelines/graph/job_name_component_spec.js
new file mode 100644
index 00000000000..8e2071ba0b3
--- /dev/null
+++ b/spec/javascripts/pipelines/graph/job_name_component_spec.js
@@ -0,0 +1,27 @@
+import Vue from 'vue';
+import jobNameComponent from '~/pipelines/components/graph/job_name_component.vue';
+
+describe('job name component', () => {
+ let component;
+
+ beforeEach(() => {
+ const JobNameComponent = Vue.extend(jobNameComponent);
+ component = new JobNameComponent({
+ propsData: {
+ name: 'foo',
+ status: {
+ icon: 'icon_status_success',
+ },
+ },
+ }).$mount();
+ });
+
+ it('should render the provided name', () => {
+ expect(component.$el.querySelector('.ci-status-text').textContent.trim()).toEqual('foo');
+ });
+
+ it('should render an icon with the provided status', () => {
+ expect(component.$el.querySelector('.ci-status-icon-success')).toBeDefined();
+ expect(component.$el.querySelector('.ci-status-icon-success svg')).toBeDefined();
+ });
+});
diff --git a/spec/javascripts/pipelines/graph/mock_data.js b/spec/javascripts/pipelines/graph/mock_data.js
new file mode 100644
index 00000000000..56c522b7f77
--- /dev/null
+++ b/spec/javascripts/pipelines/graph/mock_data.js
@@ -0,0 +1,232 @@
+/* eslint-disable quote-props, quotes, comma-dangle */
+export default {
+ "id": 123,
+ "user": {
+ "name": "Root",
+ "username": "root",
+ "id": 1,
+ "state": "active",
+ "avatar_url": null,
+ "web_url": "http://localhost:3000/root"
+ },
+ "active": false,
+ "coverage": null,
+ "path": "/root/ci-mock/pipelines/123",
+ "details": {
+ "status": {
+ "icon": "icon_status_success",
+ "text": "passed",
+ "label": "passed",
+ "group": "success",
+ "has_details": true,
+ "details_path": "/root/ci-mock/pipelines/123",
+ "favicon": "/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico"
+ },
+ "duration": 9,
+ "finished_at": "2017-04-19T14:30:27.542Z",
+ "stages": [{
+ "name": "test",
+ "title": "test: passed",
+ "groups": [{
+ "name": "test",
+ "size": 1,
+ "status": {
+ "icon": "icon_status_success",
+ "text": "passed",
+ "label": "passed",
+ "group": "success",
+ "has_details": true,
+ "details_path": "/root/ci-mock/builds/4153",
+ "favicon": "/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico",
+ "action": {
+ "icon": "icon_action_retry",
+ "title": "Retry",
+ "path": "/root/ci-mock/builds/4153/retry",
+ "method": "post"
+ }
+ },
+ "jobs": [{
+ "id": 4153,
+ "name": "test",
+ "build_path": "/root/ci-mock/builds/4153",
+ "retry_path": "/root/ci-mock/builds/4153/retry",
+ "playable": false,
+ "created_at": "2017-04-13T09:25:18.959Z",
+ "updated_at": "2017-04-13T09:25:23.118Z",
+ "status": {
+ "icon": "icon_status_success",
+ "text": "passed",
+ "label": "passed",
+ "group": "success",
+ "has_details": true,
+ "details_path": "/root/ci-mock/builds/4153",
+ "favicon": "/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico",
+ "action": {
+ "icon": "icon_action_retry",
+ "title": "Retry",
+ "path": "/root/ci-mock/builds/4153/retry",
+ "method": "post"
+ }
+ }
+ }]
+ }],
+ "status": {
+ "icon": "icon_status_success",
+ "text": "passed",
+ "label": "passed",
+ "group": "success",
+ "has_details": true,
+ "details_path": "/root/ci-mock/pipelines/123#test",
+ "favicon": "/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico"
+ },
+ "path": "/root/ci-mock/pipelines/123#test",
+ "dropdown_path": "/root/ci-mock/pipelines/123/stage.json?stage=test"
+ }, {
+ "name": "deploy",
+ "title": "deploy: passed",
+ "groups": [{
+ "name": "deploy to production",
+ "size": 1,
+ "status": {
+ "icon": "icon_status_success",
+ "text": "passed",
+ "label": "passed",
+ "group": "success",
+ "has_details": true,
+ "details_path": "/root/ci-mock/builds/4166",
+ "favicon": "/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico",
+ "action": {
+ "icon": "icon_action_retry",
+ "title": "Retry",
+ "path": "/root/ci-mock/builds/4166/retry",
+ "method": "post"
+ }
+ },
+ "jobs": [{
+ "id": 4166,
+ "name": "deploy to production",
+ "build_path": "/root/ci-mock/builds/4166",
+ "retry_path": "/root/ci-mock/builds/4166/retry",
+ "playable": false,
+ "created_at": "2017-04-19T14:29:46.463Z",
+ "updated_at": "2017-04-19T14:30:27.498Z",
+ "status": {
+ "icon": "icon_status_success",
+ "text": "passed",
+ "label": "passed",
+ "group": "success",
+ "has_details": true,
+ "details_path": "/root/ci-mock/builds/4166",
+ "favicon": "/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico",
+ "action": {
+ "icon": "icon_action_retry",
+ "title": "Retry",
+ "path": "/root/ci-mock/builds/4166/retry",
+ "method": "post"
+ }
+ }
+ }]
+ }, {
+ "name": "deploy to staging",
+ "size": 1,
+ "status": {
+ "icon": "icon_status_success",
+ "text": "passed",
+ "label": "passed",
+ "group": "success",
+ "has_details": true,
+ "details_path": "/root/ci-mock/builds/4159",
+ "favicon": "/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico",
+ "action": {
+ "icon": "icon_action_retry",
+ "title": "Retry",
+ "path": "/root/ci-mock/builds/4159/retry",
+ "method": "post"
+ }
+ },
+ "jobs": [{
+ "id": 4159,
+ "name": "deploy to staging",
+ "build_path": "/root/ci-mock/builds/4159",
+ "retry_path": "/root/ci-mock/builds/4159/retry",
+ "playable": false,
+ "created_at": "2017-04-18T16:32:08.420Z",
+ "updated_at": "2017-04-18T16:32:12.631Z",
+ "status": {
+ "icon": "icon_status_success",
+ "text": "passed",
+ "label": "passed",
+ "group": "success",
+ "has_details": true,
+ "details_path": "/root/ci-mock/builds/4159",
+ "favicon": "/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico",
+ "action": {
+ "icon": "icon_action_retry",
+ "title": "Retry",
+ "path": "/root/ci-mock/builds/4159/retry",
+ "method": "post"
+ }
+ }
+ }]
+ }],
+ "status": {
+ "icon": "icon_status_success",
+ "text": "passed",
+ "label": "passed",
+ "group": "success",
+ "has_details": true,
+ "details_path": "/root/ci-mock/pipelines/123#deploy",
+ "favicon": "/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico"
+ },
+ "path": "/root/ci-mock/pipelines/123#deploy",
+ "dropdown_path": "/root/ci-mock/pipelines/123/stage.json?stage=deploy"
+ }],
+ "artifacts": [],
+ "manual_actions": [{
+ "name": "deploy to production",
+ "path": "/root/ci-mock/builds/4166/play",
+ "playable": false
+ }]
+ },
+ "flags": {
+ "latest": true,
+ "triggered": false,
+ "stuck": false,
+ "yaml_errors": false,
+ "retryable": false,
+ "cancelable": false
+ },
+ "ref": {
+ "name": "master",
+ "path": "/root/ci-mock/tree/master",
+ "tag": false,
+ "branch": true
+ },
+ "commit": {
+ "id": "798e5f902592192afaba73f4668ae30e56eae492",
+ "short_id": "798e5f90",
+ "title": "Merge branch 'new-branch' into 'master'\r",
+ "created_at": "2017-04-13T10:25:17.000+01:00",
+ "parent_ids": ["54d483b1ed156fbbf618886ddf7ab023e24f8738", "c8e2d38a6c538822e81c57022a6e3a0cfedebbcc"],
+ "message": "Merge branch 'new-branch' into 'master'\r\n\r\nAdd new file\r\n\r\nSee merge request !1",
+ "author_name": "Root",
+ "author_email": "admin@example.com",
+ "authored_date": "2017-04-13T10:25:17.000+01:00",
+ "committer_name": "Root",
+ "committer_email": "admin@example.com",
+ "committed_date": "2017-04-13T10:25:17.000+01:00",
+ "author": {
+ "name": "Root",
+ "username": "root",
+ "id": 1,
+ "state": "active",
+ "avatar_url": null,
+ "web_url": "http://localhost:3000/root"
+ },
+ "author_gravatar_url": null,
+ "commit_url": "http://localhost:3000/root/ci-mock/commit/798e5f902592192afaba73f4668ae30e56eae492",
+ "commit_path": "/root/ci-mock/commit/798e5f902592192afaba73f4668ae30e56eae492"
+ },
+ "created_at": "2017-04-13T09:25:18.881Z",
+ "updated_at": "2017-04-19T14:30:27.561Z"
+};
diff --git a/spec/javascripts/pipelines/graph/stage_column_component_spec.js b/spec/javascripts/pipelines/graph/stage_column_component_spec.js
new file mode 100644
index 00000000000..aa4d6eedaf4
--- /dev/null
+++ b/spec/javascripts/pipelines/graph/stage_column_component_spec.js
@@ -0,0 +1,42 @@
+import Vue from 'vue';
+import stageColumnComponent from '~/pipelines/components/graph/stage_column_component.vue';
+
+describe('stage column component', () => {
+ let component;
+ const mockJob = {
+ id: 4256,
+ name: 'test',
+ status: {
+ icon: 'icon_status_success',
+ text: 'passed',
+ label: 'passed',
+ group: 'success',
+ details_path: '/root/ci-mock/builds/4256',
+ action: {
+ icon: 'icon_action_retry',
+ title: 'Retry',
+ path: '/root/ci-mock/builds/4256/retry',
+ method: 'post',
+ },
+ },
+ };
+
+ beforeEach(() => {
+ const StageColumnComponent = Vue.extend(stageColumnComponent);
+
+ component = new StageColumnComponent({
+ propsData: {
+ title: 'foo',
+ jobs: [mockJob, mockJob, mockJob],
+ },
+ }).$mount();
+ });
+
+ it('should render provided title', () => {
+ expect(component.$el.querySelector('.stage-name').textContent.trim()).toEqual('foo');
+ });
+
+ it('should render the provided jobs', () => {
+ expect(component.$el.querySelectorAll('.builds-container > ul > li').length).toEqual(3);
+ });
+});
diff --git a/spec/javascripts/pipelines/mock_data.js b/spec/javascripts/pipelines/mock_data.js
deleted file mode 100644
index 2365a662b9f..00000000000
--- a/spec/javascripts/pipelines/mock_data.js
+++ /dev/null
@@ -1,107 +0,0 @@
-export default {
- pipelines: [{
- id: 115,
- user: {
- name: 'Root',
- username: 'root',
- id: 1,
- state: 'active',
- avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
- web_url: 'http://localhost:3000/root',
- },
- path: '/root/review-app/pipelines/115',
- details: {
- status: {
- icon: 'icon_status_failed',
- text: 'failed',
- label: 'failed',
- group: 'failed',
- has_details: true,
- details_path: '/root/review-app/pipelines/115',
- },
- duration: null,
- finished_at: '2017-03-17T19:00:15.996Z',
- stages: [{
- name: 'build',
- title: 'build: failed',
- status: {
- icon: 'icon_status_failed',
- text: 'failed',
- label: 'failed',
- group: 'failed',
- has_details: true,
- details_path: '/root/review-app/pipelines/115#build',
- },
- path: '/root/review-app/pipelines/115#build',
- dropdown_path: '/root/review-app/pipelines/115/stage.json?stage=build',
- },
- {
- name: 'review',
- title: 'review: skipped',
- status: {
- icon: 'icon_status_skipped',
- text: 'skipped',
- label: 'skipped',
- group: 'skipped',
- has_details: true,
- details_path: '/root/review-app/pipelines/115#review',
- },
- path: '/root/review-app/pipelines/115#review',
- dropdown_path: '/root/review-app/pipelines/115/stage.json?stage=review',
- }],
- artifacts: [],
- manual_actions: [{
- name: 'stop_review',
- path: '/root/review-app/builds/3766/play',
- }],
- },
- flags: {
- latest: true,
- triggered: false,
- stuck: false,
- yaml_errors: false,
- retryable: true,
- cancelable: false,
- },
- ref: {
- name: 'thisisabranch',
- path: '/root/review-app/tree/thisisabranch',
- tag: false,
- branch: true,
- },
- commit: {
- id: '9e87f87625b26c42c59a2ee0398f81d20cdfe600',
- short_id: '9e87f876',
- title: 'Update README.md',
- created_at: '2017-03-15T22:58:28.000+00:00',
- parent_ids: ['3744f9226e699faec2662a8b267e5d3fd0bfff0e'],
- message: 'Update README.md',
- author_name: 'Root',
- author_email: 'admin@example.com',
- authored_date: '2017-03-15T22:58:28.000+00:00',
- committer_name: 'Root',
- committer_email: 'admin@example.com',
- committed_date: '2017-03-15T22:58:28.000+00:00',
- author: {
- name: 'Root',
- username: 'root',
- id: 1,
- state: 'active',
- avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
- web_url: 'http://localhost:3000/root',
- },
- author_gravatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
- commit_url: 'http://localhost:3000/root/review-app/commit/9e87f87625b26c42c59a2ee0398f81d20cdfe600',
- commit_path: '/root/review-app/commit/9e87f87625b26c42c59a2ee0398f81d20cdfe600',
- },
- retry_path: '/root/review-app/pipelines/115/retry',
- created_at: '2017-03-15T22:58:33.436Z',
- updated_at: '2017-03-17T19:00:15.997Z',
- }],
- count: {
- all: 52,
- running: 0,
- pending: 0,
- finished: 52,
- },
-};
diff --git a/spec/javascripts/pipelines/pipelines_spec.js b/spec/javascripts/pipelines/pipelines_spec.js
index e9c05f74ce6..3a56156358b 100644
--- a/spec/javascripts/pipelines/pipelines_spec.js
+++ b/spec/javascripts/pipelines/pipelines_spec.js
@@ -1,15 +1,20 @@
import Vue from 'vue';
import pipelinesComp from '~/pipelines/pipelines';
import Store from '~/pipelines/stores/pipelines_store';
-import pipelinesData from './mock_data';
describe('Pipelines', () => {
+ const jsonFixtureName = 'pipelines/pipelines.json';
+
preloadFixtures('static/pipelines.html.raw');
+ preloadFixtures(jsonFixtureName);
let PipelinesComponent;
+ let pipeline;
beforeEach(() => {
loadFixtures('static/pipelines.html.raw');
+ const pipelines = getJSONFixture(jsonFixtureName).pipelines;
+ pipeline = pipelines.find(p => p.id === 1);
PipelinesComponent = Vue.extend(pipelinesComp);
});
@@ -17,7 +22,7 @@ describe('Pipelines', () => {
describe('successfull request', () => {
describe('with pipelines', () => {
const pipelinesInterceptor = (request, next) => {
- next(request.respondWith(JSON.stringify(pipelinesData), {
+ next(request.respondWith(JSON.stringify(pipeline), {
status: 200,
}));
};
diff --git a/spec/javascripts/pipelines/stage_spec.js b/spec/javascripts/pipelines/stage_spec.js
index 66b57a82363..a4f32a1faed 100644
--- a/spec/javascripts/pipelines/stage_spec.js
+++ b/spec/javascripts/pipelines/stage_spec.js
@@ -1,66 +1,86 @@
import Vue from 'vue';
-import { SUCCESS_SVG } from '~/ci_status_icons';
-import Stage from '~/pipelines/components/stage';
+import stage from '~/pipelines/components/stage.vue';
-function minify(string) {
- return string.replace(/\s/g, '');
-}
+describe('Pipelines stage component', () => {
+ let StageComponent;
+ let component;
-describe('Pipelines Stage', () => {
- describe('data', () => {
- let stageReturnValue;
+ beforeEach(() => {
+ StageComponent = Vue.extend(stage);
- beforeEach(() => {
- stageReturnValue = Stage.data();
- });
+ component = new StageComponent({
+ propsData: {
+ stage: {
+ status: {
+ group: 'success',
+ icon: 'icon_status_success',
+ title: 'success',
+ },
+ dropdown_path: 'foo',
+ },
+ updateDropdown: false,
+ },
+ }).$mount();
+ });
- it('should return object with .builds and .spinner', () => {
- expect(stageReturnValue).toEqual({
- builds: '',
- spinner: '<span class="fa fa-spinner fa-spin"></span>',
- });
- });
+ it('should render a dropdown with the status icon', () => {
+ expect(component.$el.getAttribute('class')).toEqual('dropdown');
+ expect(component.$el.querySelector('svg')).toBeDefined();
+ expect(component.$el.querySelector('button').getAttribute('data-toggle')).toEqual('dropdown');
});
- describe('computed', () => {
- describe('svgHTML', function () {
- let stage;
- let svgHTML;
+ describe('with successfull request', () => {
+ const interceptor = (request, next) => {
+ next(request.respondWith(JSON.stringify({ html: 'foo' }), {
+ status: 200,
+ }));
+ };
+
+ beforeEach(() => {
+ Vue.http.interceptors.push(interceptor);
+ });
- beforeEach(() => {
- stage = { stage: { status: { icon: 'icon_status_success' } } };
+ afterEach(() => {
+ Vue.http.interceptors = _.without(
+ Vue.http.interceptors, interceptor,
+ );
+ });
- svgHTML = Stage.computed.svgHTML.call(stage);
- });
+ it('should render the received data', (done) => {
+ component.$el.querySelector('button').click();
- it("should return the correct icon for the stage's status", () => {
- expect(svgHTML).toBe(SUCCESS_SVG);
- });
+ setTimeout(() => {
+ expect(
+ component.$el.querySelector('.js-builds-dropdown-container ul').textContent.trim(),
+ ).toEqual('foo');
+ done();
+ }, 0);
});
});
- describe('when mounted', () => {
- let StageComponent;
- let renderedComponent;
- let stage;
+ describe('when request fails', () => {
+ const interceptor = (request, next) => {
+ next(request.respondWith(JSON.stringify({}), {
+ status: 500,
+ }));
+ };
beforeEach(() => {
- stage = { status: { icon: 'icon_status_success' } };
-
- StageComponent = Vue.extend(Stage);
+ Vue.http.interceptors.push(interceptor);
+ });
- renderedComponent = new StageComponent({
- propsData: {
- stage,
- },
- }).$mount();
+ afterEach(() => {
+ Vue.http.interceptors = _.without(
+ Vue.http.interceptors, interceptor,
+ );
});
- it('should render the correct status svg', () => {
- const minifiedComponent = minify(renderedComponent.$el.outerHTML);
- const expectedSVG = minify(SUCCESS_SVG);
+ it('should close the dropdown', () => {
+ component.$el.click();
- expect(minifiedComponent).toContain(expectedSVG);
+ setTimeout(() => {
+ expect(component.$el.classList.contains('open')).toEqual(false);
+ }, 0);
});
});
});
diff --git a/spec/javascripts/pipelines/time_ago_spec.js b/spec/javascripts/pipelines/time_ago_spec.js
new file mode 100644
index 00000000000..24581e8c672
--- /dev/null
+++ b/spec/javascripts/pipelines/time_ago_spec.js
@@ -0,0 +1,64 @@
+import Vue from 'vue';
+import timeAgo from '~/pipelines/components/time_ago';
+
+describe('Timeago component', () => {
+ let TimeAgo;
+ beforeEach(() => {
+ TimeAgo = Vue.extend(timeAgo);
+ });
+
+ describe('with duration', () => {
+ it('should render duration and timer svg', () => {
+ const component = new TimeAgo({
+ propsData: {
+ duration: 10,
+ finishedTime: '',
+ },
+ }).$mount();
+
+ expect(component.$el.querySelector('.duration')).toBeDefined();
+ expect(component.$el.querySelector('.duration svg')).toBeDefined();
+ });
+ });
+
+ describe('without duration', () => {
+ it('should not render duration and timer svg', () => {
+ const component = new TimeAgo({
+ propsData: {
+ duration: 0,
+ finishedTime: '',
+ },
+ }).$mount();
+
+ expect(component.$el.querySelector('.duration')).toBe(null);
+ });
+ });
+
+ describe('with finishedTime', () => {
+ it('should render time and calendar icon', () => {
+ const component = new TimeAgo({
+ propsData: {
+ duration: 0,
+ finishedTime: '2017-04-26T12:40:23.277Z',
+ },
+ }).$mount();
+
+ expect(component.$el.querySelector('.finished-at')).toBeDefined();
+ expect(component.$el.querySelector('.finished-at i.fa-calendar')).toBeDefined();
+ expect(component.$el.querySelector('.finished-at time')).toBeDefined();
+ });
+ });
+
+ describe('without finishedTime', () => {
+ it('should not render time and calendar icon', () => {
+ const component = new TimeAgo({
+ propsData: {
+ duration: 0,
+ finishedTime: '',
+ },
+ }).$mount();
+
+ expect(component.$el.querySelector('.finished-at')).toBe(null);
+ });
+ });
+});
diff --git a/spec/javascripts/pipelines_spec.js b/spec/javascripts/pipelines_spec.js
index 72770a702d3..81ac589f4e6 100644
--- a/spec/javascripts/pipelines_spec.js
+++ b/spec/javascripts/pipelines_spec.js
@@ -1,30 +1,22 @@
-require('~/pipelines');
+import Pipelines from '~/pipelines';
// Fix for phantomJS
if (!Element.prototype.matches && Element.prototype.webkitMatchesSelector) {
Element.prototype.matches = Element.prototype.webkitMatchesSelector;
}
-(() => {
- describe('Pipelines', () => {
- preloadFixtures('static/pipeline_graph.html.raw');
+describe('Pipelines', () => {
+ preloadFixtures('static/pipeline_graph.html.raw');
- beforeEach(() => {
- loadFixtures('static/pipeline_graph.html.raw');
- });
-
- it('should be defined', () => {
- expect(window.gl.Pipelines).toBeDefined();
- });
-
- it('should create a `Pipelines` instance without options', () => {
- expect(() => { new window.gl.Pipelines(); }).not.toThrow(); //eslint-disable-line
- });
+ beforeEach(() => {
+ loadFixtures('static/pipeline_graph.html.raw');
+ });
- it('should create a `Pipelines` instance with options', () => {
- const pipelines = new window.gl.Pipelines({ foo: 'bar' });
+ it('should be defined', () => {
+ expect(Pipelines).toBeDefined();
+ });
- expect(pipelines.pipelineGraph).toBeDefined();
- });
+ it('should create a `Pipelines` instance without options', () => {
+ expect(() => { new Pipelines(); }).not.toThrow(); //eslint-disable-line
});
-})();
+});
diff --git a/spec/javascripts/project_title_spec.js b/spec/javascripts/project_title_spec.js
index 3a1d4e2440f..5c51e855401 100644
--- a/spec/javascripts/project_title_spec.js
+++ b/spec/javascripts/project_title_spec.js
@@ -2,7 +2,6 @@
/* global Project */
require('select2/select2.js');
-require('~/lib/utils/type_utility');
require('~/gl_dropdown');
require('~/api');
require('~/project_select');
diff --git a/spec/javascripts/raven/index_spec.js b/spec/javascripts/raven/index_spec.js
new file mode 100644
index 00000000000..b5662cd0331
--- /dev/null
+++ b/spec/javascripts/raven/index_spec.js
@@ -0,0 +1,42 @@
+import RavenConfig from '~/raven/raven_config';
+import index from '~/raven/index';
+
+describe('RavenConfig options', () => {
+ let sentryDsn;
+ let currentUserId;
+ let gitlabUrl;
+ let isProduction;
+ let indexReturnValue;
+
+ beforeEach(() => {
+ sentryDsn = 'sentryDsn';
+ currentUserId = 'currentUserId';
+ gitlabUrl = 'gitlabUrl';
+ isProduction = 'isProduction';
+
+ window.gon = {
+ sentry_dsn: sentryDsn,
+ current_user_id: currentUserId,
+ gitlab_url: gitlabUrl,
+ };
+
+ process.env.NODE_ENV = isProduction;
+
+ spyOn(RavenConfig, 'init');
+
+ indexReturnValue = index();
+ });
+
+ it('should init with .sentryDsn, .currentUserId, .whitelistUrls and .isProduction', () => {
+ expect(RavenConfig.init).toHaveBeenCalledWith({
+ sentryDsn,
+ currentUserId,
+ whitelistUrls: [gitlabUrl],
+ isProduction,
+ });
+ });
+
+ it('should return RavenConfig', () => {
+ expect(indexReturnValue).toBe(RavenConfig);
+ });
+});
diff --git a/spec/javascripts/raven/raven_config_spec.js b/spec/javascripts/raven/raven_config_spec.js
new file mode 100644
index 00000000000..a2d720760fc
--- /dev/null
+++ b/spec/javascripts/raven/raven_config_spec.js
@@ -0,0 +1,276 @@
+import Raven from 'raven-js';
+import RavenConfig from '~/raven/raven_config';
+
+describe('RavenConfig', () => {
+ describe('IGNORE_ERRORS', () => {
+ it('should be an array of strings', () => {
+ const areStrings = RavenConfig.IGNORE_ERRORS.every(error => typeof error === 'string');
+
+ expect(areStrings).toBe(true);
+ });
+ });
+
+ describe('IGNORE_URLS', () => {
+ it('should be an array of regexps', () => {
+ const areRegExps = RavenConfig.IGNORE_URLS.every(url => url instanceof RegExp);
+
+ expect(areRegExps).toBe(true);
+ });
+ });
+
+ describe('SAMPLE_RATE', () => {
+ it('should be a finite number', () => {
+ expect(typeof RavenConfig.SAMPLE_RATE).toEqual('number');
+ });
+ });
+
+ describe('init', () => {
+ let options;
+
+ beforeEach(() => {
+ options = {
+ sentryDsn: '//sentryDsn',
+ ravenAssetUrl: '//ravenAssetUrl',
+ currentUserId: 1,
+ whitelistUrls: ['//gitlabUrl'],
+ isProduction: true,
+ };
+
+ spyOn(RavenConfig, 'configure');
+ spyOn(RavenConfig, 'bindRavenErrors');
+ spyOn(RavenConfig, 'setUser');
+
+ RavenConfig.init(options);
+ });
+
+ it('should set the options property', () => {
+ expect(RavenConfig.options).toEqual(options);
+ });
+
+ it('should call the configure method', () => {
+ expect(RavenConfig.configure).toHaveBeenCalled();
+ });
+
+ it('should call the error bindings method', () => {
+ expect(RavenConfig.bindRavenErrors).toHaveBeenCalled();
+ });
+
+ it('should call setUser', () => {
+ expect(RavenConfig.setUser).toHaveBeenCalled();
+ });
+
+ it('should not call setUser if there is no current user ID', () => {
+ RavenConfig.setUser.calls.reset();
+
+ RavenConfig.init({
+ sentryDsn: '//sentryDsn',
+ ravenAssetUrl: '//ravenAssetUrl',
+ currentUserId: undefined,
+ whitelistUrls: ['//gitlabUrl'],
+ isProduction: true,
+ });
+
+ expect(RavenConfig.setUser).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('configure', () => {
+ let options;
+ let raven;
+ let ravenConfig;
+
+ beforeEach(() => {
+ options = {
+ sentryDsn: '//sentryDsn',
+ whitelistUrls: ['//gitlabUrl'],
+ isProduction: true,
+ };
+
+ ravenConfig = jasmine.createSpyObj('ravenConfig', ['shouldSendSample']);
+ raven = jasmine.createSpyObj('raven', ['install']);
+
+ spyOn(Raven, 'config').and.returnValue(raven);
+
+ ravenConfig.options = options;
+ ravenConfig.IGNORE_ERRORS = 'ignore_errors';
+ ravenConfig.IGNORE_URLS = 'ignore_urls';
+
+ RavenConfig.configure.call(ravenConfig);
+ });
+
+ it('should call Raven.config', () => {
+ expect(Raven.config).toHaveBeenCalledWith(options.sentryDsn, {
+ whitelistUrls: options.whitelistUrls,
+ environment: 'production',
+ ignoreErrors: ravenConfig.IGNORE_ERRORS,
+ ignoreUrls: ravenConfig.IGNORE_URLS,
+ shouldSendCallback: jasmine.any(Function),
+ });
+ });
+
+ it('should call Raven.install', () => {
+ expect(raven.install).toHaveBeenCalled();
+ });
+
+ it('should set .environment to development if isProduction is false', () => {
+ ravenConfig.options.isProduction = false;
+
+ RavenConfig.configure.call(ravenConfig);
+
+ expect(Raven.config).toHaveBeenCalledWith(options.sentryDsn, {
+ whitelistUrls: options.whitelistUrls,
+ environment: 'development',
+ ignoreErrors: ravenConfig.IGNORE_ERRORS,
+ ignoreUrls: ravenConfig.IGNORE_URLS,
+ shouldSendCallback: jasmine.any(Function),
+ });
+ });
+ });
+
+ describe('setUser', () => {
+ let ravenConfig;
+
+ beforeEach(() => {
+ ravenConfig = { options: { currentUserId: 1 } };
+ spyOn(Raven, 'setUserContext');
+
+ RavenConfig.setUser.call(ravenConfig);
+ });
+
+ it('should call .setUserContext', function () {
+ expect(Raven.setUserContext).toHaveBeenCalledWith({
+ id: ravenConfig.options.currentUserId,
+ });
+ });
+ });
+
+ describe('bindRavenErrors', () => {
+ let $document;
+ let $;
+
+ beforeEach(() => {
+ $document = jasmine.createSpyObj('$document', ['on']);
+ $ = jasmine.createSpy('$').and.returnValue($document);
+
+ window.$ = $;
+
+ RavenConfig.bindRavenErrors();
+ });
+
+ it('should call .on', function () {
+ expect($document.on).toHaveBeenCalledWith('ajaxError.raven', RavenConfig.handleRavenErrors);
+ });
+ });
+
+ describe('handleRavenErrors', () => {
+ let event;
+ let req;
+ let config;
+ let err;
+
+ beforeEach(() => {
+ event = {};
+ req = { status: 'status', responseText: 'responseText', statusText: 'statusText' };
+ config = { type: 'type', url: 'url', data: 'data' };
+ err = {};
+
+ spyOn(Raven, 'captureMessage');
+
+ RavenConfig.handleRavenErrors(event, req, config, err);
+ });
+
+ it('should call Raven.captureMessage', () => {
+ expect(Raven.captureMessage).toHaveBeenCalledWith(err, {
+ extra: {
+ type: config.type,
+ url: config.url,
+ data: config.data,
+ status: req.status,
+ response: req.responseText,
+ error: err,
+ event,
+ },
+ });
+ });
+
+ describe('if no err is provided', () => {
+ beforeEach(() => {
+ Raven.captureMessage.calls.reset();
+
+ RavenConfig.handleRavenErrors(event, req, config);
+ });
+
+ it('should use req.statusText as the error value', () => {
+ expect(Raven.captureMessage).toHaveBeenCalledWith(req.statusText, {
+ extra: {
+ type: config.type,
+ url: config.url,
+ data: config.data,
+ status: req.status,
+ response: req.responseText,
+ error: req.statusText,
+ event,
+ },
+ });
+ });
+ });
+
+ describe('if no req.responseText is provided', () => {
+ beforeEach(() => {
+ req.responseText = undefined;
+
+ Raven.captureMessage.calls.reset();
+
+ RavenConfig.handleRavenErrors(event, req, config, err);
+ });
+
+ it('should use `Unknown response text` as the response', () => {
+ expect(Raven.captureMessage).toHaveBeenCalledWith(err, {
+ extra: {
+ type: config.type,
+ url: config.url,
+ data: config.data,
+ status: req.status,
+ response: 'Unknown response text',
+ error: err,
+ event,
+ },
+ });
+ });
+ });
+ });
+
+ describe('shouldSendSample', () => {
+ let randomNumber;
+
+ beforeEach(() => {
+ RavenConfig.SAMPLE_RATE = 50;
+
+ spyOn(Math, 'random').and.callFake(() => randomNumber);
+ });
+
+ it('should call Math.random', () => {
+ RavenConfig.shouldSendSample();
+
+ expect(Math.random).toHaveBeenCalled();
+ });
+
+ it('should return true if the sample rate is greater than the random number * 100', () => {
+ randomNumber = 0.1;
+
+ expect(RavenConfig.shouldSendSample()).toBe(true);
+ });
+
+ it('should return false if the sample rate is less than the random number * 100', () => {
+ randomNumber = 0.9;
+
+ expect(RavenConfig.shouldSendSample()).toBe(false);
+ });
+
+ it('should return true if the sample rate is equal to the random number * 100', () => {
+ randomNumber = 0.5;
+
+ expect(RavenConfig.shouldSendSample()).toBe(true);
+ });
+ });
+});
diff --git a/spec/javascripts/search_autocomplete_spec.js b/spec/javascripts/search_autocomplete_spec.js
index aaf058bd755..fa52a8a0dd2 100644
--- a/spec/javascripts/search_autocomplete_spec.js
+++ b/spec/javascripts/search_autocomplete_spec.js
@@ -3,7 +3,6 @@
require('~/gl_dropdown');
require('~/search_autocomplete');
require('~/lib/utils/common_utils');
-require('~/lib/utils/type_utility');
require('vendor/fuzzaldrin-plus');
(function() {
diff --git a/spec/javascripts/shortcuts_issuable_spec.js b/spec/javascripts/shortcuts_issuable_spec.js
index 9e19dabd0e3..757b8d595a4 100644
--- a/spec/javascripts/shortcuts_issuable_spec.js
+++ b/spec/javascripts/shortcuts_issuable_spec.js
@@ -13,7 +13,7 @@ require('~/shortcuts_issuable');
document.querySelector('.js-new-note-form').classList.add('js-main-target-form');
this.shortcut = new ShortcutsIssuable();
});
- describe('#replyWithSelectedText', function() {
+ describe('replyWithSelectedText', function() {
var stubSelection;
// Stub window.gl.utils.getSelectedFragment to return a node with the provided HTML.
stubSelection = function(html) {
diff --git a/spec/javascripts/sidebar/assignee_title_spec.js b/spec/javascripts/sidebar/assignee_title_spec.js
new file mode 100644
index 00000000000..5b5b1bf4140
--- /dev/null
+++ b/spec/javascripts/sidebar/assignee_title_spec.js
@@ -0,0 +1,80 @@
+import Vue from 'vue';
+import AssigneeTitle from '~/sidebar/components/assignees/assignee_title';
+
+describe('AssigneeTitle component', () => {
+ let component;
+ let AssigneeTitleComponent;
+
+ beforeEach(() => {
+ AssigneeTitleComponent = Vue.extend(AssigneeTitle);
+ });
+
+ describe('assignee title', () => {
+ it('renders assignee', () => {
+ component = new AssigneeTitleComponent({
+ propsData: {
+ numberOfAssignees: 1,
+ editable: false,
+ },
+ }).$mount();
+
+ expect(component.$el.innerText.trim()).toEqual('Assignee');
+ });
+
+ it('renders 2 assignees', () => {
+ component = new AssigneeTitleComponent({
+ propsData: {
+ numberOfAssignees: 2,
+ editable: false,
+ },
+ }).$mount();
+
+ expect(component.$el.innerText.trim()).toEqual('2 Assignees');
+ });
+ });
+
+ it('does not render spinner by default', () => {
+ component = new AssigneeTitleComponent({
+ propsData: {
+ numberOfAssignees: 0,
+ editable: false,
+ },
+ }).$mount();
+
+ expect(component.$el.querySelector('.fa')).toBeNull();
+ });
+
+ it('renders spinner when loading', () => {
+ component = new AssigneeTitleComponent({
+ propsData: {
+ loading: true,
+ numberOfAssignees: 0,
+ editable: false,
+ },
+ }).$mount();
+
+ expect(component.$el.querySelector('.fa')).not.toBeNull();
+ });
+
+ it('does not render edit link when not editable', () => {
+ component = new AssigneeTitleComponent({
+ propsData: {
+ numberOfAssignees: 0,
+ editable: false,
+ },
+ }).$mount();
+
+ expect(component.$el.querySelector('.edit-link')).toBeNull();
+ });
+
+ it('renders edit link when editable', () => {
+ component = new AssigneeTitleComponent({
+ propsData: {
+ numberOfAssignees: 0,
+ editable: true,
+ },
+ }).$mount();
+
+ expect(component.$el.querySelector('.edit-link')).not.toBeNull();
+ });
+});
diff --git a/spec/javascripts/sidebar/assignees_spec.js b/spec/javascripts/sidebar/assignees_spec.js
new file mode 100644
index 00000000000..c9453a21189
--- /dev/null
+++ b/spec/javascripts/sidebar/assignees_spec.js
@@ -0,0 +1,272 @@
+import Vue from 'vue';
+import Assignee from '~/sidebar/components/assignees/assignees';
+import UsersMock from './mock_data';
+import UsersMockHelper from '../helpers/user_mock_data_helper';
+
+describe('Assignee component', () => {
+ let component;
+ let AssigneeComponent;
+
+ beforeEach(() => {
+ AssigneeComponent = Vue.extend(Assignee);
+ });
+
+ describe('No assignees/users', () => {
+ it('displays no assignee icon when collapsed', () => {
+ component = new AssigneeComponent({
+ propsData: {
+ rootPath: 'http://localhost:3000',
+ users: [],
+ editable: false,
+ },
+ }).$mount();
+
+ const collapsed = component.$el.querySelector('.sidebar-collapsed-icon');
+ expect(collapsed.childElementCount).toEqual(1);
+ expect(collapsed.children[0].getAttribute('aria-label')).toEqual('No Assignee');
+ expect(collapsed.children[0].classList.contains('fa')).toEqual(true);
+ expect(collapsed.children[0].classList.contains('fa-user')).toEqual(true);
+ });
+
+ it('displays only "No assignee" when no users are assigned and the issue is read-only', () => {
+ component = new AssigneeComponent({
+ propsData: {
+ rootPath: 'http://localhost:3000',
+ users: [],
+ editable: false,
+ },
+ }).$mount();
+ const componentTextNoUsers = component.$el.querySelector('.assign-yourself').innerText.trim();
+
+ expect(componentTextNoUsers).toBe('No assignee');
+ expect(componentTextNoUsers.indexOf('assign yourself')).toEqual(-1);
+ });
+
+ it('displays only "No assignee" when no users are assigned and the issue can be edited', () => {
+ component = new AssigneeComponent({
+ propsData: {
+ rootPath: 'http://localhost:3000',
+ users: [],
+ editable: true,
+ },
+ }).$mount();
+ const componentTextNoUsers = component.$el.querySelector('.assign-yourself').innerText.trim();
+
+ expect(componentTextNoUsers.indexOf('No assignee')).toEqual(0);
+ expect(componentTextNoUsers.indexOf('assign yourself')).toBeGreaterThan(0);
+ });
+
+ it('emits the assign-self event when "assign yourself" is clicked', () => {
+ component = new AssigneeComponent({
+ propsData: {
+ rootPath: 'http://localhost:3000',
+ users: [],
+ editable: true,
+ },
+ }).$mount();
+
+ spyOn(component, '$emit');
+ component.$el.querySelector('.assign-yourself .btn-link').click();
+ expect(component.$emit).toHaveBeenCalledWith('assign-self');
+ });
+ });
+
+ describe('One assignee/user', () => {
+ it('displays one assignee icon when collapsed', () => {
+ component = new AssigneeComponent({
+ propsData: {
+ rootPath: 'http://localhost:3000',
+ users: [
+ UsersMock.user,
+ ],
+ editable: false,
+ },
+ }).$mount();
+
+ const collapsed = component.$el.querySelector('.sidebar-collapsed-icon');
+ const assignee = collapsed.children[0];
+ expect(collapsed.childElementCount).toEqual(1);
+ expect(assignee.querySelector('.avatar').getAttribute('src')).toEqual(UsersMock.user.avatar);
+ expect(assignee.querySelector('.avatar').getAttribute('alt')).toEqual(`${UsersMock.user.name}'s avatar`);
+ expect(assignee.querySelector('.author').innerText.trim()).toEqual(UsersMock.user.name);
+ });
+
+ it('Shows one user with avatar, username and author name', () => {
+ component = new AssigneeComponent({
+ propsData: {
+ rootPath: 'http://localhost:3000/',
+ users: [
+ UsersMock.user,
+ ],
+ editable: true,
+ },
+ }).$mount();
+
+ expect(component.$el.querySelector('.author_link')).not.toBeNull();
+ // The image
+ expect(component.$el.querySelector('.author_link img').getAttribute('src')).toEqual(UsersMock.user.avatar);
+ // Author name
+ expect(component.$el.querySelector('.author_link .author').innerText.trim()).toEqual(UsersMock.user.name);
+ // Username
+ expect(component.$el.querySelector('.author_link .username').innerText.trim()).toEqual(`@${UsersMock.user.username}`);
+ });
+
+ it('has the root url present in the assigneeUrl method', () => {
+ component = new AssigneeComponent({
+ propsData: {
+ rootPath: 'http://localhost:3000/',
+ users: [
+ UsersMock.user,
+ ],
+ editable: true,
+ },
+ }).$mount();
+
+ expect(component.assigneeUrl(UsersMock.user).indexOf('http://localhost:3000/')).not.toEqual(-1);
+ });
+ });
+
+ describe('Two or more assignees/users', () => {
+ it('displays two assignee icons when collapsed', () => {
+ const users = UsersMockHelper.createNumberRandomUsers(2);
+ component = new AssigneeComponent({
+ propsData: {
+ rootPath: 'http://localhost:3000',
+ users,
+ editable: false,
+ },
+ }).$mount();
+
+ const collapsed = component.$el.querySelector('.sidebar-collapsed-icon');
+ expect(collapsed.childElementCount).toEqual(2);
+
+ const first = collapsed.children[0];
+ expect(first.querySelector('.avatar').getAttribute('src')).toEqual(users[0].avatar);
+ expect(first.querySelector('.avatar').getAttribute('alt')).toEqual(`${users[0].name}'s avatar`);
+ expect(first.querySelector('.author').innerText.trim()).toEqual(users[0].name);
+
+ const second = collapsed.children[1];
+ expect(second.querySelector('.avatar').getAttribute('src')).toEqual(users[1].avatar);
+ expect(second.querySelector('.avatar').getAttribute('alt')).toEqual(`${users[1].name}'s avatar`);
+ expect(second.querySelector('.author').innerText.trim()).toEqual(users[1].name);
+ });
+
+ it('displays one assignee icon and counter when collapsed', () => {
+ const users = UsersMockHelper.createNumberRandomUsers(3);
+ component = new AssigneeComponent({
+ propsData: {
+ rootPath: 'http://localhost:3000',
+ users,
+ editable: false,
+ },
+ }).$mount();
+
+ const collapsed = component.$el.querySelector('.sidebar-collapsed-icon');
+ expect(collapsed.childElementCount).toEqual(2);
+
+ const first = collapsed.children[0];
+ expect(first.querySelector('.avatar').getAttribute('src')).toEqual(users[0].avatar);
+ expect(first.querySelector('.avatar').getAttribute('alt')).toEqual(`${users[0].name}'s avatar`);
+ expect(first.querySelector('.author').innerText.trim()).toEqual(users[0].name);
+
+ const second = collapsed.children[1];
+ expect(second.querySelector('.avatar-counter').innerText.trim()).toEqual('+2');
+ });
+
+ it('Shows two assignees', () => {
+ const users = UsersMockHelper.createNumberRandomUsers(2);
+ component = new AssigneeComponent({
+ propsData: {
+ rootPath: 'http://localhost:3000',
+ users,
+ editable: true,
+ },
+ }).$mount();
+
+ expect(component.$el.querySelectorAll('.user-item').length).toEqual(users.length);
+ expect(component.$el.querySelector('.user-list-more')).toBe(null);
+ });
+
+ it('Shows the "show-less" assignees label', (done) => {
+ const users = UsersMockHelper.createNumberRandomUsers(6);
+ component = new AssigneeComponent({
+ propsData: {
+ rootPath: 'http://localhost:3000',
+ users,
+ editable: true,
+ },
+ }).$mount();
+
+ expect(component.$el.querySelectorAll('.user-item').length).toEqual(component.defaultRenderCount);
+ expect(component.$el.querySelector('.user-list-more')).not.toBe(null);
+ const usersLabelExpectation = users.length - component.defaultRenderCount;
+ expect(component.$el.querySelector('.user-list-more .btn-link').innerText.trim())
+ .not.toBe(`+${usersLabelExpectation} more`);
+ component.toggleShowLess();
+ Vue.nextTick(() => {
+ expect(component.$el.querySelector('.user-list-more .btn-link').innerText.trim())
+ .toBe('- show less');
+ done();
+ });
+ });
+
+ it('Shows the "show-less" when "n+ more " label is clicked', (done) => {
+ const users = UsersMockHelper.createNumberRandomUsers(6);
+ component = new AssigneeComponent({
+ propsData: {
+ rootPath: 'http://localhost:3000',
+ users,
+ editable: true,
+ },
+ }).$mount();
+
+ component.$el.querySelector('.user-list-more .btn-link').click();
+ Vue.nextTick(() => {
+ expect(component.$el.querySelector('.user-list-more .btn-link').innerText.trim())
+ .toBe('- show less');
+ done();
+ });
+ });
+
+ it('gets the count of avatar via a computed property ', () => {
+ const users = UsersMockHelper.createNumberRandomUsers(6);
+ component = new AssigneeComponent({
+ propsData: {
+ rootPath: 'http://localhost:3000',
+ users,
+ editable: true,
+ },
+ }).$mount();
+
+ expect(component.sidebarAvatarCounter).toEqual(`+${users.length - 1}`);
+ });
+
+ describe('n+ more label', () => {
+ beforeEach(() => {
+ const users = UsersMockHelper.createNumberRandomUsers(6);
+ component = new AssigneeComponent({
+ propsData: {
+ rootPath: 'http://localhost:3000',
+ users,
+ editable: true,
+ },
+ }).$mount();
+ });
+
+ it('shows "+1 more" label', () => {
+ expect(component.$el.querySelector('.user-list-more .btn-link').innerText.trim())
+ .toBe('+ 1 more');
+ });
+
+ it('shows "show less" label', (done) => {
+ component.toggleShowLess();
+
+ Vue.nextTick(() => {
+ expect(component.$el.querySelector('.user-list-more .btn-link').innerText.trim())
+ .toBe('- show less');
+ done();
+ });
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/sidebar/mock_data.js b/spec/javascripts/sidebar/mock_data.js
new file mode 100644
index 00000000000..9fc8667ecc9
--- /dev/null
+++ b/spec/javascripts/sidebar/mock_data.js
@@ -0,0 +1,109 @@
+/* eslint-disable quote-props*/
+
+const sidebarMockData = {
+ 'GET': {
+ '/gitlab-org/gitlab-shell/issues/5.json': {
+ id: 45,
+ iid: 5,
+ author_id: 23,
+ description: 'Nulla ullam commodi delectus adipisci quis sit.',
+ lock_version: null,
+ milestone_id: 21,
+ position: 0,
+ state: 'closed',
+ title: 'Vel et nulla voluptatibus corporis dolor iste saepe laborum.',
+ updated_by_id: 1,
+ created_at: '2017-02-02T21: 49: 49.664Z',
+ updated_at: '2017-05-03T22: 26: 03.760Z',
+ deleted_at: null,
+ time_estimate: 0,
+ total_time_spent: 0,
+ human_time_estimate: null,
+ human_total_time_spent: null,
+ branch_name: null,
+ confidential: false,
+ assignees: [
+ {
+ name: 'User 0',
+ username: 'user0',
+ id: 22,
+ state: 'active',
+ avatar_url: 'http: //www.gravatar.com/avatar/52e4ce24a915fb7e51e1ad3b57f4b00a?s=80\u0026d=identicon',
+ web_url: 'http: //localhost:3001/user0',
+ },
+ {
+ name: 'Marguerite Bartell',
+ username: 'tajuana',
+ id: 18,
+ state: 'active',
+ avatar_url: 'http: //www.gravatar.com/avatar/4852a41fb41616bf8f140d3701673f53?s=80\u0026d=identicon',
+ web_url: 'http: //localhost:3001/tajuana',
+ },
+ {
+ name: 'Laureen Ritchie',
+ username: 'michaele.will',
+ id: 16,
+ state: 'active',
+ avatar_url: 'http: //www.gravatar.com/avatar/e301827eb03be955c9c172cb9a8e4e8a?s=80\u0026d=identicon',
+ web_url: 'http: //localhost:3001/michaele.will',
+ },
+ ],
+ due_date: null,
+ moved_to_id: null,
+ project_id: 4,
+ weight: null,
+ milestone: {
+ id: 21,
+ iid: 1,
+ project_id: 4,
+ title: 'v0.0',
+ description: 'Molestiae commodi laboriosam odio sunt eaque reprehenderit.',
+ state: 'active',
+ created_at: '2017-02-02T21: 49: 30.530Z',
+ updated_at: '2017-02-02T21: 49: 30.530Z',
+ due_date: null,
+ start_date: null,
+ },
+ labels: [],
+ },
+ },
+ 'PUT': {
+ '/gitlab-org/gitlab-shell/issues/5.json': {
+ data: {},
+ },
+ },
+};
+
+export default {
+ mediator: {
+ endpoint: '/gitlab-org/gitlab-shell/issues/5.json',
+ editable: true,
+ currentUser: {
+ id: 1,
+ name: 'Administrator',
+ username: 'root',
+ avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ },
+ rootPath: '/',
+ },
+ time: {
+ time_estimate: 3600,
+ total_time_spent: 0,
+ human_time_estimate: '1h',
+ human_total_time_spent: null,
+ },
+ user: {
+ avatar: 'http://gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ id: 1,
+ name: 'Administrator',
+ username: 'root',
+ },
+
+ sidebarMockInterceptor(request, next) {
+ const body = sidebarMockData[request.method.toUpperCase()][request.url];
+
+ next(request.respondWith(JSON.stringify(body), {
+ status: 200,
+ }));
+ },
+};
diff --git a/spec/javascripts/sidebar/sidebar_assignees_spec.js b/spec/javascripts/sidebar/sidebar_assignees_spec.js
new file mode 100644
index 00000000000..865951b2ad7
--- /dev/null
+++ b/spec/javascripts/sidebar/sidebar_assignees_spec.js
@@ -0,0 +1,46 @@
+import Vue from 'vue';
+import SidebarAssignees from '~/sidebar/components/assignees/sidebar_assignees';
+import SidebarMediator from '~/sidebar/sidebar_mediator';
+import SidebarService from '~/sidebar/services/sidebar_service';
+import SidebarStore from '~/sidebar/stores/sidebar_store';
+import Mock from './mock_data';
+
+describe('sidebar assignees', () => {
+ let component;
+ let SidebarAssigneeComponent;
+ preloadFixtures('issues/open-issue.html.raw');
+
+ beforeEach(() => {
+ Vue.http.interceptors.push(Mock.sidebarMockInterceptor);
+ SidebarAssigneeComponent = Vue.extend(SidebarAssignees);
+ spyOn(SidebarMediator.prototype, 'saveAssignees').and.callThrough();
+ spyOn(SidebarMediator.prototype, 'assignYourself').and.callThrough();
+ this.mediator = new SidebarMediator(Mock.mediator);
+ loadFixtures('issues/open-issue.html.raw');
+ this.sidebarAssigneesEl = document.querySelector('#js-vue-sidebar-assignees');
+ });
+
+ afterEach(() => {
+ SidebarService.singleton = null;
+ SidebarStore.singleton = null;
+ SidebarMediator.singleton = null;
+ Vue.http.interceptors = _.without(Vue.http.interceptors, Mock.sidebarMockInterceptor);
+ });
+
+ it('calls the mediator when saves the assignees', () => {
+ component = new SidebarAssigneeComponent()
+ .$mount(this.sidebarAssigneesEl);
+ component.saveAssignees();
+
+ expect(SidebarMediator.prototype.saveAssignees).toHaveBeenCalled();
+ });
+
+ it('calls the mediator when "assignSelf" method is called', () => {
+ component = new SidebarAssigneeComponent()
+ .$mount(this.sidebarAssigneesEl);
+ component.assignSelf();
+
+ expect(SidebarMediator.prototype.assignYourself).toHaveBeenCalled();
+ expect(this.mediator.store.assignees.length).toEqual(1);
+ });
+});
diff --git a/spec/javascripts/sidebar/sidebar_mediator_spec.js b/spec/javascripts/sidebar/sidebar_mediator_spec.js
new file mode 100644
index 00000000000..e246f41ee82
--- /dev/null
+++ b/spec/javascripts/sidebar/sidebar_mediator_spec.js
@@ -0,0 +1,41 @@
+import Vue from 'vue';
+import SidebarMediator from '~/sidebar/sidebar_mediator';
+import SidebarStore from '~/sidebar/stores/sidebar_store';
+import SidebarService from '~/sidebar/services/sidebar_service';
+import Mock from './mock_data';
+
+describe('Sidebar mediator', () => {
+ beforeEach(() => {
+ Vue.http.interceptors.push(Mock.sidebarMockInterceptor);
+ this.mediator = new SidebarMediator(Mock.mediator);
+ });
+
+ afterEach(() => {
+ SidebarService.singleton = null;
+ SidebarStore.singleton = null;
+ SidebarMediator.singleton = null;
+ Vue.http.interceptors = _.without(Vue.http.interceptors, Mock.sidebarMockInterceptor);
+ });
+
+ it('assigns yourself ', () => {
+ this.mediator.assignYourself();
+
+ expect(this.mediator.store.currentUser).toEqual(Mock.mediator.currentUser);
+ expect(this.mediator.store.assignees[0]).toEqual(Mock.mediator.currentUser);
+ });
+
+ it('saves assignees', (done) => {
+ this.mediator.saveAssignees('issue[assignee_ids]')
+ .then((resp) => {
+ expect(resp.status).toEqual(200);
+ done();
+ })
+ .catch(() => {});
+ });
+
+ it('fetches the data', () => {
+ spyOn(this.mediator.service, 'get').and.callThrough();
+ this.mediator.fetch();
+ expect(this.mediator.service.get).toHaveBeenCalled();
+ });
+});
diff --git a/spec/javascripts/sidebar/sidebar_service_spec.js b/spec/javascripts/sidebar/sidebar_service_spec.js
new file mode 100644
index 00000000000..91a4dd669a7
--- /dev/null
+++ b/spec/javascripts/sidebar/sidebar_service_spec.js
@@ -0,0 +1,33 @@
+import Vue from 'vue';
+import SidebarService from '~/sidebar/services/sidebar_service';
+import Mock from './mock_data';
+
+describe('Sidebar service', () => {
+ beforeEach(() => {
+ Vue.http.interceptors.push(Mock.sidebarMockInterceptor);
+ this.service = new SidebarService('/gitlab-org/gitlab-shell/issues/5.json');
+ });
+
+ afterEach(() => {
+ SidebarService.singleton = null;
+ Vue.http.interceptors = _.without(Vue.http.interceptors, Mock.sidebarMockInterceptor);
+ });
+
+ it('gets the data', (done) => {
+ this.service.get()
+ .then((resp) => {
+ expect(resp).toBeDefined();
+ done();
+ })
+ .catch(() => {});
+ });
+
+ it('updates the data', (done) => {
+ this.service.update('issue[assignee_ids]', [1])
+ .then((resp) => {
+ expect(resp).toBeDefined();
+ done();
+ })
+ .catch(() => {});
+ });
+});
diff --git a/spec/javascripts/sidebar/sidebar_store_spec.js b/spec/javascripts/sidebar/sidebar_store_spec.js
new file mode 100644
index 00000000000..29facf483b5
--- /dev/null
+++ b/spec/javascripts/sidebar/sidebar_store_spec.js
@@ -0,0 +1,80 @@
+import SidebarStore from '~/sidebar/stores/sidebar_store';
+import Mock from './mock_data';
+import UsersMockHelper from '../helpers/user_mock_data_helper';
+
+describe('Sidebar store', () => {
+ const assignee = {
+ id: 2,
+ name: 'gitlab user 2',
+ username: 'gitlab2',
+ avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ };
+
+ const anotherAssignee = {
+ id: 3,
+ name: 'gitlab user 3',
+ username: 'gitlab3',
+ avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ };
+
+ beforeEach(() => {
+ this.store = new SidebarStore({
+ currentUser: {
+ id: 1,
+ name: 'Administrator',
+ username: 'root',
+ avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ },
+ editable: true,
+ rootPath: '/',
+ endpoint: '/gitlab-org/gitlab-shell/issues/5.json',
+ });
+ });
+
+ afterEach(() => {
+ SidebarStore.singleton = null;
+ });
+
+ it('adds a new assignee', () => {
+ this.store.addAssignee(assignee);
+ expect(this.store.assignees.length).toEqual(1);
+ });
+
+ it('removes an assignee', () => {
+ this.store.removeAssignee(assignee);
+ expect(this.store.assignees.length).toEqual(0);
+ });
+
+ it('finds an existent assignee', () => {
+ let foundAssignee;
+
+ this.store.addAssignee(assignee);
+ foundAssignee = this.store.findAssignee(assignee);
+ expect(foundAssignee).toBeDefined();
+ expect(foundAssignee).toEqual(assignee);
+ foundAssignee = this.store.findAssignee(anotherAssignee);
+ expect(foundAssignee).toBeUndefined();
+ });
+
+ it('removes all assignees', () => {
+ this.store.removeAllAssignees();
+ expect(this.store.assignees.length).toEqual(0);
+ });
+
+ it('set assigned data', () => {
+ const users = {
+ assignees: UsersMockHelper.createNumberRandomUsers(3),
+ };
+
+ this.store.setAssigneeData(users);
+ expect(this.store.assignees.length).toEqual(3);
+ });
+
+ it('set time tracking data', () => {
+ this.store.setTimeTrackingData(Mock.time);
+ expect(this.store.timeEstimate).toEqual(Mock.time.time_estimate);
+ expect(this.store.totalTimeSpent).toEqual(Mock.time.total_time_spent);
+ expect(this.store.humanTimeEstimate).toEqual(Mock.time.human_time_estimate);
+ expect(this.store.humanTotalTimeSpent).toEqual(Mock.time.human_total_time_spent);
+ });
+});
diff --git a/spec/javascripts/signin_tabs_memoizer_spec.js b/spec/javascripts/signin_tabs_memoizer_spec.js
index d83d9a57b42..5b4f5933b34 100644
--- a/spec/javascripts/signin_tabs_memoizer_spec.js
+++ b/spec/javascripts/signin_tabs_memoizer_spec.js
@@ -1,3 +1,5 @@
+import AccessorUtilities from '~/lib/utils/accessor';
+
require('~/signin_tabs_memoizer');
((global) => {
@@ -19,6 +21,8 @@ require('~/signin_tabs_memoizer');
beforeEach(() => {
loadFixtures(fixtureTemplate);
+
+ spyOn(AccessorUtilities, 'isLocalStorageAccessSafe').and.returnValue(true);
});
it('does nothing if no tab was previously selected', () => {
@@ -49,5 +53,91 @@ require('~/signin_tabs_memoizer');
expect(memo.readData()).toEqual('#standard');
});
+
+ describe('class constructor', () => {
+ beforeEach(() => {
+ memo = createMemoizer();
+ });
+
+ it('should set .isLocalStorageAvailable', () => {
+ expect(AccessorUtilities.isLocalStorageAccessSafe).toHaveBeenCalled();
+ expect(memo.isLocalStorageAvailable).toBe(true);
+ });
+ });
+
+ describe('saveData', () => {
+ beforeEach(() => {
+ memo = {
+ currentTabKey,
+ };
+
+ spyOn(localStorage, 'setItem');
+ });
+
+ describe('if .isLocalStorageAvailable is `false`', () => {
+ beforeEach(function () {
+ memo.isLocalStorageAvailable = false;
+
+ global.ActiveTabMemoizer.prototype.saveData.call(memo);
+ });
+
+ it('should not call .setItem', () => {
+ expect(localStorage.setItem).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('if .isLocalStorageAvailable is `true`', () => {
+ const value = 'value';
+
+ beforeEach(function () {
+ memo.isLocalStorageAvailable = true;
+
+ global.ActiveTabMemoizer.prototype.saveData.call(memo, value);
+ });
+
+ it('should call .setItem', () => {
+ expect(localStorage.setItem).toHaveBeenCalledWith(currentTabKey, value);
+ });
+ });
+ });
+
+ describe('readData', () => {
+ const itemValue = 'itemValue';
+ let readData;
+
+ beforeEach(() => {
+ memo = {
+ currentTabKey,
+ };
+
+ spyOn(localStorage, 'getItem').and.returnValue(itemValue);
+ });
+
+ describe('if .isLocalStorageAvailable is `false`', () => {
+ beforeEach(function () {
+ memo.isLocalStorageAvailable = false;
+
+ readData = global.ActiveTabMemoizer.prototype.readData.call(memo);
+ });
+
+ it('should not call .getItem and should return `null`', () => {
+ expect(localStorage.getItem).not.toHaveBeenCalled();
+ expect(readData).toBe(null);
+ });
+ });
+
+ describe('if .isLocalStorageAvailable is `true`', () => {
+ beforeEach(function () {
+ memo.isLocalStorageAvailable = true;
+
+ readData = global.ActiveTabMemoizer.prototype.readData.call(memo);
+ });
+
+ it('should call .getItem and return the localStorage value', () => {
+ expect(window.localStorage.getItem).toHaveBeenCalledWith(currentTabKey);
+ expect(readData).toBe(itemValue);
+ });
+ });
+ });
});
})(window);
diff --git a/spec/javascripts/subbable_resource_spec.js b/spec/javascripts/subbable_resource_spec.js
deleted file mode 100644
index 454386697f5..00000000000
--- a/spec/javascripts/subbable_resource_spec.js
+++ /dev/null
@@ -1,63 +0,0 @@
-/* eslint-disable max-len, arrow-parens, comma-dangle */
-
-require('~/subbable_resource');
-
-/*
-* Test that each rest verb calls the publish and subscribe function and passes the correct value back
-*
-*
-* */
-((global) => {
- describe('Subbable Resource', function () {
- describe('PubSub', function () {
- beforeEach(function () {
- this.MockResource = new global.SubbableResource('https://example.com');
- });
- it('should successfully add a single subscriber', function () {
- const callback = () => {};
- this.MockResource.subscribe(callback);
-
- expect(this.MockResource.subscribers.length).toBe(1);
- expect(this.MockResource.subscribers[0]).toBe(callback);
- });
-
- it('should successfully add multiple subscribers', function () {
- const callbackOne = () => {};
- const callbackTwo = () => {};
- const callbackThree = () => {};
-
- this.MockResource.subscribe(callbackOne);
- this.MockResource.subscribe(callbackTwo);
- this.MockResource.subscribe(callbackThree);
-
- expect(this.MockResource.subscribers.length).toBe(3);
- });
-
- it('should successfully publish an update to a single subscriber', function () {
- const state = { myprop: 1 };
-
- const callbacks = {
- one: (data) => expect(data.myprop).toBe(2),
- two: (data) => expect(data.myprop).toBe(2),
- three: (data) => expect(data.myprop).toBe(2)
- };
-
- const spyOne = spyOn(callbacks, 'one');
- const spyTwo = spyOn(callbacks, 'two');
- const spyThree = spyOn(callbacks, 'three');
-
- this.MockResource.subscribe(callbacks.one);
- this.MockResource.subscribe(callbacks.two);
- this.MockResource.subscribe(callbacks.three);
-
- state.myprop += 1;
-
- this.MockResource.publish(state);
-
- expect(spyOne).toHaveBeenCalled();
- expect(spyTwo).toHaveBeenCalled();
- expect(spyThree).toHaveBeenCalled();
- });
- });
- });
-})(window.gl || (window.gl = {}));
diff --git a/spec/javascripts/test_bundle.js b/spec/javascripts/test_bundle.js
index 07dc51a7815..0464b5d2329 100644
--- a/spec/javascripts/test_bundle.js
+++ b/spec/javascripts/test_bundle.js
@@ -1,8 +1,8 @@
// enable test fixtures
require('jasmine-jquery');
-jasmine.getFixtures().fixturesPath = 'base/spec/javascripts/fixtures';
-jasmine.getJSONFixtures().fixturesPath = 'base/spec/javascripts/fixtures';
+jasmine.getFixtures().fixturesPath = '/base/spec/javascripts/fixtures';
+jasmine.getJSONFixtures().fixturesPath = '/base/spec/javascripts/fixtures';
// include common libraries
require('~/commons/index.js');
@@ -55,7 +55,6 @@ if (process.env.BABEL_ENV === 'coverage') {
'./merge_conflicts/merge_conflicts_bundle.js',
'./merge_conflicts/components/inline_conflict_lines.js',
'./merge_conflicts/components/parallel_conflict_lines.js',
- './merge_request_widget/ci_bundle.js',
'./monitoring/monitoring_bundle.js',
'./network/network_bundle.js',
'./network/branch_graph.js',
diff --git a/spec/javascripts/u2f/mock_u2f_device.js b/spec/javascripts/u2f/mock_u2f_device.js
index 6677fe9c1ee..4eb8ad3d9e4 100644
--- a/spec/javascripts/u2f/mock_u2f_device.js
+++ b/spec/javascripts/u2f/mock_u2f_device.js
@@ -1,12 +1,10 @@
/* eslint-disable space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-unused-expressions, no-return-assign, no-param-reassign, max-len */
(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);
+ this.respondToAuthenticateRequest = this.respondToAuthenticateRequest.bind(this);
+ this.respondToRegisterRequest = this.respondToRegisterRequest.bind(this);
window.u2f || (window.u2f = {});
window.u2f.register = (function(_this) {
return function(appId, registerRequests, signRequests, callback) {
diff --git a/spec/javascripts/version_check_image_spec.js b/spec/javascripts/version_check_image_spec.js
index 464c1fce210..83ffeca253a 100644
--- a/spec/javascripts/version_check_image_spec.js
+++ b/spec/javascripts/version_check_image_spec.js
@@ -3,7 +3,7 @@ const VersionCheckImage = require('~/version_check_image');
require('jquery');
describe('VersionCheckImage', function () {
- describe('.bindErrorEvent', function () {
+ describe('bindErrorEvent', function () {
ClassSpecHelper.itShouldBeAStaticMethod(VersionCheckImage, 'bindErrorEvent');
beforeEach(function () {
diff --git a/spec/javascripts/visibility_select_spec.js b/spec/javascripts/visibility_select_spec.js
index 9727c03c91e..b26ed41f27a 100644
--- a/spec/javascripts/visibility_select_spec.js
+++ b/spec/javascripts/visibility_select_spec.js
@@ -22,7 +22,7 @@ require('~/visibility_select');
spyOn(Element.prototype, 'querySelector').and.callFake(selector => mockElements[selector]);
});
- describe('#constructor', function () {
+ describe('constructor', function () {
beforeEach(function () {
this.visibilitySelect = new VisibilitySelect(mockElements.container);
});
@@ -48,7 +48,7 @@ require('~/visibility_select');
});
});
- describe('#init', function () {
+ describe('init', function () {
describe('if there is a select', function () {
beforeEach(function () {
this.visibilitySelect = new VisibilitySelect(mockElements.container);
@@ -85,7 +85,7 @@ require('~/visibility_select');
});
});
- describe('#updateHelpText', function () {
+ describe('updateHelpText', function () {
beforeEach(function () {
this.visibilitySelect = new VisibilitySelect(mockElements.container);
this.visibilitySelect.init();
diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_author_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_author_spec.js
new file mode 100644
index 00000000000..a750bc78f36
--- /dev/null
+++ b/spec/javascripts/vue_mr_widget/components/mr_widget_author_spec.js
@@ -0,0 +1,39 @@
+import Vue from 'vue';
+import authorComponent from '~/vue_merge_request_widget/components/mr_widget_author';
+
+const author = {
+ webUrl: 'http://foo.bar',
+ avatarUrl: 'http://gravatar.com/foo',
+ name: 'fatihacet',
+};
+const createComponent = () => {
+ const Component = Vue.extend(authorComponent);
+
+ return new Component({
+ el: document.createElement('div'),
+ propsData: { author },
+ });
+};
+
+describe('MRWidgetAuthor', () => {
+ describe('props', () => {
+ it('should have props', () => {
+ const authorProp = authorComponent.props.author;
+
+ expect(authorProp).toBeDefined();
+ expect(authorProp.type instanceof Object).toBeTruthy();
+ expect(authorProp.required).toBeTruthy();
+ });
+ });
+
+ describe('template', () => {
+ it('should have correct elements', () => {
+ const el = createComponent().$el;
+
+ expect(el.tagName).toEqual('A');
+ expect(el.getAttribute('href')).toEqual(author.webUrl);
+ expect(el.querySelector('img').getAttribute('src')).toEqual(author.avatarUrl);
+ expect(el.querySelector('.author').innerText.trim()).toEqual(author.name);
+ });
+ });
+});
diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_author_time_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_author_time_spec.js
new file mode 100644
index 00000000000..515ddcbb875
--- /dev/null
+++ b/spec/javascripts/vue_mr_widget/components/mr_widget_author_time_spec.js
@@ -0,0 +1,61 @@
+import Vue from 'vue';
+import authorTimeComponent from '~/vue_merge_request_widget/components/mr_widget_author_time';
+
+const props = {
+ actionText: 'Merged by',
+ author: {
+ webUrl: 'http://foo.bar',
+ avatarUrl: 'http://gravatar.com/foo',
+ name: 'fatihacet',
+ },
+ dateTitle: '2017-03-23T23:02:00.807Z',
+ dateReadable: '12 hours ago',
+};
+const createComponent = () => {
+ const Component = Vue.extend(authorTimeComponent);
+
+ return new Component({
+ el: document.createElement('div'),
+ propsData: props,
+ });
+};
+
+describe('MRWidgetAuthorTime', () => {
+ describe('props', () => {
+ it('should have props', () => {
+ const { actionText, author, dateTitle, dateReadable } = authorTimeComponent.props;
+ const ActionTextClass = actionText.type;
+ const DateTitleClass = dateTitle.type;
+ const DateReadableClass = dateReadable.type;
+
+ expect(new ActionTextClass() instanceof String).toBeTruthy();
+ expect(actionText.required).toBeTruthy();
+
+ expect(author.type instanceof Object).toBeTruthy();
+ expect(author.required).toBeTruthy();
+
+ expect(new DateTitleClass() instanceof String).toBeTruthy();
+ expect(dateTitle.required).toBeTruthy();
+
+ expect(new DateReadableClass() instanceof String).toBeTruthy();
+ expect(dateReadable.required).toBeTruthy();
+ });
+ });
+
+ describe('components', () => {
+ it('should have components', () => {
+ expect(authorTimeComponent.components['mr-widget-author']).toBeDefined();
+ });
+ });
+
+ describe('template', () => {
+ it('should have correct elements', () => {
+ const el = createComponent().$el;
+
+ expect(el.tagName).toEqual('H4');
+ expect(el.querySelector('a').getAttribute('href')).toEqual(props.author.webUrl);
+ expect(el.querySelector('time').innerText).toContain(props.dateReadable);
+ expect(el.querySelector('time').getAttribute('title')).toEqual(props.dateTitle);
+ });
+ });
+});
diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_deployment_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_deployment_spec.js
new file mode 100644
index 00000000000..d4b200875df
--- /dev/null
+++ b/spec/javascripts/vue_mr_widget/components/mr_widget_deployment_spec.js
@@ -0,0 +1,188 @@
+import Vue from 'vue';
+import deploymentComponent from '~/vue_merge_request_widget/components/mr_widget_deployment';
+import MRWidgetService from '~/vue_merge_request_widget/services/mr_widget_service';
+import { statusIconEntityMap } from '~/vue_shared/ci_status_icons';
+
+const deploymentMockData = [
+ {
+ id: 15,
+ name: 'review/diplo',
+ url: '/root/acets-review-apps/environments/15',
+ stop_url: '/root/acets-review-apps/environments/15/stop',
+ metrics_url: '/root/acets-review-apps/environments/15/deployments/1/metrics',
+ external_url: 'http://diplo.',
+ external_url_formatted: 'diplo.',
+ deployed_at: '2017-03-22T22:44:42.258Z',
+ deployed_at_formatted: 'Mar 22, 2017 10:44pm',
+ },
+];
+const createComponent = () => {
+ const Component = Vue.extend(deploymentComponent);
+ const mr = {
+ deployments: deploymentMockData,
+ };
+ const service = {};
+
+ return new Component({
+ el: document.createElement('div'),
+ propsData: { mr, service },
+ });
+};
+
+describe('MRWidgetDeployment', () => {
+ describe('props', () => {
+ it('should have props', () => {
+ const { mr, service } = deploymentComponent.props;
+
+ expect(mr.type instanceof Object).toBeTruthy();
+ expect(mr.required).toBeTruthy();
+
+ expect(service.type instanceof Object).toBeTruthy();
+ expect(service.required).toBeTruthy();
+ });
+ });
+
+ describe('computed', () => {
+ describe('svg', () => {
+ it('should have the proper SVG icon', () => {
+ const vm = createComponent(deploymentMockData);
+ expect(vm.svg).toEqual(statusIconEntityMap.icon_status_success);
+ });
+ });
+ });
+
+ describe('methods', () => {
+ let vm = createComponent();
+ const deployment = deploymentMockData[0];
+
+ describe('formatDate', () => {
+ it('should work', () => {
+ const readable = gl.utils.getTimeago().format(deployment.deployed_at);
+ expect(vm.formatDate(deployment.deployed_at)).toEqual(readable);
+ });
+ });
+
+ describe('hasExternalUrls', () => {
+ it('should return true', () => {
+ expect(vm.hasExternalUrls(deployment)).toBeTruthy();
+ });
+
+ it('should return false when there is not enough information', () => {
+ expect(vm.hasExternalUrls()).toBeFalsy();
+ expect(vm.hasExternalUrls({ external_url: 'Diplo' })).toBeFalsy();
+ expect(vm.hasExternalUrls({ external_url_formatted: 'Diplo' })).toBeFalsy();
+ });
+ });
+
+ describe('hasDeploymentTime', () => {
+ it('should return true', () => {
+ expect(vm.hasDeploymentTime(deployment)).toBeTruthy();
+ });
+
+ it('should return false when there is not enough information', () => {
+ expect(vm.hasDeploymentTime()).toBeFalsy();
+ expect(vm.hasDeploymentTime({ deployed_at: 'Diplo' })).toBeFalsy();
+ expect(vm.hasDeploymentTime({ deployed_at_formatted: 'Diplo' })).toBeFalsy();
+ });
+ });
+
+ describe('hasDeploymentMeta', () => {
+ it('should return true', () => {
+ expect(vm.hasDeploymentMeta(deployment)).toBeTruthy();
+ });
+
+ it('should return false when there is not enough information', () => {
+ expect(vm.hasDeploymentMeta()).toBeFalsy();
+ expect(vm.hasDeploymentMeta({ url: 'Diplo' })).toBeFalsy();
+ expect(vm.hasDeploymentMeta({ name: 'Diplo' })).toBeFalsy();
+ });
+ });
+
+ describe('stopEnvironment', () => {
+ const url = '/foo/bar';
+ const returnPromise = () => new Promise((resolve) => {
+ resolve({
+ json() {
+ return {
+ redirect_url: url,
+ };
+ },
+ });
+ });
+ const mockStopEnvironment = () => {
+ vm.stopEnvironment(deploymentMockData);
+ return vm;
+ };
+
+ it('should show a confirm dialog and call service.stopEnvironment when confirmed', (done) => {
+ spyOn(window, 'confirm').and.returnValue(true);
+ spyOn(MRWidgetService, 'stopEnvironment').and.returnValue(returnPromise(true));
+ spyOn(gl.utils, 'visitUrl').and.returnValue(true);
+ vm = mockStopEnvironment();
+
+ expect(window.confirm).toHaveBeenCalled();
+ expect(MRWidgetService.stopEnvironment).toHaveBeenCalledWith(deploymentMockData.stop_url);
+ setTimeout(() => {
+ expect(gl.utils.visitUrl).toHaveBeenCalledWith(url);
+ done();
+ }, 333);
+ });
+
+ it('should show a confirm dialog but should not work if the dialog is rejected', () => {
+ spyOn(window, 'confirm').and.returnValue(false);
+ spyOn(MRWidgetService, 'stopEnvironment').and.returnValue(returnPromise(false));
+ vm = mockStopEnvironment();
+
+ expect(window.confirm).toHaveBeenCalled();
+ expect(MRWidgetService.stopEnvironment).not.toHaveBeenCalled();
+ });
+ });
+ });
+
+ describe('template', () => {
+ let vm;
+ let el;
+ const [deployment] = deploymentMockData;
+
+ beforeEach(() => {
+ vm = createComponent(deploymentMockData);
+ el = vm.$el;
+ });
+
+ it('should render template elements correctly', () => {
+ expect(el.classList.contains('mr-widget-heading')).toBeTruthy();
+ expect(el.querySelector('.js-icon-link')).toBeDefined();
+ expect(el.querySelector('.js-deploy-meta').getAttribute('href')).toEqual(deployment.url);
+ expect(el.querySelector('.js-deploy-meta').innerText).toContain(deployment.name);
+ expect(el.querySelector('.js-deploy-url').getAttribute('href')).toEqual(deployment.external_url);
+ expect(el.querySelector('.js-deploy-url').innerText).toContain(deployment.external_url_formatted);
+ expect(el.querySelector('.js-deploy-time').innerText).toContain(vm.formatDate(deployment.deployed_at));
+ expect(el.querySelector('.js-mr-memory-usage')).toBeDefined();
+ expect(el.querySelector('button')).toBeDefined();
+ });
+
+ it('should list multiple deployments', (done) => {
+ vm.mr.deployments.push(deployment);
+ vm.mr.deployments.push(deployment);
+
+ Vue.nextTick(() => {
+ expect(el.querySelectorAll('.ci-widget').length).toEqual(3);
+ expect(el.querySelectorAll('.js-mr-memory-usage').length).toEqual(3);
+ done();
+ });
+ });
+
+ it('should not have some elements when there is not enough data', (done) => {
+ vm.mr.deployments = [{}];
+
+ Vue.nextTick(() => {
+ expect(el.querySelectorAll('.js-deploy-meta').length).toEqual(0);
+ expect(el.querySelectorAll('.js-deploy-url').length).toEqual(0);
+ expect(el.querySelectorAll('.js-deploy-time').length).toEqual(0);
+ expect(el.querySelectorAll('.js-mr-memory-usage').length).toEqual(0);
+ expect(el.querySelectorAll('.button').length).toEqual(0);
+ done();
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_header_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_header_spec.js
new file mode 100644
index 00000000000..7f3eea7d2e5
--- /dev/null
+++ b/spec/javascripts/vue_mr_widget/components/mr_widget_header_spec.js
@@ -0,0 +1,102 @@
+import Vue from 'vue';
+import headerComponent from '~/vue_merge_request_widget/components/mr_widget_header';
+
+const createComponent = (mr) => {
+ const Component = Vue.extend(headerComponent);
+ return new Component({
+ el: document.createElement('div'),
+ propsData: { mr },
+ });
+};
+
+describe('MRWidgetHeader', () => {
+ describe('props', () => {
+ it('should have props', () => {
+ const { mr } = headerComponent.props;
+
+ expect(mr.type instanceof Object).toBeTruthy();
+ expect(mr.required).toBeTruthy();
+ });
+ });
+
+ describe('computed', () => {
+ let vm;
+ beforeEach(() => {
+ vm = createComponent({
+ divergedCommitsCount: 12,
+ sourceBranch: 'mr-widget-refactor',
+ sourceBranchLink: '/foo/bar/mr-widget-refactor',
+ targetBranch: 'master',
+ });
+ });
+
+ it('shouldShowCommitsBehindText', () => {
+ expect(vm.shouldShowCommitsBehindText).toBeTruthy();
+
+ vm.mr.divergedCommitsCount = 0;
+ expect(vm.shouldShowCommitsBehindText).toBeFalsy();
+ });
+
+ it('commitsText', () => {
+ expect(vm.commitsText).toEqual('commits');
+
+ vm.mr.divergedCommitsCount = 1;
+ expect(vm.commitsText).toEqual('commit');
+ });
+ });
+
+ describe('template', () => {
+ let vm;
+ let el;
+ const sourceBranchPath = '/foo/bar/mr-widget-refactor';
+ const mr = {
+ divergedCommitsCount: 12,
+ sourceBranch: 'mr-widget-refactor',
+ sourceBranchLink: `<a href="${sourceBranchPath}">mr-widget-refactor</a>`,
+ targetBranchPath: 'foo/bar/commits-path',
+ targetBranch: 'master',
+ isOpen: true,
+ emailPatchesPath: '/mr/email-patches',
+ plainDiffPath: '/mr/plainDiffPath',
+ };
+
+ beforeEach(() => {
+ vm = createComponent(mr);
+ el = vm.$el;
+ });
+
+ it('should render template elements correctly', () => {
+ expect(el.classList.contains('mr-source-target')).toBeTruthy();
+ const sourceBranchLink = el.querySelectorAll('.label-branch')[0];
+ const targetBranchLink = el.querySelectorAll('.label-branch')[1];
+
+ expect(sourceBranchLink.textContent).toContain(mr.sourceBranch);
+ expect(targetBranchLink.textContent).toContain(mr.targetBranch);
+ expect(sourceBranchLink.querySelector('a').getAttribute('href')).toEqual(sourceBranchPath);
+ expect(targetBranchLink.querySelector('a').getAttribute('href')).toEqual(mr.targetBranchPath);
+ expect(el.querySelector('.diverged-commits-count').textContent).toContain('12 commits behind');
+
+ expect(el.textContent).toContain('Check out branch');
+ expect(el.querySelectorAll('.dropdown li a')[0].getAttribute('href')).toEqual(mr.emailPatchesPath);
+ expect(el.querySelectorAll('.dropdown li a')[1].getAttribute('href')).toEqual(mr.plainDiffPath);
+ });
+
+ it('should not have right action links if the MR state is not open', (done) => {
+ vm.mr.isOpen = false;
+ Vue.nextTick(() => {
+ expect(el.textContent).not.toContain('Check out branch');
+ expect(el.querySelectorAll('.dropdown li a').length).toEqual(0);
+ done();
+ });
+ });
+
+ it('should not render diverged commits count if the MR has no diverged commits', (done) => {
+ vm.mr.divergedCommitsCount = null;
+ Vue.nextTick(() => {
+ expect(el.textContent).not.toContain('commits behind');
+ expect(el.querySelectorAll('.diverged-commits-count').length).toEqual(0);
+ done();
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_memory_usage_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_memory_usage_spec.js
new file mode 100644
index 00000000000..da9dff18ada
--- /dev/null
+++ b/spec/javascripts/vue_mr_widget/components/mr_widget_memory_usage_spec.js
@@ -0,0 +1,184 @@
+import Vue from 'vue';
+import memoryUsageComponent from '~/vue_merge_request_widget/components/mr_widget_memory_usage';
+import MRWidgetService from '~/vue_merge_request_widget/services/mr_widget_service';
+
+const url = '/root/acets-review-apps/environments/15/deployments/1/metrics';
+
+const metricsMockData = {
+ success: true,
+ metrics: {
+ memory_values: [
+ {
+ metric: {},
+ values: [
+ [1493716685, '4.30859375'],
+ ],
+ },
+ ],
+ },
+ last_update: '2017-05-02T12:34:49.628Z',
+ deployment_time: 1493718485,
+};
+
+const createComponent = () => {
+ const Component = Vue.extend(memoryUsageComponent);
+
+ return new Component({
+ el: document.createElement('div'),
+ propsData: {
+ metricsUrl: url,
+ memoryMetrics: [],
+ deploymentTime: 0,
+ hasMetrics: false,
+ loadFailed: false,
+ loadingMetrics: true,
+ backOffRequestCounter: 0,
+ },
+ });
+};
+
+const messages = {
+ loadingMetrics: 'Loading deployment statistics.',
+ hasMetrics: 'Deployment memory usage:',
+ loadFailed: 'Failed to load deployment statistics.',
+ metricsUnavailable: 'Deployment statistics are not available currently.',
+};
+
+describe('MemoryUsage', () => {
+ let vm;
+ let el;
+
+ beforeEach(() => {
+ vm = createComponent();
+ el = vm.$el;
+ });
+
+ describe('props', () => {
+ it('should have props with defaults', () => {
+ const { metricsUrl } = memoryUsageComponent.props;
+ const MetricsUrlTypeClass = metricsUrl.type;
+
+ Vue.nextTick(() => {
+ expect(new MetricsUrlTypeClass() instanceof String).toBeTruthy();
+ expect(metricsUrl.required).toBeTruthy();
+ });
+ });
+ });
+
+ describe('data', () => {
+ it('should have default data', () => {
+ const data = memoryUsageComponent.data();
+
+ expect(Array.isArray(data.memoryMetrics)).toBeTruthy();
+ expect(data.memoryMetrics.length).toBe(0);
+
+ expect(typeof data.deploymentTime).toBe('number');
+ expect(data.deploymentTime).toBe(0);
+
+ expect(typeof data.hasMetrics).toBe('boolean');
+ expect(data.hasMetrics).toBeFalsy();
+
+ expect(typeof data.loadFailed).toBe('boolean');
+ expect(data.loadFailed).toBeFalsy();
+
+ expect(typeof data.loadingMetrics).toBe('boolean');
+ expect(data.loadingMetrics).toBeTruthy();
+
+ expect(typeof data.backOffRequestCounter).toBe('number');
+ expect(data.backOffRequestCounter).toBe(0);
+ });
+ });
+
+ describe('methods', () => {
+ const { metrics, deployment_time } = metricsMockData;
+
+ describe('computeGraphData', () => {
+ it('should populate sparkline graph', () => {
+ vm.computeGraphData(metrics, deployment_time);
+ const { hasMetrics, memoryMetrics, deploymentTime } = vm;
+
+ expect(hasMetrics).toBeTruthy();
+ expect(memoryMetrics.length > 0).toBeTruthy();
+ expect(deploymentTime).toEqual(deployment_time);
+ });
+ });
+
+ describe('loadMetrics', () => {
+ const returnServicePromise = () => new Promise((resolve) => {
+ resolve({
+ json() {
+ return metricsMockData;
+ },
+ });
+ });
+
+ it('should load metrics data using MRWidgetService', (done) => {
+ spyOn(MRWidgetService, 'fetchMetrics').and.returnValue(returnServicePromise(true));
+ spyOn(vm, 'computeGraphData');
+
+ vm.loadMetrics();
+ setTimeout(() => {
+ expect(MRWidgetService.fetchMetrics).toHaveBeenCalledWith(url);
+ expect(vm.computeGraphData).toHaveBeenCalledWith(metrics, deployment_time);
+ done();
+ }, 333);
+ });
+ });
+ });
+
+ describe('template', () => {
+ it('should render template elements correctly', () => {
+ expect(el.classList.contains('mr-memory-usage')).toBeTruthy();
+ expect(el.querySelector('.js-usage-info')).toBeDefined();
+ });
+
+ it('should show loading metrics message while metrics are being loaded', (done) => {
+ vm.loadingMetrics = true;
+ vm.hasMetrics = false;
+ vm.loadFailed = false;
+
+ Vue.nextTick(() => {
+ expect(el.querySelector('.js-usage-info.usage-info-loading')).toBeDefined();
+ expect(el.querySelector('.js-usage-info .usage-info-load-spinner')).toBeDefined();
+ expect(el.querySelector('.js-usage-info').innerText).toContain(messages.loadingMetrics);
+ done();
+ });
+ });
+
+ it('should show deployment memory usage when metrics are loaded', (done) => {
+ vm.loadingMetrics = false;
+ vm.hasMetrics = true;
+ vm.loadFailed = false;
+
+ Vue.nextTick(() => {
+ expect(el.querySelector('.memory-graph-container')).toBeDefined();
+ expect(el.querySelector('.js-usage-info').innerText).toContain(messages.hasMetrics);
+ done();
+ });
+ });
+
+ it('should show failure message when metrics loading failed', (done) => {
+ vm.loadingMetrics = false;
+ vm.hasMetrics = false;
+ vm.loadFailed = true;
+
+ Vue.nextTick(() => {
+ expect(el.querySelector('.js-usage-info.usage-info-failed')).toBeDefined();
+ expect(el.querySelector('.js-usage-info').innerText).toContain(messages.loadFailed);
+ done();
+ });
+ });
+
+ it('should show metrics unavailable message when metrics loading failed', (done) => {
+ vm.loadingMetrics = false;
+ vm.hasMetrics = false;
+ vm.loadFailed = false;
+
+ Vue.nextTick(() => {
+ expect(el.querySelector('.js-usage-info.usage-info-unavailable')).toBeDefined();
+ expect(el.querySelector('.js-usage-info').innerText).toContain(messages.metricsUnavailable);
+ done();
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_merge_help_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_merge_help_spec.js
new file mode 100644
index 00000000000..4da4fc82c26
--- /dev/null
+++ b/spec/javascripts/vue_mr_widget/components/mr_widget_merge_help_spec.js
@@ -0,0 +1,51 @@
+import Vue from 'vue';
+import mergeHelpComponent from '~/vue_merge_request_widget/components/mr_widget_merge_help';
+
+const props = {
+ missingBranch: 'this-is-not-the-branch-you-are-looking-for',
+};
+const text = `If the ${props.missingBranch} branch exists in your local repository`;
+
+const createComponent = () => {
+ const Component = Vue.extend(mergeHelpComponent);
+ return new Component({
+ el: document.createElement('div'),
+ propsData: props,
+ });
+};
+
+describe('MRWidgetMergeHelp', () => {
+ describe('props', () => {
+ it('should have props', () => {
+ const { missingBranch } = mergeHelpComponent.props;
+ const MissingBranchTypeClass = missingBranch.type;
+
+ expect(new MissingBranchTypeClass() instanceof String).toBeTruthy();
+ expect(missingBranch.required).toBeFalsy();
+ expect(missingBranch.default).toEqual('');
+ });
+ });
+
+ describe('template', () => {
+ let vm;
+ let el;
+
+ beforeEach(() => {
+ vm = createComponent();
+ el = vm.$el;
+ });
+
+ it('should have the correct elements', () => {
+ expect(el.classList.contains('mr-widget-help')).toBeTruthy();
+ expect(el.textContent).toContain(text);
+ });
+
+ it('should not show missing branch name if missingBranch props is not provided', (done) => {
+ vm.missingBranch = null;
+ Vue.nextTick(() => {
+ expect(el.textContent).not.toContain(text);
+ done();
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js
new file mode 100644
index 00000000000..647b59520f8
--- /dev/null
+++ b/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js
@@ -0,0 +1,131 @@
+import Vue from 'vue';
+import { statusIconEntityMap } from '~/vue_shared/ci_status_icons';
+import pipelineComponent from '~/vue_merge_request_widget/components/mr_widget_pipeline';
+import mockData from '../mock_data';
+
+const createComponent = (mr) => {
+ const Component = Vue.extend(pipelineComponent);
+ return new Component({
+ el: document.createElement('div'),
+ propsData: { mr },
+ });
+};
+
+describe('MRWidgetPipeline', () => {
+ describe('props', () => {
+ it('should have props', () => {
+ const { mr } = pipelineComponent.props;
+
+ expect(mr.type instanceof Object).toBeTruthy();
+ expect(mr.required).toBeTruthy();
+ });
+ });
+
+ describe('components', () => {
+ it('should have components added', () => {
+ expect(pipelineComponent.components['pipeline-stage']).toBeDefined();
+ expect(pipelineComponent.components.ciIcon).toBeDefined();
+ });
+ });
+
+ describe('computed', () => {
+ describe('svg', () => {
+ it('should have the proper SVG icon', () => {
+ const vm = createComponent({ pipeline: mockData.pipeline });
+
+ expect(vm.svg).toEqual(statusIconEntityMap.icon_status_failed);
+ });
+ });
+
+ describe('hasCIError', () => {
+ it('should return false when there is no CI error', () => {
+ const vm = createComponent({
+ pipeline: mockData.pipeline,
+ hasCI: true,
+ ciStatus: 'success',
+ });
+
+ expect(vm.hasCIError).toBeFalsy();
+ });
+
+ it('should return true when there is a CI error', () => {
+ const vm = createComponent({
+ pipeline: mockData.pipeline,
+ hasCI: true,
+ ciStatus: null,
+ });
+
+ expect(vm.hasCIError).toBeTruthy();
+ });
+ });
+ });
+
+ describe('template', () => {
+ let vm;
+ let el;
+ const { pipeline } = mockData;
+ const mr = {
+ hasCI: true,
+ ciStatus: 'success',
+ pipelineDetailedStatus: pipeline.details.status,
+ pipeline,
+ };
+
+ beforeEach(() => {
+ vm = createComponent(mr);
+ el = vm.$el;
+ });
+
+ it('should render template elements correctly', () => {
+ expect(el.classList.contains('mr-widget-heading')).toBeTruthy();
+ expect(el.querySelectorAll('.ci-status-icon.ci-status-icon-success').length).toEqual(1);
+ expect(el.querySelector('.pipeline-id').textContent).toContain(`#${pipeline.id}`);
+ expect(el.innerText).toContain('passed');
+ expect(el.innerText).toContain('with stages');
+ expect(el.querySelector('.pipeline-id').getAttribute('href')).toEqual(pipeline.path);
+ expect(el.querySelectorAll('.stage-container').length).toEqual(2);
+ expect(el.querySelector('.js-ci-error')).toEqual(null);
+ expect(el.querySelector('.js-commit-link').getAttribute('href')).toEqual(pipeline.commit.commit_path);
+ expect(el.querySelector('.js-commit-link').textContent).toContain(pipeline.commit.short_id);
+ expect(el.querySelector('.js-mr-coverage').textContent).toContain(`Coverage ${pipeline.coverage}%.`);
+ });
+
+ it('should list single stage', (done) => {
+ pipeline.details.stages.splice(0, 1);
+
+ Vue.nextTick(() => {
+ expect(el.querySelectorAll('.stage-container button').length).toEqual(1);
+ expect(el.innerText).toContain('with stage');
+ done();
+ });
+ });
+
+ it('should not have stages when there is no stage', (done) => {
+ vm.mr.pipeline.details.stages = [];
+
+ Vue.nextTick(() => {
+ expect(el.querySelectorAll('.stage-container button').length).toEqual(0);
+ done();
+ });
+ });
+
+ it('should not have coverage text when pipeline has no coverage info', (done) => {
+ vm.mr.pipeline.coverage = null;
+
+ Vue.nextTick(() => {
+ expect(el.querySelector('.js-mr-coverage')).toEqual(null);
+ done();
+ });
+ });
+
+ it('should show CI error when there is a CI error', (done) => {
+ vm.mr.ciStatus = null;
+
+ Vue.nextTick(() => {
+ expect(el.querySelectorAll('.js-ci-error').length).toEqual(1);
+ expect(el.innerText).toContain('Could not connect to the CI server');
+ done();
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_related_links_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_related_links_spec.js
new file mode 100644
index 00000000000..f6e0c3dfb74
--- /dev/null
+++ b/spec/javascripts/vue_mr_widget/components/mr_widget_related_links_spec.js
@@ -0,0 +1,138 @@
+import Vue from 'vue';
+import relatedLinksComponent from '~/vue_merge_request_widget/components/mr_widget_related_links';
+
+const createComponent = (data) => {
+ const Component = Vue.extend(relatedLinksComponent);
+
+ return new Component({
+ el: document.createElement('div'),
+ propsData: data,
+ });
+};
+
+describe('MRWidgetRelatedLinks', () => {
+ describe('props', () => {
+ it('should have props', () => {
+ const { relatedLinks } = relatedLinksComponent.props;
+
+ expect(relatedLinks).toBeDefined();
+ expect(relatedLinks.type instanceof Object).toBeTruthy();
+ expect(relatedLinks.required).toBeTruthy();
+ });
+ });
+
+ describe('computed', () => {
+ describe('hasLinks', () => {
+ it('should return correct value when we have links reference', () => {
+ const data = {
+ relatedLinks: {
+ closing: '/foo',
+ mentioned: '/foo',
+ assignToMe: '/foo',
+ },
+ };
+ const vm = createComponent(data);
+ expect(vm.hasLinks).toBeTruthy();
+
+ vm.relatedLinks.closing = null;
+ expect(vm.hasLinks).toBeTruthy();
+
+ vm.relatedLinks.mentioned = null;
+ expect(vm.hasLinks).toBeTruthy();
+
+ vm.relatedLinks.assignToMe = null;
+ expect(vm.hasLinks).toBeFalsy();
+ });
+ });
+ });
+
+ describe('methods', () => {
+ const data = {
+ relatedLinks: {
+ closing: '<a href="#">#23</a> and <a>#42</a>',
+ mentioned: '<a href="#">#7</a>',
+ },
+ };
+ const vm = createComponent(data);
+
+ describe('hasMultipleIssues', () => {
+ it('should return true if the given text has multiple issues', () => {
+ expect(vm.hasMultipleIssues(data.relatedLinks.closing)).toBeTruthy();
+ });
+
+ it('should return false if the given text has one issue', () => {
+ expect(vm.hasMultipleIssues(data.relatedLinks.mentioned)).toBeFalsy();
+ });
+ });
+
+ describe('issueLabel', () => {
+ it('should return true if the given text has multiple issues', () => {
+ expect(vm.issueLabel('closing')).toEqual('issues');
+ });
+
+ it('should return false if the given text has one issue', () => {
+ expect(vm.issueLabel('mentioned')).toEqual('issue');
+ });
+ });
+
+ describe('verbLabel', () => {
+ it('should return true if the given text has multiple issues', () => {
+ expect(vm.verbLabel('closing')).toEqual('are');
+ });
+
+ it('should return false if the given text has one issue', () => {
+ expect(vm.verbLabel('mentioned')).toEqual('is');
+ });
+ });
+ });
+
+ describe('template', () => {
+ it('should have only have closing issues text', () => {
+ const vm = createComponent({
+ relatedLinks: {
+ closing: '<a href="#">#23</a> and <a>#42</a>',
+ },
+ });
+ const content = vm.$el.textContent.replace(/\n(\s)+/g, ' ').trim();
+
+ expect(content).toContain('Closes issues #23 and #42');
+ expect(content).not.toContain('mentioned');
+ });
+
+ it('should have only have mentioned issues text', () => {
+ const vm = createComponent({
+ relatedLinks: {
+ mentioned: '<a href="#">#7</a>',
+ },
+ });
+
+ expect(vm.$el.innerText).toContain('issue #7');
+ expect(vm.$el.innerText).toContain('is mentioned but will not be closed.');
+ expect(vm.$el.innerText).not.toContain('Closes');
+ });
+
+ it('should have closing and mentioned issues at the same time', () => {
+ const vm = createComponent({
+ relatedLinks: {
+ closing: '<a href="#">#7</a>',
+ mentioned: '<a href="#">#23</a> and <a>#42</a>',
+ },
+ });
+ const content = vm.$el.textContent.replace(/\n(\s)+/g, ' ').trim();
+
+ expect(content).toContain('Closes issue #7.');
+ expect(content).toContain('issues #23 and #42');
+ expect(content).toContain('are mentioned but will not be closed.');
+ });
+
+ it('should have assing issues link', () => {
+ const vm = createComponent({
+ relatedLinks: {
+ assignToMe: '<a href="#">Assign yourself to these issues</a>',
+ },
+ });
+
+ expect(vm.$el.innerText).toContain('Assign yourself to these issues');
+ });
+ });
+});
diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_archived_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_archived_spec.js
new file mode 100644
index 00000000000..cac2f561a0b
--- /dev/null
+++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_archived_spec.js
@@ -0,0 +1,18 @@
+import Vue from 'vue';
+import archivedComponent from '~/vue_merge_request_widget/components/states/mr_widget_archived';
+
+describe('MRWidgetArchived', () => {
+ describe('template', () => {
+ it('should have correct elements', () => {
+ const Component = Vue.extend(archivedComponent);
+ const el = new Component({
+ el: document.createElement('div'),
+ }).$el;
+
+ expect(el.classList.contains('mr-widget-body')).toBeTruthy();
+ expect(el.querySelector('button').classList.contains('btn-success')).toBeTruthy();
+ expect(el.querySelector('button').disabled).toBeTruthy();
+ expect(el.innerText).toContain('This project is archived, write access has been disabled.');
+ });
+ });
+});
diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_auto_merge_failed_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_auto_merge_failed_spec.js
new file mode 100644
index 00000000000..47b4ba893e0
--- /dev/null
+++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_auto_merge_failed_spec.js
@@ -0,0 +1,32 @@
+import Vue from 'vue';
+import autoMergeFailedComponent from '~/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed';
+
+const mergeError = 'This is the merge error';
+
+describe('MRWidgetAutoMergeFailed', () => {
+ describe('props', () => {
+ it('should have props', () => {
+ const mrProp = autoMergeFailedComponent.props.mr;
+
+ expect(mrProp.type instanceof Object).toBeTruthy();
+ expect(mrProp.required).toBeTruthy();
+ });
+ });
+
+ describe('template', () => {
+ const Component = Vue.extend(autoMergeFailedComponent);
+ const vm = new Component({
+ el: document.createElement('div'),
+ propsData: {
+ mr: { mergeError },
+ },
+ });
+
+ it('should have correct elements', () => {
+ expect(vm.$el.classList.contains('mr-widget-body')).toBeTruthy();
+ expect(vm.$el.querySelector('button').getAttribute('disabled')).toBeTruthy();
+ expect(vm.$el.innerText).toContain('This merge request failed to be merged automatically.');
+ expect(vm.$el.innerText).toContain(mergeError);
+ });
+ });
+});
diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_checking_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_checking_spec.js
new file mode 100644
index 00000000000..3be11d47227
--- /dev/null
+++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_checking_spec.js
@@ -0,0 +1,19 @@
+import Vue from 'vue';
+import checkingComponent from '~/vue_merge_request_widget/components/states/mr_widget_checking';
+
+describe('MRWidgetChecking', () => {
+ describe('template', () => {
+ it('should have correct elements', () => {
+ const Component = Vue.extend(checkingComponent);
+ const el = new Component({
+ el: document.createElement('div'),
+ }).$el;
+
+ expect(el.classList.contains('mr-widget-body')).toBeTruthy();
+ expect(el.querySelector('button').classList.contains('btn-success')).toBeTruthy();
+ expect(el.querySelector('button').disabled).toBeTruthy();
+ expect(el.innerText).toContain('Checking ability to merge automatically.');
+ expect(el.querySelector('i')).toBeDefined();
+ });
+ });
+});
diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_closed_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_closed_spec.js
new file mode 100644
index 00000000000..47303d1e80f
--- /dev/null
+++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_closed_spec.js
@@ -0,0 +1,51 @@
+import Vue from 'vue';
+import closedComponent from '~/vue_merge_request_widget/components/states/mr_widget_closed';
+
+const mr = {
+ targetBranch: 'good-branch',
+ targetBranchPath: '/good-branch',
+ closedBy: {
+ name: 'Fatih Acet',
+ username: 'fatihacet',
+ },
+ updatedAt: '2017-03-23T20:08:08.845Z',
+ closedAt: '1 day ago',
+};
+
+const createComponent = () => {
+ const Component = Vue.extend(closedComponent);
+
+ return new Component({
+ el: document.createElement('div'),
+ propsData: { mr },
+ }).$el;
+};
+
+describe('MRWidgetClosed', () => {
+ describe('props', () => {
+ it('should have props', () => {
+ const mrProp = closedComponent.props.mr;
+
+ expect(mrProp.type instanceof Object).toBeTruthy();
+ expect(mrProp.required).toBeTruthy();
+ });
+ });
+
+ describe('components', () => {
+ it('should have components added', () => {
+ expect(closedComponent.components['mr-widget-author-and-time']).toBeDefined();
+ });
+ });
+
+ describe('template', () => {
+ it('should have correct elements', () => {
+ const el = createComponent();
+
+ expect(el.querySelector('h4').textContent).toContain('Closed by');
+ expect(el.querySelector('h4').textContent).toContain(mr.closedBy.name);
+ expect(el.textContent).toContain('The changes were not merged into');
+ expect(el.querySelector('.label-branch').getAttribute('href')).toEqual(mr.targetBranchPath);
+ expect(el.querySelector('.label-branch').textContent).toContain(mr.targetBranch);
+ });
+ });
+});
diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_conflicts_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_conflicts_spec.js
new file mode 100644
index 00000000000..e7ae85caec4
--- /dev/null
+++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_conflicts_spec.js
@@ -0,0 +1,69 @@
+import Vue from 'vue';
+import conflictsComponent from '~/vue_merge_request_widget/components/states/mr_widget_conflicts';
+
+const path = '/conflicts';
+const createComponent = () => {
+ const Component = Vue.extend(conflictsComponent);
+
+ return new Component({
+ el: document.createElement('div'),
+ propsData: {
+ mr: {
+ canMerge: true,
+ conflictResolutionPath: path,
+ },
+ },
+ });
+};
+
+describe('MRWidgetConflicts', () => {
+ describe('props', () => {
+ it('should have props', () => {
+ const { mr } = conflictsComponent.props;
+
+ expect(mr.type instanceof Object).toBeTruthy();
+ expect(mr.required).toBeTruthy();
+ });
+ });
+
+ describe('template', () => {
+ it('should have correct elements', () => {
+ const el = createComponent().$el;
+ const resolveButton = el.querySelectorAll('.btn-group .btn')[0];
+ const mergeLocallyButton = el.querySelectorAll('.btn-group .btn')[1];
+
+ expect(el.textContent).toContain('There are merge conflicts.');
+ expect(el.textContent).not.toContain('ask someone with write access');
+ expect(el.querySelector('.btn-success').disabled).toBeTruthy();
+ expect(el.querySelectorAll('.btn-group .btn').length).toBe(2);
+ expect(resolveButton.textContent).toContain('Resolve conflicts');
+ expect(resolveButton.getAttribute('href')).toEqual(path);
+ expect(mergeLocallyButton.textContent).toContain('Merge locally');
+ });
+
+ describe('when user does not have permission to merge', () => {
+ let vm;
+
+ beforeEach(() => {
+ vm = createComponent();
+ vm.mr.canMerge = false;
+ });
+
+ it('should show proper message', (done) => {
+ Vue.nextTick(() => {
+ expect(vm.$el.textContent).toContain('ask someone with write access');
+ done();
+ });
+ });
+
+ it('should not have action buttons', (done) => {
+ Vue.nextTick(() => {
+ expect(vm.$el.querySelectorAll('.btn').length).toBe(1);
+ expect(vm.$el.querySelector('a.js-resolve-conflicts-button')).toEqual(null);
+ expect(vm.$el.querySelector('a.js-merge-locally-button')).toEqual(null);
+ done();
+ });
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_failed_to_merge_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_failed_to_merge_spec.js
new file mode 100644
index 00000000000..587b83430d9
--- /dev/null
+++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_failed_to_merge_spec.js
@@ -0,0 +1,122 @@
+import Vue from 'vue';
+import failedToMergeComponent from '~/vue_merge_request_widget/components/states/mr_widget_failed_to_merge';
+import eventHub from '~/vue_merge_request_widget/event_hub';
+
+const mr = {
+ mergeError: 'Merge error happened.',
+};
+const createComponent = () => {
+ const Component = Vue.extend(failedToMergeComponent);
+ return new Component({
+ el: document.createElement('div'),
+ propsData: { mr },
+ });
+};
+
+describe('MRWidgetFailedToMerge', () => {
+ describe('data', () => {
+ it('should have default data', () => {
+ const data = failedToMergeComponent.data();
+
+ expect(data.timer).toEqual(10);
+ expect(data.isRefreshing).toBeFalsy();
+ });
+ });
+
+ describe('computed', () => {
+ describe('timerText', () => {
+ it('should return correct timer text', () => {
+ const vm = createComponent();
+ expect(vm.timerText).toEqual('10 seconds');
+
+ vm.timer = 1;
+ expect(vm.timerText).toEqual('a second');
+ });
+ });
+ });
+
+ describe('created', () => {
+ it('should disable polling', () => {
+ spyOn(eventHub, '$emit');
+ createComponent();
+
+ expect(eventHub.$emit).toHaveBeenCalledWith('DisablePolling');
+ });
+ });
+
+ describe('methods', () => {
+ describe('refresh', () => {
+ it('should emit event to request component refresh', () => {
+ spyOn(eventHub, '$emit');
+ const vm = createComponent();
+
+ expect(vm.isRefreshing).toBeFalsy();
+
+ vm.refresh();
+ expect(vm.isRefreshing).toBeTruthy();
+ expect(eventHub.$emit).toHaveBeenCalledWith('MRWidgetUpdateRequested');
+ expect(eventHub.$emit).toHaveBeenCalledWith('EnablePolling');
+ });
+ });
+
+ describe('updateTimer', () => {
+ it('should update timer and emit event when timer end', () => {
+ const vm = createComponent();
+ spyOn(vm, 'refresh');
+
+ expect(vm.timer).toEqual(10);
+
+ for (let i = 0; i < 10; i++) { // eslint-disable-line
+ expect(vm.timer).toEqual(10 - i);
+ vm.updateTimer();
+ }
+
+ expect(vm.refresh).toHaveBeenCalled();
+ });
+ });
+ });
+
+ describe('template', () => {
+ let vm;
+ let el;
+
+ beforeEach(() => {
+ vm = createComponent();
+ el = vm.$el;
+ });
+
+ it('should have correct elements', (done) => {
+ expect(el.classList.contains('mr-widget-body')).toBeTruthy();
+ expect(el.innerText).toContain('Merge error happened.');
+ expect(el.innerText).toContain('Refreshing in 10 seconds');
+ expect(el.innerText).not.toContain('Merge failed.');
+ expect(el.querySelector('button').getAttribute('disabled')).toBeTruthy();
+ expect(el.querySelector('button').innerText).toContain('Merge');
+ expect(el.querySelector('.js-refresh-button').innerText).toContain('Refresh now');
+ expect(el.querySelector('.js-refresh-label')).toEqual(null);
+ expect(el.innerText).not.toContain('Refreshing now...');
+ setTimeout(() => {
+ expect(el.innerText).toContain('Refreshing in 9 seconds');
+ done();
+ }, 1010);
+ });
+
+ it('should just generic merge failed message if merge_error is not available', (done) => {
+ vm.mr.mergeError = null;
+
+ Vue.nextTick(() => {
+ expect(el.innerText).toContain('Merge failed.');
+ expect(el.innerText).not.toContain('Merge error happened.');
+ done();
+ });
+ });
+
+ it('should show refresh label when refresh requested', () => {
+ vm.refresh();
+ Vue.nextTick(() => {
+ expect(el.innerText).not.toContain('Merge failed. Refreshing');
+ expect(el.innerText).toContain('Refreshing now...');
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_locked_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_locked_spec.js
new file mode 100644
index 00000000000..fb2ef606604
--- /dev/null
+++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_locked_spec.js
@@ -0,0 +1,33 @@
+import Vue from 'vue';
+import lockedComponent from '~/vue_merge_request_widget/components/states/mr_widget_locked';
+
+describe('MRWidgetLocked', () => {
+ describe('props', () => {
+ it('should have props', () => {
+ const { mr } = lockedComponent.props;
+
+ expect(mr.type instanceof Object).toBeTruthy();
+ expect(mr.required).toBeTruthy();
+ });
+ });
+
+ describe('template', () => {
+ it('should have correct elements', () => {
+ const Component = Vue.extend(lockedComponent);
+ const mr = {
+ targetBranchPath: '/branch-path',
+ targetBranch: 'branch',
+ };
+ const el = new Component({
+ el: document.createElement('div'),
+ propsData: { mr },
+ }).$el;
+
+ expect(el.classList.contains('mr-widget-body')).toBeTruthy();
+ expect(el.innerText).toContain('it is locked');
+ expect(el.innerText).toContain('changes will be merged into');
+ expect(el.querySelector('.label-branch a').getAttribute('href')).toEqual(mr.targetBranchPath);
+ expect(el.querySelector('.label-branch a').textContent).toContain(mr.targetBranch);
+ });
+ });
+});
diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_merge_when_pipeline_succeeds_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_merge_when_pipeline_succeeds_spec.js
new file mode 100644
index 00000000000..8d8b90cea16
--- /dev/null
+++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_merge_when_pipeline_succeeds_spec.js
@@ -0,0 +1,213 @@
+import Vue from 'vue';
+import mwpsComponent from '~/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds';
+import eventHub from '~/vue_merge_request_widget/event_hub';
+
+const targetBranchPath = '/foo/bar';
+const targetBranch = 'foo';
+const sha = '1EA2EZ34';
+
+const createComponent = () => {
+ const Component = Vue.extend(mwpsComponent);
+ const mr = {
+ shouldRemoveSourceBranch: false,
+ canRemoveSourceBranch: true,
+ canCancelAutomaticMerge: true,
+ mergeUserId: 1,
+ currentUserId: 1,
+ setToMWPSBy: {},
+ sha,
+ targetBranchPath,
+ targetBranch,
+ };
+
+ const service = {
+ cancelAutomaticMerge() {},
+ mergeResource: {
+ save() {},
+ },
+ };
+
+ return new Component({
+ el: document.createElement('div'),
+ propsData: { mr, service },
+ });
+};
+
+describe('MRWidgetMergeWhenPipelineSucceeds', () => {
+ describe('props', () => {
+ it('should have props', () => {
+ const { mr, service } = mwpsComponent.props;
+
+ expect(mr.type instanceof Object).toBeTruthy();
+ expect(mr.required).toBeTruthy();
+
+ expect(service.type instanceof Object).toBeTruthy();
+ expect(service.required).toBeTruthy();
+ });
+ });
+
+ describe('components', () => {
+ it('should have components added', () => {
+ expect(mwpsComponent.components['mr-widget-author']).toBeDefined();
+ });
+ });
+
+ describe('data', () => {
+ it('should have default data', () => {
+ const data = mwpsComponent.data();
+
+ expect(data.isCancellingAutoMerge).toBeFalsy();
+ expect(data.isRemovingSourceBranch).toBeFalsy();
+ });
+ });
+
+ describe('computed', () => {
+ describe('canRemoveSourceBranch', () => {
+ it('should return true when user is able to remove source branch', () => {
+ const vm = createComponent();
+
+ expect(vm.canRemoveSourceBranch).toBeTruthy();
+ });
+
+ it('should return false when user id is not the same with who set the MWPS', () => {
+ const vm = createComponent();
+
+ vm.mr.mergeUserId = 2;
+ expect(vm.canRemoveSourceBranch).toBeFalsy();
+
+ vm.mr.currentUserId = 2;
+ expect(vm.canRemoveSourceBranch).toBeTruthy();
+
+ vm.mr.currentUserId = 3;
+ expect(vm.canRemoveSourceBranch).toBeFalsy();
+ });
+
+ it('should return false when shouldRemoveSourceBranch set to false', () => {
+ const vm = createComponent();
+
+ vm.mr.shouldRemoveSourceBranch = true;
+ expect(vm.canRemoveSourceBranch).toBeFalsy();
+ });
+
+ it('should return false if user is not able to remove the source branch', () => {
+ const vm = createComponent();
+
+ vm.mr.canRemoveSourceBranch = false;
+ expect(vm.canRemoveSourceBranch).toBeFalsy();
+ });
+ });
+ });
+
+ describe('methods', () => {
+ describe('cancelAutomaticMerge', () => {
+ it('should set flag and call service then tell main component to update the widget with data', (done) => {
+ const vm = createComponent();
+ const mrObj = {
+ is_new_mr_data: true,
+ };
+ spyOn(eventHub, '$emit');
+ spyOn(vm.service, 'cancelAutomaticMerge').and.returnValue(new Promise((resolve) => {
+ resolve({
+ json() {
+ return mrObj;
+ },
+ });
+ }));
+
+ vm.cancelAutomaticMerge();
+ setTimeout(() => {
+ expect(vm.isCancellingAutoMerge).toBeTruthy();
+ expect(eventHub.$emit).toHaveBeenCalledWith('UpdateWidgetData', mrObj);
+ done();
+ }, 333);
+ });
+ });
+
+ describe('removeSourceBranch', () => {
+ it('should set flag and call service then request main component to update the widget', (done) => {
+ const vm = createComponent();
+ spyOn(eventHub, '$emit');
+ spyOn(vm.service.mergeResource, 'save').and.returnValue(new Promise((resolve) => {
+ resolve({
+ json() {
+ return {
+ status: 'merge_when_pipeline_succeeds',
+ };
+ },
+ });
+ }));
+
+ vm.removeSourceBranch();
+ setTimeout(() => {
+ expect(eventHub.$emit).toHaveBeenCalledWith('MRWidgetUpdateRequested');
+ expect(vm.service.mergeResource.save).toHaveBeenCalledWith({
+ sha,
+ merge_when_pipeline_succeeds: true,
+ should_remove_source_branch: true,
+ });
+ done();
+ }, 333);
+ });
+ });
+ });
+
+ describe('template', () => {
+ let vm;
+ let el;
+
+ beforeEach(() => {
+ vm = createComponent();
+ el = vm.$el;
+ });
+
+ it('should have correct elements', () => {
+ expect(el.classList.contains('mr-widget-body')).toBeTruthy();
+ expect(el.innerText).toContain('to be merged automatically when the pipeline succeeds.');
+ expect(el.innerText).toContain('The changes will be merged into');
+ expect(el.innerText).toContain(targetBranch);
+ expect(el.innerText).toContain('The source branch will not be removed.');
+ expect(el.querySelector('.js-cancel-auto-merge').innerText).toContain('Cancel automatic merge');
+ expect(el.querySelector('.js-cancel-auto-merge').getAttribute('disabled')).toBeFalsy();
+ expect(el.querySelector('.js-remove-source-branch').innerText).toContain('Remove source branch');
+ expect(el.querySelector('.js-remove-source-branch').getAttribute('disabled')).toBeFalsy();
+ });
+
+ it('should disable cancel auto merge button when the action is in progress', (done) => {
+ vm.isCancellingAutoMerge = true;
+
+ Vue.nextTick(() => {
+ expect(el.querySelector('.js-cancel-auto-merge').getAttribute('disabled')).toBeTruthy();
+ done();
+ });
+ });
+
+ it('should show source branch will be removed text when it source branch set to remove', (done) => {
+ vm.mr.shouldRemoveSourceBranch = true;
+
+ Vue.nextTick(() => {
+ const normalizedText = el.innerText.replace(/\s+/g, ' ');
+ expect(normalizedText).toContain('The source branch will be removed.');
+ expect(normalizedText).not.toContain('The source branch will not be removed.');
+ done();
+ });
+ });
+
+ it('should not show remove source branch button when user not able to remove source branch', (done) => {
+ vm.mr.currentUserId = 4;
+
+ Vue.nextTick(() => {
+ expect(el.querySelector('.js-remove-source-branch')).toEqual(null);
+ done();
+ });
+ });
+
+ it('should disable remove source branch button when the action is in progress', (done) => {
+ vm.isRemovingSourceBranch = true;
+
+ Vue.nextTick(() => {
+ expect(el.querySelector('.js-remove-source-branch').getAttribute('disabled')).toBeTruthy();
+ done();
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_merged_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_merged_spec.js
new file mode 100644
index 00000000000..6628010112d
--- /dev/null
+++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_merged_spec.js
@@ -0,0 +1,174 @@
+import Vue from 'vue';
+import mergedComponent from '~/vue_merge_request_widget/components/states/mr_widget_merged';
+import eventHub from '~/vue_merge_request_widget/event_hub';
+
+const targetBranch = 'foo';
+
+const createComponent = () => {
+ const Component = Vue.extend(mergedComponent);
+ const mr = {
+ isRemovingSourceBranch: false,
+ cherryPickInForkPath: false,
+ canCherryPickInCurrentMR: true,
+ revertInForkPath: false,
+ canRevertInCurrentMR: true,
+ canRemoveSourceBranch: true,
+ sourceBranchRemoved: true,
+ mergedBy: {},
+ mergedAt: '',
+ updatedAt: '',
+ targetBranch,
+ };
+
+ const service = {
+ removeSourceBranch() {},
+ };
+
+ return new Component({
+ el: document.createElement('div'),
+ propsData: { mr, service },
+ });
+};
+
+describe('MRWidgetMerged', () => {
+ describe('props', () => {
+ it('should have props', () => {
+ const { mr, service } = mergedComponent.props;
+
+ expect(mr.type instanceof Object).toBeTruthy();
+ expect(mr.required).toBeTruthy();
+
+ expect(service.type instanceof Object).toBeTruthy();
+ expect(service.required).toBeTruthy();
+ });
+ });
+
+ describe('components', () => {
+ it('should have components added', () => {
+ expect(mergedComponent.components['mr-widget-author-and-time']).toBeDefined();
+ });
+ });
+
+ describe('data', () => {
+ it('should have default data', () => {
+ const data = mergedComponent.data();
+
+ expect(data.isMakingRequest).toBeFalsy();
+ });
+ });
+
+ describe('computed', () => {
+ describe('shouldShowRemoveSourceBranch', () => {
+ it('should correct value when fields changed', () => {
+ const vm = createComponent();
+ vm.mr.sourceBranchRemoved = false;
+ expect(vm.shouldShowRemoveSourceBranch).toBeTruthy();
+
+ vm.mr.sourceBranchRemoved = true;
+ expect(vm.shouldShowRemoveSourceBranch).toBeFalsy();
+
+ vm.mr.sourceBranchRemoved = false;
+ vm.mr.canRemoveSourceBranch = false;
+ expect(vm.shouldShowRemoveSourceBranch).toBeFalsy();
+
+ vm.mr.canRemoveSourceBranch = true;
+ vm.isMakingRequest = true;
+ expect(vm.shouldShowRemoveSourceBranch).toBeFalsy();
+
+ vm.mr.isRemovingSourceBranch = true;
+ vm.mr.canRemoveSourceBranch = true;
+ vm.isMakingRequest = true;
+ expect(vm.shouldShowRemoveSourceBranch).toBeFalsy();
+ });
+ });
+ describe('shouldShowSourceBranchRemoving', () => {
+ it('should correct value when fields changed', () => {
+ const vm = createComponent();
+ vm.mr.sourceBranchRemoved = false;
+ expect(vm.shouldShowSourceBranchRemoving).toBeFalsy();
+
+ vm.mr.sourceBranchRemoved = true;
+ expect(vm.shouldShowRemoveSourceBranch).toBeFalsy();
+
+ vm.mr.sourceBranchRemoved = false;
+ vm.isMakingRequest = true;
+ expect(vm.shouldShowSourceBranchRemoving).toBeTruthy();
+
+ vm.isMakingRequest = false;
+ vm.mr.isRemovingSourceBranch = true;
+ expect(vm.shouldShowSourceBranchRemoving).toBeTruthy();
+ });
+ });
+ });
+
+ describe('methods', () => {
+ describe('removeSourceBranch', () => {
+ it('should set flag and call service then request main component to update the widget', (done) => {
+ const vm = createComponent();
+ spyOn(eventHub, '$emit');
+ spyOn(vm.service, 'removeSourceBranch').and.returnValue(new Promise((resolve) => {
+ resolve({
+ json() {
+ return {
+ message: 'Branch was removed',
+ };
+ },
+ });
+ }));
+
+ vm.removeSourceBranch();
+ setTimeout(() => {
+ const args = eventHub.$emit.calls.argsFor(0);
+ expect(vm.isMakingRequest).toBeTruthy();
+ expect(args[0]).toEqual('MRWidgetUpdateRequested');
+ expect(args[1]).not.toThrow();
+ done();
+ }, 333);
+ });
+ });
+ });
+
+ describe('template', () => {
+ let vm;
+ let el;
+
+ beforeEach(() => {
+ vm = createComponent();
+ el = vm.$el;
+ });
+
+ it('should have correct elements', () => {
+ expect(el.classList.contains('mr-widget-body')).toBeTruthy();
+ expect(el.querySelector('.js-mr-widget-author')).toBeDefined();
+ expect(el.innerText).toContain('The changes were merged into');
+ expect(el.innerText).toContain(targetBranch);
+ expect(el.innerText).toContain('The source branch has been removed.');
+ expect(el.innerText).toContain('Revert');
+ expect(el.innerText).toContain('Cherry-pick');
+ expect(el.innerText).not.toContain('You can remove source branch now.');
+ expect(el.innerText).not.toContain('The source branch is being removed.');
+ });
+
+ it('should not show source branch removed text', (done) => {
+ vm.mr.sourceBranchRemoved = false;
+
+ Vue.nextTick(() => {
+ expect(el.innerText).toContain('You can remove source branch now.');
+ expect(el.innerText).not.toContain('The source branch has been removed.');
+ done();
+ });
+ });
+
+ it('should show source branch removing text', (done) => {
+ vm.mr.isRemovingSourceBranch = true;
+ vm.mr.sourceBranchRemoved = false;
+
+ Vue.nextTick(() => {
+ expect(el.innerText).toContain('The source branch is being removed.');
+ expect(el.innerText).not.toContain('You can remove source branch now.');
+ expect(el.innerText).not.toContain('The source branch has been removed.');
+ done();
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_missing_branch_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_missing_branch_spec.js
new file mode 100644
index 00000000000..98674d12afb
--- /dev/null
+++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_missing_branch_spec.js
@@ -0,0 +1,55 @@
+import Vue from 'vue';
+import missingBranchComponent from '~/vue_merge_request_widget/components/states/mr_widget_missing_branch';
+
+const createComponent = () => {
+ const Component = Vue.extend(missingBranchComponent);
+ const mr = {
+ sourceBranchRemoved: true,
+ };
+
+ return new Component({
+ el: document.createElement('div'),
+ propsData: { mr },
+ });
+};
+
+describe('MRWidgetMissingBranch', () => {
+ describe('props', () => {
+ it('should have props', () => {
+ const mrProp = missingBranchComponent.props.mr;
+
+ expect(mrProp.type instanceof Object).toBeTruthy();
+ expect(mrProp.required).toBeTruthy();
+ });
+ });
+
+ describe('components', () => {
+ it('should have components added', () => {
+ expect(missingBranchComponent.components['mr-widget-merge-help']).toBeDefined();
+ });
+ });
+
+ describe('computed', () => {
+ describe('missingBranchName', () => {
+ it('should return proper branch name', () => {
+ const vm = createComponent();
+ expect(vm.missingBranchName).toEqual('source');
+
+ vm.mr.sourceBranchRemoved = false;
+ expect(vm.missingBranchName).toEqual('target');
+ });
+ });
+ });
+
+ describe('template', () => {
+ it('should have correct elements', () => {
+ const el = createComponent().$el;
+ const content = el.textContent.replace(/\n(\s)+/g, ' ').trim();
+
+ expect(el.classList.contains('mr-widget-body')).toBeTruthy();
+ expect(el.querySelector('button').getAttribute('disabled')).toBeTruthy();
+ expect(content).toContain('source branch does not exist.');
+ expect(content).toContain('Please restore the source branch or use a different source branch.');
+ });
+ });
+});
diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_not_allowed_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_not_allowed_spec.js
new file mode 100644
index 00000000000..61e00f4cf79
--- /dev/null
+++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_not_allowed_spec.js
@@ -0,0 +1,17 @@
+import Vue from 'vue';
+import notAllowedComponent from '~/vue_merge_request_widget/components/states/mr_widget_not_allowed';
+
+describe('MRWidgetNotAllowed', () => {
+ describe('template', () => {
+ const Component = Vue.extend(notAllowedComponent);
+ const vm = new Component({
+ el: document.createElement('div'),
+ });
+ it('should have correct elements', () => {
+ expect(vm.$el.classList.contains('mr-widget-body')).toBeTruthy();
+ expect(vm.$el.querySelector('button').getAttribute('disabled')).toBeTruthy();
+ expect(vm.$el.innerText).toContain('Ready to be merged automatically.');
+ expect(vm.$el.innerText).toContain('Ask someone with write access to this repository to merge this request.');
+ });
+ });
+});
diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_nothing_to_merge_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_nothing_to_merge_spec.js
new file mode 100644
index 00000000000..d40c67b189d
--- /dev/null
+++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_nothing_to_merge_spec.js
@@ -0,0 +1,17 @@
+import Vue from 'vue';
+import nothingToMergeComponent from '~/vue_merge_request_widget/components/states/mr_widget_nothing_to_merge';
+
+describe('MRWidgetNothingToMerge', () => {
+ describe('template', () => {
+ const Component = Vue.extend(nothingToMergeComponent);
+ const vm = new Component({
+ el: document.createElement('div'),
+ });
+ it('should have correct elements', () => {
+ expect(vm.$el.classList.contains('mr-widget-body')).toBeTruthy();
+ expect(vm.$el.querySelector('button').getAttribute('disabled')).toBeTruthy();
+ expect(vm.$el.innerText).toContain('There is nothing to merge from source branch into target branch.');
+ expect(vm.$el.innerText).toContain('Please push new commits or use a different branch.');
+ });
+ });
+});
diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_pipeline_blocked_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_pipeline_blocked_spec.js
new file mode 100644
index 00000000000..b293d118571
--- /dev/null
+++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_pipeline_blocked_spec.js
@@ -0,0 +1,16 @@
+import Vue from 'vue';
+import pipelineBlockedComponent from '~/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked';
+
+describe('MRWidgetPipelineBlocked', () => {
+ describe('template', () => {
+ const Component = Vue.extend(pipelineBlockedComponent);
+ const vm = new Component({
+ el: document.createElement('div'),
+ });
+ it('should have correct elements', () => {
+ expect(vm.$el.classList.contains('mr-widget-body')).toBeTruthy();
+ expect(vm.$el.querySelector('button').getAttribute('disabled')).toBeTruthy();
+ expect(vm.$el.innerText).toContain('Pipeline blocked. The pipeline for this merge request requires a manual action to proceed.');
+ });
+ });
+});
diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_pipeline_failed_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_pipeline_failed_spec.js
new file mode 100644
index 00000000000..807fba705d4
--- /dev/null
+++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_pipeline_failed_spec.js
@@ -0,0 +1,16 @@
+import Vue from 'vue';
+import pipelineFailedComponent from '~/vue_merge_request_widget/components/states/mr_widget_pipeline_failed';
+
+describe('MRWidgetPipelineFailed', () => {
+ describe('template', () => {
+ const Component = Vue.extend(pipelineFailedComponent);
+ const vm = new Component({
+ el: document.createElement('div'),
+ });
+ it('should have correct elements', () => {
+ expect(vm.$el.classList.contains('mr-widget-body')).toBeTruthy();
+ expect(vm.$el.querySelector('button').getAttribute('disabled')).toBeTruthy();
+ expect(vm.$el.innerText).toContain('The pipeline for this merge request failed. Please retry the job or push a new commit to fix the failure.');
+ });
+ });
+});
diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js
new file mode 100644
index 00000000000..74df99415c9
--- /dev/null
+++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js
@@ -0,0 +1,389 @@
+import Vue from 'vue';
+import readyToMergeComponent from '~/vue_merge_request_widget/components/states/mr_widget_ready_to_merge';
+import eventHub from '~/vue_merge_request_widget/event_hub';
+import * as simplePoll from '~/lib/utils/simple_poll';
+
+const commitMessage = 'This is the commit message';
+const commitMessageWithDescription = 'This is the commit message description';
+const createComponent = () => {
+ const Component = Vue.extend(readyToMergeComponent);
+ const mr = {
+ isPipelineActive: false,
+ pipeline: null,
+ isPipelineFailed: false,
+ onlyAllowMergeIfPipelineSucceeds: false,
+ hasCI: false,
+ ciStatus: null,
+ sha: '12345678',
+ commitMessage,
+ commitMessageWithDescription,
+ };
+
+ const service = {
+ merge() {},
+ poll() {},
+ };
+
+ return new Component({
+ el: document.createElement('div'),
+ propsData: { mr, service },
+ });
+};
+
+describe('MRWidgetReadyToMerge', () => {
+ let vm;
+
+ beforeEach(() => {
+ vm = createComponent();
+ });
+
+ describe('props', () => {
+ it('should have props', () => {
+ const { mr, service } = readyToMergeComponent.props;
+
+ expect(mr.type instanceof Object).toBeTruthy();
+ expect(mr.required).toBeTruthy();
+
+ expect(service.type instanceof Object).toBeTruthy();
+ expect(service.required).toBeTruthy();
+ });
+ });
+
+ describe('data', () => {
+ it('should have default data', () => {
+ expect(vm.removeSourceBranch).toBeTruthy(true);
+ expect(vm.mergeWhenBuildSucceeds).toBeFalsy();
+ expect(vm.useCommitMessageWithDescription).toBeFalsy();
+ expect(vm.setToMergeWhenPipelineSucceeds).toBeFalsy();
+ expect(vm.showCommitMessageEditor).toBeFalsy();
+ expect(vm.isMakingRequest).toBeFalsy();
+ expect(vm.isMergingImmediately).toBeFalsy();
+ expect(vm.commitMessage).toBe(vm.mr.commitMessage);
+ expect(vm.successSvg).toBeDefined();
+ expect(vm.warningSvg).toBeDefined();
+ });
+ });
+
+ describe('computed', () => {
+ describe('commitMessageLinkTitle', () => {
+ const withDesc = 'Include description in commit message';
+ const withoutDesc = "Don't include description in commit message";
+
+ it('should return message wit description', () => {
+ expect(vm.commitMessageLinkTitle).toEqual(withDesc);
+ });
+
+ it('should return message without description', () => {
+ vm.useCommitMessageWithDescription = true;
+ expect(vm.commitMessageLinkTitle).toEqual(withoutDesc);
+ });
+ });
+
+ describe('mergeButtonClass', () => {
+ const defaultClass = 'btn btn-success accept-merge-request';
+ const failedClass = `${defaultClass} btn-danger`;
+ const inActionClass = `${defaultClass} btn-info`;
+
+ it('should return default class', () => {
+ vm.mr.pipeline = true;
+ expect(vm.mergeButtonClass).toEqual(defaultClass);
+ });
+
+ it('should return failed class when MR has CI but also has an unknown status', () => {
+ vm.mr.hasCI = true;
+ expect(vm.mergeButtonClass).toEqual(failedClass);
+ });
+
+ it('should return default class when MR has no pipeline', () => {
+ expect(vm.mergeButtonClass).toEqual(defaultClass);
+ });
+
+ it('should return in action class when pipeline is active', () => {
+ vm.mr.pipeline = {};
+ vm.mr.isPipelineActive = true;
+ expect(vm.mergeButtonClass).toEqual(inActionClass);
+ });
+
+ it('should return failed class when pipeline is failed', () => {
+ vm.mr.pipeline = {};
+ vm.mr.isPipelineFailed = true;
+ expect(vm.mergeButtonClass).toEqual(failedClass);
+ });
+ });
+
+ describe('mergeButtonText', () => {
+ it('should return Merge', () => {
+ expect(vm.mergeButtonText).toEqual('Merge');
+ });
+
+ it('should return Merge in progress', () => {
+ vm.isMergingImmediately = true;
+ expect(vm.mergeButtonText).toEqual('Merge in progress');
+ });
+
+ it('should return Merge when pipeline succeeds', () => {
+ vm.isMergingImmediately = false;
+ vm.mr.isPipelineActive = true;
+ expect(vm.mergeButtonText).toEqual('Merge when pipeline succeeds');
+ });
+ });
+
+ describe('shouldShowMergeOptionsDropdown', () => {
+ it('should return false with initial data', () => {
+ expect(vm.shouldShowMergeOptionsDropdown).toBeFalsy();
+ });
+
+ it('should return true when pipeline active', () => {
+ vm.mr.isPipelineActive = true;
+ expect(vm.shouldShowMergeOptionsDropdown).toBeTruthy();
+ });
+
+ it('should return false when pipeline active but only merge when pipeline succeeds set in project options', () => {
+ vm.mr.isPipelineActive = true;
+ vm.mr.onlyAllowMergeIfPipelineSucceeds = true;
+ expect(vm.shouldShowMergeOptionsDropdown).toBeFalsy();
+ });
+ });
+
+ describe('isMergeButtonDisabled', () => {
+ it('should return false with initial data', () => {
+ expect(vm.isMergeButtonDisabled).toBeFalsy();
+ });
+
+ it('should return true when there is no commit message', () => {
+ vm.commitMessage = '';
+ expect(vm.isMergeButtonDisabled).toBeTruthy();
+ });
+
+ it('should return true if merge is not allowed', () => {
+ vm.mr.onlyAllowMergeIfPipelineSucceeds = true;
+ vm.mr.isPipelineFailed = true;
+ expect(vm.isMergeButtonDisabled).toBeTruthy();
+ });
+
+ it('should return true when there vm instance is making request', () => {
+ vm.isMakingRequest = true;
+ expect(vm.isMergeButtonDisabled).toBeTruthy();
+ });
+ });
+ });
+
+ describe('methods', () => {
+ describe('isMergeAllowed', () => {
+ it('should return false with initial data', () => {
+ expect(vm.isMergeAllowed()).toBeTruthy();
+ });
+
+ it('should return false when MR is set only merge when pipeline succeeds', () => {
+ vm.mr.onlyAllowMergeIfPipelineSucceeds = true;
+ expect(vm.isMergeAllowed()).toBeTruthy();
+ });
+
+ it('should return true true', () => {
+ vm.mr.onlyAllowMergeIfPipelineSucceeds = true;
+ vm.mr.isPipelineFailed = true;
+ expect(vm.isMergeAllowed()).toBeFalsy();
+ });
+ });
+
+ describe('updateCommitMessage', () => {
+ it('should revert flag and change commitMessage', () => {
+ expect(vm.useCommitMessageWithDescription).toBeFalsy();
+ expect(vm.commitMessage).toEqual(commitMessage);
+ vm.updateCommitMessage();
+ expect(vm.useCommitMessageWithDescription).toBeTruthy();
+ expect(vm.commitMessage).toEqual(commitMessageWithDescription);
+ vm.updateCommitMessage();
+ expect(vm.useCommitMessageWithDescription).toBeFalsy();
+ expect(vm.commitMessage).toEqual(commitMessage);
+ });
+ });
+
+ describe('toggleCommitMessageEditor', () => {
+ it('should toggle showCommitMessageEditor flag', () => {
+ expect(vm.showCommitMessageEditor).toBeFalsy();
+ vm.toggleCommitMessageEditor();
+ expect(vm.showCommitMessageEditor).toBeTruthy();
+ });
+ });
+
+ describe('handleMergeButtonClick', () => {
+ const returnPromise = status => new Promise((resolve) => {
+ resolve({
+ json() {
+ return { status };
+ },
+ });
+ });
+
+ it('should handle merge when pipeline succeeds', (done) => {
+ spyOn(eventHub, '$emit');
+ spyOn(vm.service, 'merge').and.returnValue(returnPromise('merge_when_pipeline_succeeds'));
+ vm.removeSourceBranch = false;
+ vm.handleMergeButtonClick(true);
+
+ setTimeout(() => {
+ expect(vm.setToMergeWhenPipelineSucceeds).toBeTruthy();
+ expect(vm.isMakingRequest).toBeTruthy();
+ expect(eventHub.$emit).toHaveBeenCalledWith('MRWidgetUpdateRequested');
+
+ const params = vm.service.merge.calls.argsFor(0)[0];
+ expect(params.sha).toEqual(vm.mr.sha);
+ expect(params.commit_message).toEqual(vm.mr.commitMessage);
+ expect(params.should_remove_source_branch).toBeFalsy();
+ expect(params.merge_when_pipeline_succeeds).toBeTruthy();
+ done();
+ }, 333);
+ });
+
+ it('should handle merge failed', (done) => {
+ spyOn(eventHub, '$emit');
+ spyOn(vm.service, 'merge').and.returnValue(returnPromise('failed'));
+ vm.handleMergeButtonClick(false, true);
+
+ setTimeout(() => {
+ expect(vm.setToMergeWhenPipelineSucceeds).toBeFalsy();
+ expect(vm.isMakingRequest).toBeTruthy();
+ expect(eventHub.$emit).toHaveBeenCalledWith('FailedToMerge', undefined);
+
+ const params = vm.service.merge.calls.argsFor(0)[0];
+ expect(params.should_remove_source_branch).toBeTruthy();
+ expect(params.merge_when_pipeline_succeeds).toBeFalsy();
+ done();
+ }, 333);
+ });
+
+ it('should handle merge action accepted case', (done) => {
+ spyOn(vm.service, 'merge').and.returnValue(returnPromise('success'));
+ spyOn(vm, 'initiateMergePolling');
+ vm.handleMergeButtonClick();
+
+ setTimeout(() => {
+ expect(vm.setToMergeWhenPipelineSucceeds).toBeFalsy();
+ expect(vm.isMakingRequest).toBeTruthy();
+ expect(vm.initiateMergePolling).toHaveBeenCalled();
+
+ const params = vm.service.merge.calls.argsFor(0)[0];
+ expect(params.should_remove_source_branch).toBeTruthy();
+ expect(params.merge_when_pipeline_succeeds).toBeFalsy();
+ done();
+ }, 333);
+ });
+ });
+
+ describe('initiateMergePolling', () => {
+ it('should call simplePoll', () => {
+ spyOn(simplePoll, 'default');
+ vm.initiateMergePolling();
+ expect(simplePoll.default).toHaveBeenCalled();
+ });
+ });
+
+ describe('handleMergePolling', () => {
+ const returnPromise = state => new Promise((resolve) => {
+ resolve({
+ json() {
+ return { state, source_branch_exists: true };
+ },
+ });
+ });
+
+ it('should call start and stop polling when MR merged', (done) => {
+ spyOn(eventHub, '$emit');
+ spyOn(vm.service, 'poll').and.returnValue(returnPromise('merged'));
+ spyOn(vm, 'initiateRemoveSourceBranchPolling');
+
+ let cpc = false; // continuePollingCalled
+ let spc = false; // stopPollingCalled
+
+ vm.handleMergePolling(() => { cpc = true; }, () => { spc = true; });
+ setTimeout(() => {
+ expect(vm.service.poll).toHaveBeenCalled();
+ expect(eventHub.$emit).toHaveBeenCalledWith('MRWidgetUpdateRequested');
+ expect(eventHub.$emit).toHaveBeenCalledWith('FetchActionsContent');
+ expect(vm.initiateRemoveSourceBranchPolling).toHaveBeenCalled();
+ expect(cpc).toBeFalsy();
+ expect(spc).toBeTruthy();
+
+ done();
+ }, 333);
+ });
+
+ it('should continue polling until MR is merged', (done) => {
+ spyOn(vm.service, 'poll').and.returnValue(returnPromise('some_other_state'));
+ spyOn(vm, 'initiateRemoveSourceBranchPolling');
+
+ let cpc = false; // continuePollingCalled
+ let spc = false; // stopPollingCalled
+
+ vm.handleMergePolling(() => { cpc = true; }, () => { spc = true; });
+ setTimeout(() => {
+ expect(cpc).toBeTruthy();
+ expect(spc).toBeFalsy();
+
+ done();
+ }, 333);
+ });
+ });
+
+ describe('initiateRemoveSourceBranchPolling', () => {
+ it('should emit event and call simplePoll', () => {
+ spyOn(eventHub, '$emit');
+ spyOn(simplePoll, 'default');
+
+ vm.initiateRemoveSourceBranchPolling();
+ expect(eventHub.$emit).toHaveBeenCalledWith('SetBranchRemoveFlag', [true]);
+ expect(simplePoll.default).toHaveBeenCalled();
+ });
+ });
+
+ describe('handleRemoveBranchPolling', () => {
+ const returnPromise = state => new Promise((resolve) => {
+ resolve({
+ json() {
+ return { source_branch_exists: state };
+ },
+ });
+ });
+
+ it('should call start and stop polling when MR merged', (done) => {
+ spyOn(eventHub, '$emit');
+ spyOn(vm.service, 'poll').and.returnValue(returnPromise(false));
+
+ let cpc = false; // continuePollingCalled
+ let spc = false; // stopPollingCalled
+
+ vm.handleRemoveBranchPolling(() => { cpc = true; }, () => { spc = true; });
+ setTimeout(() => {
+ expect(vm.service.poll).toHaveBeenCalled();
+
+ const args = eventHub.$emit.calls.argsFor(0);
+ expect(args[0]).toEqual('MRWidgetUpdateRequested');
+ expect(args[1]).toBeDefined();
+ args[1]();
+ expect(eventHub.$emit).toHaveBeenCalledWith('SetBranchRemoveFlag', [false]);
+
+ expect(cpc).toBeFalsy();
+ expect(spc).toBeTruthy();
+
+ done();
+ }, 333);
+ });
+
+ it('should continue polling until MR is merged', (done) => {
+ spyOn(vm.service, 'poll').and.returnValue(returnPromise(true));
+
+ let cpc = false; // continuePollingCalled
+ let spc = false; // stopPollingCalled
+
+ vm.handleRemoveBranchPolling(() => { cpc = true; }, () => { spc = true; });
+ setTimeout(() => {
+ expect(cpc).toBeTruthy();
+ expect(spc).toBeFalsy();
+
+ done();
+ }, 333);
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_sha_mismatch_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_sha_mismatch_spec.js
new file mode 100644
index 00000000000..5fb1d69a8b3
--- /dev/null
+++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_sha_mismatch_spec.js
@@ -0,0 +1,16 @@
+import Vue from 'vue';
+import shaMismatchComponent from '~/vue_merge_request_widget/components/states/mr_widget_sha_mismatch';
+
+describe('MRWidgetSHAMismatch', () => {
+ describe('template', () => {
+ const Component = Vue.extend(shaMismatchComponent);
+ const vm = new Component({
+ el: document.createElement('div'),
+ });
+ it('should have correct elements', () => {
+ expect(vm.$el.classList.contains('mr-widget-body')).toBeTruthy();
+ expect(vm.$el.querySelector('button').getAttribute('disabled')).toBeTruthy();
+ expect(vm.$el.innerText).toContain('The source branch HEAD has recently changed. Please reload the page and review the changes before merging.');
+ });
+ });
+});
diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_unresolved_discussions_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_unresolved_discussions_spec.js
new file mode 100644
index 00000000000..fe87f110354
--- /dev/null
+++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_unresolved_discussions_spec.js
@@ -0,0 +1,47 @@
+import Vue from 'vue';
+import unresolvedDiscussionsComponent from '~/vue_merge_request_widget/components/states/mr_widget_unresolved_discussions';
+
+describe('MRWidgetUnresolvedDiscussions', () => {
+ describe('props', () => {
+ it('should have props', () => {
+ const { mr } = unresolvedDiscussionsComponent.props;
+
+ expect(mr.type instanceof Object).toBeTruthy();
+ expect(mr.required).toBeTruthy();
+ });
+ });
+
+ describe('template', () => {
+ let el;
+ let vm;
+ const path = 'foo/bar';
+
+ beforeEach(() => {
+ const Component = Vue.extend(unresolvedDiscussionsComponent);
+ const mr = {
+ createIssueToResolveDiscussionsPath: path,
+ };
+ vm = new Component({
+ el: document.createElement('div'),
+ propsData: { mr },
+ });
+ el = vm.$el;
+ });
+
+ it('should have correct elements', () => {
+ expect(el.classList.contains('mr-widget-body')).toBeTruthy();
+ expect(el.innerText).toContain('There are unresolved discussions. Please resolve these discussions');
+ expect(el.innerText).toContain('Create an issue to resolve them later');
+ expect(el.querySelector('.js-create-issue').getAttribute('href')).toEqual(path);
+ });
+
+ it('should not show create issue button if user cannot create issue', (done) => {
+ vm.mr.createIssueToResolveDiscussionsPath = '';
+
+ Vue.nextTick(() => {
+ expect(el.querySelector('.js-create-issue')).toEqual(null);
+ done();
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_wip_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_wip_spec.js
new file mode 100644
index 00000000000..45bd1a69964
--- /dev/null
+++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_wip_spec.js
@@ -0,0 +1,96 @@
+import Vue from 'vue';
+import wipComponent from '~/vue_merge_request_widget/components/states/mr_widget_wip';
+import eventHub from '~/vue_merge_request_widget/event_hub';
+
+const createComponent = () => {
+ const Component = Vue.extend(wipComponent);
+ const mr = {
+ title: 'The best MR ever',
+ removeWIPPath: '/path/to/remove/wip',
+ };
+ const service = {
+ removeWIP() {},
+ };
+ return new Component({
+ el: document.createElement('div'),
+ propsData: { mr, service },
+ });
+};
+
+describe('MRWidgetWIP', () => {
+ describe('props', () => {
+ it('should have props', () => {
+ const { mr, service } = wipComponent.props;
+
+ expect(mr.type instanceof Object).toBeTruthy();
+ expect(mr.required).toBeTruthy();
+
+ expect(service.type instanceof Object).toBeTruthy();
+ expect(service.required).toBeTruthy();
+ });
+ });
+
+ describe('data', () => {
+ it('should have default data', () => {
+ const vm = createComponent();
+ expect(vm.isMakingRequest).toBeFalsy();
+ });
+ });
+
+ describe('methods', () => {
+ const mrObj = {
+ is_new_mr_data: true,
+ };
+
+ describe('removeWIP', () => {
+ it('should make a request to service and handle response', (done) => {
+ const vm = createComponent();
+
+ spyOn(window, 'Flash').and.returnValue(true);
+ spyOn(eventHub, '$emit');
+ spyOn(vm.service, 'removeWIP').and.returnValue(new Promise((resolve) => {
+ resolve({
+ json() {
+ return mrObj;
+ },
+ });
+ }));
+
+ vm.removeWIP();
+ setTimeout(() => {
+ expect(vm.isMakingRequest).toBeTruthy();
+ expect(eventHub.$emit).toHaveBeenCalledWith('UpdateWidgetData', mrObj);
+ expect(window.Flash).toHaveBeenCalledWith('The merge request can now be merged.', 'notice');
+ done();
+ }, 333);
+ });
+ });
+ });
+
+ describe('template', () => {
+ let vm;
+ let el;
+
+ beforeEach(() => {
+ vm = createComponent();
+ el = vm.$el;
+ });
+
+ it('should have correct elements', () => {
+ expect(el.classList.contains('mr-widget-body')).toBeTruthy();
+ expect(el.innerText).toContain('This merge request is currently Work In Progress and therefore unable to merge');
+ expect(el.querySelector('button').getAttribute('disabled')).toBeTruthy();
+ expect(el.querySelector('button').innerText).toContain('Merge');
+ expect(el.querySelector('.js-remove-wip').innerText).toContain('Resolve WIP status');
+ });
+
+ it('should not show removeWIP button is user cannot update MR', (done) => {
+ vm.mr.removeWIPPath = '';
+
+ Vue.nextTick(() => {
+ expect(el.querySelector('.js-remove-wip')).toEqual(null);
+ done();
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/vue_mr_widget/mock_data.js b/spec/javascripts/vue_mr_widget/mock_data.js
new file mode 100644
index 00000000000..e6f96d5588b
--- /dev/null
+++ b/spec/javascripts/vue_mr_widget/mock_data.js
@@ -0,0 +1,214 @@
+/* eslint-disable */
+
+export default {
+ "id": 132,
+ "iid": 22,
+ "assignee_id": null,
+ "author_id": 1,
+ "description": "",
+ "lock_version": null,
+ "milestone_id": null,
+ "position": 0,
+ "state": "merged",
+ "title": "Update README.md",
+ "updated_by_id": null,
+ "created_at": "2017-04-07T12:27:26.718Z",
+ "updated_at": "2017-04-07T15:39:25.852Z",
+ "deleted_at": null,
+ "time_estimate": 0,
+ "total_time_spent": 0,
+ "human_time_estimate": null,
+ "human_total_time_spent": null,
+ "in_progress_merge_commit_sha": null,
+ "locked_at": null,
+ "merge_commit_sha": "53027d060246c8f47e4a9310fb332aa52f221775",
+ "merge_error": null,
+ "merge_params": {
+ "force_remove_source_branch": null
+ },
+ "merge_status": "can_be_merged",
+ "merge_user_id": null,
+ "merge_when_pipeline_succeeds": false,
+ "source_branch": "daaaa",
+ "source_project_id": 19,
+ "target_branch": "master",
+ "target_project_id": 19,
+ "merge_event": {
+ "author": {
+ "name": "Administrator",
+ "username": "root",
+ "id": 1,
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+ "web_url": "http://localhost:3000/root"
+ },
+ "updated_at": "2017-04-07T15:39:25.696Z"
+ },
+ "closed_event": null,
+ "author": {
+ "name": "Administrator",
+ "username": "root",
+ "id": 1,
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+ "web_url": "http://localhost:3000/root"
+ },
+ "merge_user": null,
+ "diff_head_sha": "104096c51715e12e7ae41f9333e9fa35b73f385d",
+ "diff_head_commit_short_id": "104096c5",
+ "merge_commit_message": "Merge branch 'daaaa' into 'master'\n\nUpdate README.md\n\nSee merge request !22",
+ "pipeline": {
+ "id": 172,
+ "user": {
+ "name": "Administrator",
+ "username": "root",
+ "id": 1,
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+ "web_url": "http://localhost:3000/root"
+ },
+ "active": false,
+ "coverage": "92.16",
+ "path": "/root/acets-app/pipelines/172",
+ "details": {
+ "status": {
+ "icon": "icon_status_success",
+ "favicon": "favicon_status_success",
+ "text": "passed",
+ "label": "passed",
+ "group": "success",
+ "has_details": true,
+ "details_path": "/root/acets-app/pipelines/172"
+ },
+ "duration": null,
+ "finished_at": "2017-04-07T14:00:14.256Z",
+ "stages": [
+ {
+ "name": "build",
+ "title": "build: failed",
+ "status": {
+ "icon": "icon_status_failed",
+ "favicon": "favicon_status_failed",
+ "text": "failed",
+ "label": "failed",
+ "group": "failed",
+ "has_details": true,
+ "details_path": "/root/acets-app/pipelines/172#build"
+ },
+ "path": "/root/acets-app/pipelines/172#build",
+ "dropdown_path": "/root/acets-app/pipelines/172/stage.json?stage=build"
+ },
+ {
+ "name": "review",
+ "title": "review: skipped",
+ "status": {
+ "icon": "icon_status_skipped",
+ "favicon": "favicon_status_skipped",
+ "text": "skipped",
+ "label": "skipped",
+ "group": "skipped",
+ "has_details": true,
+ "details_path": "/root/acets-app/pipelines/172#review"
+ },
+ "path": "/root/acets-app/pipelines/172#review",
+ "dropdown_path": "/root/acets-app/pipelines/172/stage.json?stage=review"
+ }
+ ],
+ "artifacts": [
+
+ ],
+ "manual_actions": [
+ {
+ "name": "stop_review",
+ "path": "/root/acets-app/builds/1427/play",
+ "playable": false
+ }
+ ]
+ },
+ "flags": {
+ "latest": false,
+ "triggered": false,
+ "stuck": false,
+ "yaml_errors": false,
+ "retryable": true,
+ "cancelable": false
+ },
+ "ref": {
+ "name": "daaaa",
+ "path": "/root/acets-app/tree/daaaa",
+ "tag": false,
+ "branch": true
+ },
+ "commit": {
+ "id": "104096c51715e12e7ae41f9333e9fa35b73f385d",
+ "short_id": "104096c5",
+ "title": "Update README.md",
+ "created_at": "2017-04-07T15:27:18.000+03:00",
+ "parent_ids": [
+ "2396536178668d8930c29d904e53bd4d06228b32"
+ ],
+ "message": "Update README.md",
+ "author_name": "Administrator",
+ "author_email": "admin@example.com",
+ "authored_date": "2017-04-07T15:27:18.000+03:00",
+ "committer_name": "Administrator",
+ "committer_email": "admin@example.com",
+ "committed_date": "2017-04-07T15:27:18.000+03:00",
+ "author": {
+ "name": "Administrator",
+ "username": "root",
+ "id": 1,
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+ "web_url": "http://localhost:3000/root"
+ },
+ "author_gravatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+ "commit_url": "http://localhost:3000/root/acets-app/commit/104096c51715e12e7ae41f9333e9fa35b73f385d",
+ "commit_path": "/root/acets-app/commit/104096c51715e12e7ae41f9333e9fa35b73f385d"
+ },
+ "retry_path": "/root/acets-app/pipelines/172/retry",
+ "created_at": "2017-04-07T12:27:19.520Z",
+ "updated_at": "2017-04-07T15:28:44.800Z"
+ },
+ "work_in_progress": false,
+ "source_branch_exists": false,
+ "mergeable_discussions_state": true,
+ "conflicts_can_be_resolved_in_ui": false,
+ "branch_missing": true,
+ "commits_count": 1,
+ "has_conflicts": false,
+ "can_be_merged": true,
+ "has_ci": true,
+ "ci_status": "success",
+ "pipeline_status_path": "/root/acets-app/merge_requests/22/pipeline_status",
+ "issues_links": {
+ "closing": "",
+ "mentioned_but_not_closing": ""
+ },
+ "current_user": {
+ "can_resolve_conflicts": true,
+ "can_remove_source_branch": false,
+ "can_revert_on_current_merge_request": true,
+ "can_cherry_pick_on_current_merge_request": true
+ },
+ "target_branch_path": "/root/acets-app/branches/master",
+ "source_branch_path": "/root/acets-app/branches/daaaa",
+ "conflict_resolution_ui_path": "/root/acets-app/merge_requests/22/conflicts",
+ "remove_wip_path": "/root/acets-app/merge_requests/22/remove_wip",
+ "cancel_merge_when_pipeline_succeeds_path": "/root/acets-app/merge_requests/22/cancel_merge_when_pipeline_succeeds",
+ "create_issue_to_resolve_discussions_path": "/root/acets-app/issues/new?merge_request_to_resolve_discussions_of=22",
+ "merge_path": "/root/acets-app/merge_requests/22/merge",
+ "cherry_pick_in_fork_path": "/root/acets-app/forks?continue%5Bnotice%5D=You%27re+not+allowed+to+make+changes+to+this+project+directly.+A+fork+of+this+project+has+been+created+that+you+can+make+changes+in%2C+so+you+can+submit+a+merge+request.+Try+to+revert+this+commit+again.&continue%5Bnotice_now%5D=You%27re+not+allowed+to+make+changes+to+this+project+directly.+A+fork+of+this+project+is+being+created+that+you+can+make+changes+in%2C+so+you+can+submit+a+merge+request.&continue%5Bto%5D=%2Froot%2Facets-app%2Fmerge_requests%2F22&namespace_key=1",
+ "revert_in_fork_path": "/root/acets-app/forks?continue%5Bnotice%5D=You%27re+not+allowed+to+make+changes+to+this+project+directly.+A+fork+of+this+project+has+been+created+that+you+can+make+changes+in%2C+so+you+can+submit+a+merge+request.+Try+to+cherry-pick+this+commit+again.&continue%5Bnotice_now%5D=You%27re+not+allowed+to+make+changes+to+this+project+directly.+A+fork+of+this+project+is+being+created+that+you+can+make+changes+in%2C+so+you+can+submit+a+merge+request.&continue%5Bto%5D=%2Froot%2Facets-app%2Fmerge_requests%2F22&namespace_key=1",
+ "email_patches_path": "/root/acets-app/merge_requests/22.patch",
+ "plain_diff_path": "/root/acets-app/merge_requests/22.diff",
+ "ci_status_path": "/root/acets-app/merge_requests/22/ci_status",
+ "status_path": "/root/acets-app/merge_requests/22.json",
+ "merge_check_path": "/root/acets-app/merge_requests/22/merge_check",
+ "ci_environments_status_url": "/root/acets-app/merge_requests/22/ci_environments_status",
+ "project_archived": false,
+ "merge_commit_message_with_description": "Merge branch 'daaaa' into 'master'\n\nUpdate README.md\n\nSee merge request !22",
+ "diverged_commits_count": 0,
+ "only_allow_merge_if_pipeline_succeeds": false,
+ "commit_change_content_path": "/root/acets-app/merge_requests/22/commit_change_content"
+}
diff --git a/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js b/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js
new file mode 100644
index 00000000000..22ee7dcf0e7
--- /dev/null
+++ b/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js
@@ -0,0 +1,326 @@
+import Vue from 'vue';
+import MRWidgetService from '~/vue_merge_request_widget/services/mr_widget_service';
+import mrWidgetOptions from '~/vue_merge_request_widget/mr_widget_options';
+import eventHub from '~/vue_merge_request_widget/event_hub';
+import mockData from './mock_data';
+
+const createComponent = () => {
+ delete mrWidgetOptions.el; // Prevent component mounting
+ gl.mrWidgetData = mockData;
+ const Component = Vue.extend(mrWidgetOptions);
+ return new Component();
+};
+
+const returnPromise = data => new Promise((resolve) => {
+ resolve({
+ json() {
+ return data;
+ },
+ body: data,
+ });
+});
+
+describe('mrWidgetOptions', () => {
+ let vm;
+
+ beforeEach(() => {
+ vm = createComponent();
+ });
+
+ describe('data', () => {
+ it('should instantiate Store and Service', () => {
+ expect(vm.mr).toBeDefined();
+ expect(vm.service).toBeDefined();
+ });
+ });
+
+ describe('computed', () => {
+ describe('componentName', () => {
+ it('should return merged component', () => {
+ expect(vm.componentName).toEqual('mr-widget-merged');
+ });
+
+ it('should return conflicts component', () => {
+ vm.mr.state = 'conflicts';
+ expect(vm.componentName).toEqual('mr-widget-conflicts');
+ });
+ });
+
+ describe('shouldRenderMergeHelp', () => {
+ it('should return false for the initial merged state', () => {
+ expect(vm.shouldRenderMergeHelp).toBeFalsy();
+ });
+
+ it('should return true for a state which requires help widget', () => {
+ vm.mr.state = 'conflicts';
+ expect(vm.shouldRenderMergeHelp).toBeTruthy();
+ });
+ });
+
+ describe('shouldRenderPipelines', () => {
+ it('should return true for the initial data', () => {
+ expect(vm.shouldRenderPipelines).toBeTruthy();
+ });
+
+ it('should return true when pipeline is empty but MR.hasCI is set to true', () => {
+ vm.mr.pipeline = {};
+ expect(vm.shouldRenderPipelines).toBeTruthy();
+ });
+
+ it('should return true when pipeline available', () => {
+ vm.mr.hasCI = false;
+ expect(vm.shouldRenderPipelines).toBeTruthy();
+ });
+
+ it('should return false when there is no pipeline', () => {
+ vm.mr.pipeline = {};
+ vm.mr.hasCI = false;
+ expect(vm.shouldRenderPipelines).toBeFalsy();
+ });
+ });
+
+ describe('shouldRenderRelatedLinks', () => {
+ it('should return false for the initial data', () => {
+ expect(vm.shouldRenderRelatedLinks).toBeFalsy();
+ });
+
+ it('should return true if there is relatedLinks in MR', () => {
+ vm.mr.relatedLinks = {};
+ expect(vm.shouldRenderRelatedLinks).toBeTruthy();
+ });
+ });
+
+ describe('shouldRenderDeployments', () => {
+ it('should return false for the initial data', () => {
+ expect(vm.shouldRenderDeployments).toBeFalsy();
+ });
+
+ it('should return true if there is deployments', () => {
+ vm.mr.deployments.push({}, {});
+ expect(vm.shouldRenderDeployments).toBeTruthy();
+ });
+ });
+ });
+
+ describe('methods', () => {
+ describe('checkStatus', () => {
+ it('should tell service to check status', (done) => {
+ spyOn(vm.service, 'checkStatus').and.returnValue(returnPromise(mockData));
+ spyOn(vm.mr, 'setData');
+ let isCbExecuted = false;
+ const cb = () => {
+ isCbExecuted = true;
+ };
+
+ vm.checkStatus(cb);
+
+ setTimeout(() => {
+ expect(vm.service.checkStatus).toHaveBeenCalled();
+ expect(vm.mr.setData).toHaveBeenCalled();
+ expect(isCbExecuted).toBeTruthy();
+ done();
+ }, 333);
+ });
+ });
+
+ describe('initPolling', () => {
+ it('should call SmartInterval', () => {
+ spyOn(gl, 'SmartInterval').and.returnValue({
+ resume() {},
+ stopTimer() {},
+ });
+ vm.initPolling();
+
+ expect(vm.pollingInterval).toBeDefined();
+ expect(gl.SmartInterval).toHaveBeenCalled();
+ });
+ });
+
+ describe('initDeploymentsPolling', () => {
+ it('should call SmartInterval', () => {
+ spyOn(gl, 'SmartInterval');
+ vm.initDeploymentsPolling();
+
+ expect(vm.deploymentsInterval).toBeDefined();
+ expect(gl.SmartInterval).toHaveBeenCalled();
+ });
+ });
+
+ describe('fetchDeployments', () => {
+ it('should fetch deployments', (done) => {
+ spyOn(vm.service, 'fetchDeployments').and.returnValue(returnPromise([{ deployment: 1 }]));
+
+ vm.fetchDeployments();
+
+ setTimeout(() => {
+ expect(vm.service.fetchDeployments).toHaveBeenCalled();
+ expect(vm.mr.deployments.length).toEqual(1);
+ expect(vm.mr.deployments[0].deployment).toEqual(1);
+ done();
+ }, 333);
+ });
+ });
+
+ describe('fetchActionsContent', () => {
+ it('should fetch content of Cherry Pick and Revert modals', (done) => {
+ spyOn(vm.service, 'fetchMergeActionsContent').and.returnValue(returnPromise('hello world'));
+
+ vm.fetchActionsContent();
+
+ setTimeout(() => {
+ expect(vm.service.fetchMergeActionsContent).toHaveBeenCalled();
+ expect(document.body.textContent).toContain('hello world');
+ done();
+ }, 333);
+ });
+ });
+
+ describe('bindEventHubListeners', () => {
+ it('should bind eventHub listeners', () => {
+ spyOn(vm, 'checkStatus').and.returnValue(() => {});
+ spyOn(vm.service, 'checkStatus').and.returnValue(returnPromise(mockData));
+ spyOn(vm, 'fetchActionsContent');
+ spyOn(vm.mr, 'setData');
+ spyOn(vm, 'resumePolling');
+ spyOn(vm, 'stopPolling');
+ spyOn(eventHub, '$on');
+
+ vm.bindEventHubListeners();
+
+ eventHub.$emit('SetBranchRemoveFlag', ['flag']);
+ expect(vm.mr.isRemovingSourceBranch).toEqual('flag');
+
+ eventHub.$emit('FailedToMerge');
+ expect(vm.mr.state).toEqual('failedToMerge');
+
+ eventHub.$emit('UpdateWidgetData', mockData);
+ expect(vm.mr.setData).toHaveBeenCalledWith(mockData);
+
+ eventHub.$emit('EnablePolling');
+ expect(vm.resumePolling).toHaveBeenCalled();
+
+ eventHub.$emit('DisablePolling');
+ expect(vm.stopPolling).toHaveBeenCalled();
+
+ const listenersWithServiceRequest = {
+ MRWidgetUpdateRequested: true,
+ FetchActionsContent: true,
+ };
+
+ const allArgs = eventHub.$on.calls.allArgs();
+ allArgs.forEach((params) => {
+ const eventName = params[0];
+ const callback = params[1];
+
+ if (listenersWithServiceRequest[eventName]) {
+ listenersWithServiceRequest[eventName] = callback;
+ }
+ });
+
+ listenersWithServiceRequest.MRWidgetUpdateRequested();
+ expect(vm.checkStatus).toHaveBeenCalled();
+
+ listenersWithServiceRequest.FetchActionsContent();
+ expect(vm.fetchActionsContent).toHaveBeenCalled();
+ });
+ });
+
+ describe('handleMounted', () => {
+ it('should call required methods to do the initial kick-off', () => {
+ spyOn(vm, 'checkStatus');
+ spyOn(vm, 'initDeploymentsPolling');
+ spyOn(vm, 'setFavicon');
+
+ vm.handleMounted();
+
+ expect(vm.checkStatus).toHaveBeenCalled();
+ expect(vm.setFavicon).toHaveBeenCalled();
+ expect(vm.initDeploymentsPolling).toHaveBeenCalled();
+ });
+ });
+
+ describe('setFavicon', () => {
+ it('should call setFavicon method', () => {
+ spyOn(gl.utils, 'setFavicon');
+ vm.setFavicon();
+
+ expect(gl.utils.setFavicon).toHaveBeenCalledWith(vm.mr.ciStatusFaviconPath);
+ });
+
+ it('should not call setFavicon when there is no ciStatusFaviconPath', () => {
+ spyOn(gl.utils, 'setFavicon');
+ vm.mr.ciStatusFaviconPath = null;
+ vm.setFavicon();
+
+ expect(gl.utils.setFavicon).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('resumePolling', () => {
+ it('should call stopTimer on pollingInterval', () => {
+ spyOn(vm.pollingInterval, 'resume');
+
+ vm.resumePolling();
+ expect(vm.pollingInterval.resume).toHaveBeenCalled();
+ });
+ });
+
+ describe('stopPolling', () => {
+ it('should call stopTimer on pollingInterval', () => {
+ spyOn(vm.pollingInterval, 'stopTimer');
+
+ vm.stopPolling();
+ expect(vm.pollingInterval.stopTimer).toHaveBeenCalled();
+ });
+ });
+
+ describe('createService', () => {
+ it('should instantiate a Service', () => {
+ const endpoints = {
+ mergePath: '/nice/path',
+ mergeCheckPath: '/nice/path',
+ cancelAutoMergePath: '/nice/path',
+ removeWIPPath: '/nice/path',
+ sourceBranchPath: '/nice/path',
+ ciEnvironmentsStatusPath: '/nice/path',
+ statusPath: '/nice/path',
+ mergeActionsContentPath: '/nice/path',
+ };
+
+ const serviceInstance = vm.createService(endpoints);
+ const isInstanceOfMRService = serviceInstance instanceof MRWidgetService;
+ expect(isInstanceOfMRService).toBe(true);
+ Object.keys(serviceInstance).forEach((key) => {
+ expect(serviceInstance[key]).toBeDefined();
+ });
+ });
+ });
+ });
+
+ describe('components', () => {
+ it('should register all components', () => {
+ const comps = mrWidgetOptions.components;
+ expect(comps['mr-widget-header']).toBeDefined();
+ expect(comps['mr-widget-merge-help']).toBeDefined();
+ expect(comps['mr-widget-pipeline']).toBeDefined();
+ expect(comps['mr-widget-deployment']).toBeDefined();
+ expect(comps['mr-widget-related-links']).toBeDefined();
+ expect(comps['mr-widget-merged']).toBeDefined();
+ expect(comps['mr-widget-closed']).toBeDefined();
+ expect(comps['mr-widget-locked']).toBeDefined();
+ expect(comps['mr-widget-failed-to-merge']).toBeDefined();
+ expect(comps['mr-widget-wip']).toBeDefined();
+ expect(comps['mr-widget-archived']).toBeDefined();
+ expect(comps['mr-widget-conflicts']).toBeDefined();
+ expect(comps['mr-widget-nothing-to-merge']).toBeDefined();
+ expect(comps['mr-widget-not-allowed']).toBeDefined();
+ expect(comps['mr-widget-missing-branch']).toBeDefined();
+ expect(comps['mr-widget-ready-to-merge']).toBeDefined();
+ expect(comps['mr-widget-checking']).toBeDefined();
+ expect(comps['mr-widget-unresolved-discussions']).toBeDefined();
+ expect(comps['mr-widget-pipeline-blocked']).toBeDefined();
+ expect(comps['mr-widget-pipeline-failed']).toBeDefined();
+ expect(comps['mr-widget-merge-when-pipeline-succeeds']).toBeDefined();
+ });
+ });
+});
diff --git a/spec/javascripts/vue_mr_widget/services/mr_widget_service_spec.js b/spec/javascripts/vue_mr_widget/services/mr_widget_service_spec.js
new file mode 100644
index 00000000000..b63633c03b8
--- /dev/null
+++ b/spec/javascripts/vue_mr_widget/services/mr_widget_service_spec.js
@@ -0,0 +1,46 @@
+import Vue from 'vue';
+import VueResource from 'vue-resource';
+import MRWidgetService from '~/vue_merge_request_widget/services/mr_widget_service';
+
+Vue.use(VueResource);
+
+describe('MRWidgetService', () => {
+ const mr = {
+ mergePath: './',
+ mergeCheckPath: './',
+ cancelAutoMergePath: './',
+ removeWIPPath: './',
+ sourceBranchPath: './',
+ ciEnvironmentsStatusPath: './',
+ statusPath: './',
+ mergeActionsContentPath: './',
+ isServiceStore: true,
+ };
+
+ it('should have store and resources created in constructor', () => {
+ const service = new MRWidgetService(mr);
+
+ expect(service.mergeResource).toBeDefined();
+ expect(service.mergeCheckResource).toBeDefined();
+ expect(service.cancelAutoMergeResource).toBeDefined();
+ expect(service.removeWIPResource).toBeDefined();
+ expect(service.removeSourceBranchResource).toBeDefined();
+ expect(service.deploymentsResource).toBeDefined();
+ expect(service.pollResource).toBeDefined();
+ expect(service.mergeActionsContentResource).toBeDefined();
+ });
+
+ it('should have methods defined', () => {
+ const service = new MRWidgetService(mr);
+
+ expect(service.merge()).toBeDefined();
+ expect(service.cancelAutomaticMerge()).toBeDefined();
+ expect(service.removeWIP()).toBeDefined();
+ expect(service.removeSourceBranch()).toBeDefined();
+ expect(service.fetchDeployments()).toBeDefined();
+ expect(service.poll()).toBeDefined();
+ expect(service.checkStatus()).toBeDefined();
+ expect(service.fetchMergeActionsContent()).toBeDefined();
+ expect(MRWidgetService.stopEnvironment()).toBeDefined();
+ });
+});
diff --git a/spec/javascripts/vue_mr_widget/stores/get_state_key_spec.js b/spec/javascripts/vue_mr_widget/stores/get_state_key_spec.js
new file mode 100644
index 00000000000..9a331d99865
--- /dev/null
+++ b/spec/javascripts/vue_mr_widget/stores/get_state_key_spec.js
@@ -0,0 +1,65 @@
+import getStateKey from '~/vue_merge_request_widget/stores/get_state_key';
+
+describe('getStateKey', () => {
+ it('should return proper state name', () => {
+ const context = {
+ mergeStatus: 'checked',
+ mergeWhenPipelineSucceeds: false,
+ canMerge: true,
+ onlyAllowMergeIfPipelineSucceeds: false,
+ isPipelineFailed: false,
+ hasMergeableDiscussionsState: false,
+ isPipelineBlocked: false,
+ canBeMerged: false,
+ };
+ const data = {
+ project_archived: false,
+ branch_missing: false,
+ commits_count: 2,
+ has_conflicts: false,
+ work_in_progress: false,
+ };
+ const bound = getStateKey.bind(context, data);
+ expect(bound()).toEqual(null);
+
+ context.canBeMerged = true;
+ expect(bound()).toEqual('readyToMerge');
+
+ context.hasSHAChanged = true;
+ expect(bound()).toEqual('shaMismatch');
+
+ context.isPipelineBlocked = true;
+ expect(bound()).toEqual('pipelineBlocked');
+
+ context.hasMergeableDiscussionsState = true;
+ expect(bound()).toEqual('unresolvedDiscussions');
+
+ context.onlyAllowMergeIfPipelineSucceeds = true;
+ context.isPipelineFailed = true;
+ expect(bound()).toEqual('pipelineFailed');
+
+ context.canMerge = false;
+ expect(bound()).toEqual('notAllowedToMerge');
+
+ context.mergeWhenPipelineSucceeds = true;
+ expect(bound()).toEqual('mergeWhenPipelineSucceeds');
+
+ data.work_in_progress = true;
+ expect(bound()).toEqual('workInProgress');
+
+ data.has_conflicts = true;
+ expect(bound()).toEqual('conflicts');
+
+ context.mergeStatus = 'unchecked';
+ expect(bound()).toEqual('checking');
+
+ data.commits_count = 0;
+ expect(bound()).toEqual('nothingToMerge');
+
+ data.branch_missing = true;
+ expect(bound()).toEqual('missingBranch');
+
+ data.project_archived = true;
+ expect(bound()).toEqual('archived');
+ });
+});
diff --git a/spec/javascripts/vue_mr_widget/stores/mr_widget_store_spec.js b/spec/javascripts/vue_mr_widget/stores/mr_widget_store_spec.js
new file mode 100644
index 00000000000..56dd0198ae2
--- /dev/null
+++ b/spec/javascripts/vue_mr_widget/stores/mr_widget_store_spec.js
@@ -0,0 +1,22 @@
+import MergeRequestStore from '~/vue_merge_request_widget/stores/mr_widget_store';
+import mockData from '../mock_data';
+
+describe('MergeRequestStore', () => {
+ describe('setData', () => {
+ let store;
+
+ beforeEach(() => {
+ store = new MergeRequestStore(mockData);
+ });
+
+ it('should set hasSHAChanged when the diff SHA changes', () => {
+ store.setData({ ...mockData, diff_head_sha: 'a-different-string' });
+ expect(store.hasSHAChanged).toBe(true);
+ });
+
+ it('should not set hasSHAChanged when other data changes', () => {
+ store.setData({ ...mockData, work_in_progress: !mockData.work_in_progress });
+ expect(store.hasSHAChanged).toBe(false);
+ });
+ });
+});
diff --git a/spec/javascripts/vue_shared/ci_action_icons_spec.js b/spec/javascripts/vue_shared/ci_action_icons_spec.js
new file mode 100644
index 00000000000..3d53a5ab24d
--- /dev/null
+++ b/spec/javascripts/vue_shared/ci_action_icons_spec.js
@@ -0,0 +1,27 @@
+import getActionIcon from '~/vue_shared/ci_action_icons';
+import cancelSVG from 'icons/_icon_action_cancel.svg';
+import retrySVG from 'icons/_icon_action_retry.svg';
+import playSVG from 'icons/_icon_action_play.svg';
+import stopSVG from 'icons/_icon_action_stop.svg';
+
+describe('getActionIcon', () => {
+ it('should return an empty string', () => {
+ expect(getActionIcon()).toEqual('');
+ });
+
+ it('should return cancel svg', () => {
+ expect(getActionIcon('icon_action_cancel')).toEqual(cancelSVG);
+ });
+
+ it('should return retry svg', () => {
+ expect(getActionIcon('icon_action_retry')).toEqual(retrySVG);
+ });
+
+ it('should return play svg', () => {
+ expect(getActionIcon('icon_action_play')).toEqual(playSVG);
+ });
+
+ it('should render stop svg', () => {
+ expect(getActionIcon('icon_action_stop')).toEqual(stopSVG);
+ });
+});
diff --git a/spec/javascripts/vue_shared/ci_status_icon_spec.js b/spec/javascripts/vue_shared/ci_status_icon_spec.js
new file mode 100644
index 00000000000..b6621d6054d
--- /dev/null
+++ b/spec/javascripts/vue_shared/ci_status_icon_spec.js
@@ -0,0 +1,27 @@
+import { borderlessStatusIconEntityMap, statusIconEntityMap } from '~/vue_shared/ci_status_icons';
+
+describe('CI status icons', () => {
+ const statuses = [
+ 'icon_status_canceled',
+ 'icon_status_created',
+ 'icon_status_failed',
+ 'icon_status_manual',
+ 'icon_status_pending',
+ 'icon_status_running',
+ 'icon_status_skipped',
+ 'icon_status_success',
+ 'icon_status_warning',
+ ];
+
+ it('should have a dictionary for borderless icons', () => {
+ statuses.forEach((status) => {
+ expect(borderlessStatusIconEntityMap[status]).toBeDefined();
+ });
+ });
+
+ it('should have a dictionary for icons', () => {
+ statuses.forEach((status) => {
+ expect(statusIconEntityMap[status]).toBeDefined();
+ });
+ });
+});
diff --git a/spec/javascripts/vue_shared/components/ci_badge_link_spec.js b/spec/javascripts/vue_shared/components/ci_badge_link_spec.js
new file mode 100644
index 00000000000..daed4da3e15
--- /dev/null
+++ b/spec/javascripts/vue_shared/components/ci_badge_link_spec.js
@@ -0,0 +1,89 @@
+import Vue from 'vue';
+import ciBadge from '~/vue_shared/components/ci_badge_link.vue';
+
+describe('CI Badge Link Component', () => {
+ let CIBadge;
+
+ const statuses = {
+ canceled: {
+ text: 'canceled',
+ label: 'canceled',
+ group: 'canceled',
+ icon: 'icon_status_canceled',
+ details_path: 'status/canceled',
+ },
+ created: {
+ text: 'created',
+ label: 'created',
+ group: 'created',
+ icon: 'icon_status_created',
+ details_path: 'status/created',
+ },
+ failed: {
+ text: 'failed',
+ label: 'failed',
+ group: 'failed',
+ icon: 'icon_status_failed',
+ details_path: 'status/failed',
+ },
+ manual: {
+ text: 'manual',
+ label: 'manual action',
+ group: 'manual',
+ icon: 'icon_status_manual',
+ details_path: 'status/manual',
+ },
+ pending: {
+ text: 'pending',
+ label: 'pending',
+ group: 'pending',
+ icon: 'icon_status_pending',
+ details_path: 'status/pending',
+ },
+ running: {
+ text: 'running',
+ label: 'running',
+ group: 'running',
+ icon: 'icon_status_running',
+ details_path: 'status/running',
+ },
+ skipped: {
+ text: 'skipped',
+ label: 'skipped',
+ group: 'skipped',
+ icon: 'icon_status_skipped',
+ details_path: 'status/skipped',
+ },
+ success_warining: {
+ text: 'passed',
+ label: 'passed',
+ group: 'success_with_warnings',
+ icon: 'icon_status_warning',
+ details_path: 'status/warning',
+ },
+ success: {
+ text: 'passed',
+ label: 'passed',
+ group: 'passed',
+ icon: 'icon_status_success',
+ details_path: 'status/passed',
+ },
+ };
+
+ it('should render each status badge', () => {
+ CIBadge = Vue.extend(ciBadge);
+ Object.keys(statuses).map((status) => {
+ const vm = new CIBadge({
+ propsData: {
+ status: statuses[status],
+ },
+ }).$mount();
+
+ expect(vm.$el.getAttribute('href')).toEqual(statuses[status].details_path);
+ expect(vm.$el.textContent.trim()).toEqual(statuses[status].text);
+ expect(vm.$el.getAttribute('class')).toEqual(`ci-status ci-${statuses[status].group}`);
+ expect(vm.$el.querySelector('svg')).toBeDefined();
+ return vm;
+ });
+ });
+});
diff --git a/spec/javascripts/vue_shared/components/ci_icon_spec.js b/spec/javascripts/vue_shared/components/ci_icon_spec.js
new file mode 100644
index 00000000000..d8664408595
--- /dev/null
+++ b/spec/javascripts/vue_shared/components/ci_icon_spec.js
@@ -0,0 +1,139 @@
+import Vue from 'vue';
+import ciIcon from '~/vue_shared/components/ci_icon.vue';
+
+describe('CI Icon component', () => {
+ let CiIcon;
+ beforeEach(() => {
+ CiIcon = Vue.extend(ciIcon);
+ });
+
+ it('should render a span element with an svg', () => {
+ const component = new CiIcon({
+ propsData: {
+ status: {
+ icon: 'icon_status_success',
+ },
+ },
+ }).$mount();
+
+ expect(component.$el.tagName).toEqual('SPAN');
+ expect(component.$el.querySelector('span > svg')).toBeDefined();
+ });
+
+ it('should render a success status', () => {
+ const component = new CiIcon({
+ propsData: {
+ status: {
+ icon: 'icon_status_success',
+ group: 'success',
+ },
+ },
+ }).$mount();
+
+ expect(component.$el.classList.contains('ci-status-icon-success')).toEqual(true);
+ });
+
+ it('should render a failed status', () => {
+ const component = new CiIcon({
+ propsData: {
+ status: {
+ icon: 'icon_status_failed',
+ group: 'failed',
+ },
+ },
+ }).$mount();
+
+ expect(component.$el.classList.contains('ci-status-icon-failed')).toEqual(true);
+ });
+
+ it('should render success with warnings status', () => {
+ const component = new CiIcon({
+ propsData: {
+ status: {
+ icon: 'icon_status_warning',
+ group: 'warning',
+ },
+ },
+ }).$mount();
+
+ expect(component.$el.classList.contains('ci-status-icon-warning')).toEqual(true);
+ });
+
+ it('should render pending status', () => {
+ const component = new CiIcon({
+ propsData: {
+ status: {
+ icon: 'icon_status_pending',
+ group: 'pending',
+ },
+ },
+ }).$mount();
+
+ expect(component.$el.classList.contains('ci-status-icon-pending')).toEqual(true);
+ });
+
+ it('should render running status', () => {
+ const component = new CiIcon({
+ propsData: {
+ status: {
+ icon: 'icon_status_running',
+ group: 'running',
+ },
+ },
+ }).$mount();
+
+ expect(component.$el.classList.contains('ci-status-icon-running')).toEqual(true);
+ });
+
+ it('should render created status', () => {
+ const component = new CiIcon({
+ propsData: {
+ status: {
+ icon: 'icon_status_created',
+ group: 'created',
+ },
+ },
+ }).$mount();
+
+ expect(component.$el.classList.contains('ci-status-icon-created')).toEqual(true);
+ });
+
+ it('should render skipped status', () => {
+ const component = new CiIcon({
+ propsData: {
+ status: {
+ icon: 'icon_status_skipped',
+ group: 'skipped',
+ },
+ },
+ }).$mount();
+
+ expect(component.$el.classList.contains('ci-status-icon-skipped')).toEqual(true);
+ });
+
+ it('should render canceled status', () => {
+ const component = new CiIcon({
+ propsData: {
+ status: {
+ icon: 'icon_status_canceled',
+ group: 'canceled',
+ },
+ },
+ }).$mount();
+
+ expect(component.$el.classList.contains('ci-status-icon-canceled')).toEqual(true);
+ });
+
+ it('should render status for manual action', () => {
+ const component = new CiIcon({
+ propsData: {
+ status: {
+ icon: 'icon_status_manual',
+ group: 'manual',
+ },
+ },
+ }).$mount();
+
+ expect(component.$el.classList.contains('ci-status-icon-manual')).toEqual(true);
+ });
+});
diff --git a/spec/javascripts/vue_shared/components/commit_spec.js b/spec/javascripts/vue_shared/components/commit_spec.js
index df547299d75..242010ba688 100644
--- a/spec/javascripts/vue_shared/components/commit_spec.js
+++ b/spec/javascripts/vue_shared/components/commit_spec.js
@@ -61,16 +61,16 @@ describe('Commit component', () => {
});
it('should render a link to the ref url', () => {
- expect(component.$el.querySelector('.branch-name').getAttribute('href')).toEqual(props.commitRef.ref_url);
+ expect(component.$el.querySelector('.ref-name').getAttribute('href')).toEqual(props.commitRef.ref_url);
});
it('should render the ref name', () => {
- expect(component.$el.querySelector('.branch-name').textContent).toContain(props.commitRef.name);
+ expect(component.$el.querySelector('.ref-name').textContent).toContain(props.commitRef.name);
});
it('should render the commit short sha with a link to the commit url', () => {
- expect(component.$el.querySelector('.commit-id').getAttribute('href')).toEqual(props.commitUrl);
- expect(component.$el.querySelector('.commit-id').textContent).toContain(props.shortSha);
+ expect(component.$el.querySelector('.commit-sha').getAttribute('href')).toEqual(props.commitUrl);
+ expect(component.$el.querySelector('.commit-sha').textContent).toContain(props.shortSha);
});
it('should render the given commitIconSvg', () => {
diff --git a/spec/javascripts/vue_shared/components/loading_icon_spec.js b/spec/javascripts/vue_shared/components/loading_icon_spec.js
new file mode 100644
index 00000000000..1baf3537741
--- /dev/null
+++ b/spec/javascripts/vue_shared/components/loading_icon_spec.js
@@ -0,0 +1,53 @@
+import Vue from 'vue';
+import loadingIcon from '~/vue_shared/components/loading_icon.vue';
+
+describe('Loading Icon Component', () => {
+ let LoadingIconComponent;
+
+ beforeEach(() => {
+ LoadingIconComponent = Vue.extend(loadingIcon);
+ });
+
+ it('should render a spinner font awesome icon', () => {
+ const component = new LoadingIconComponent().$mount();
+
+ expect(
+ component.$el.querySelector('i').getAttribute('class'),
+ ).toEqual('fa fa-spin fa-spinner fa-1x');
+
+ expect(component.$el.tagName).toEqual('DIV');
+ expect(component.$el.classList.contains('text-center')).toEqual(true);
+ });
+
+ it('should render accessibility attributes', () => {
+ const component = new LoadingIconComponent().$mount();
+
+ const icon = component.$el.querySelector('i');
+ expect(icon.getAttribute('aria-hidden')).toEqual('true');
+ expect(icon.getAttribute('aria-label')).toEqual('Loading');
+ });
+
+ it('should render the provided label', () => {
+ const component = new LoadingIconComponent({
+ propsData: {
+ label: 'This is a loading icon',
+ },
+ }).$mount();
+
+ expect(
+ component.$el.querySelector('i').getAttribute('aria-label'),
+ ).toEqual('This is a loading icon');
+ });
+
+ it('should render the provided size', () => {
+ const component = new LoadingIconComponent({
+ propsData: {
+ size: '2',
+ },
+ }).$mount();
+
+ expect(
+ component.$el.querySelector('i').classList.contains('fa-2x'),
+ ).toEqual(true);
+ });
+});
diff --git a/spec/javascripts/vue_shared/components/memory_graph_spec.js b/spec/javascripts/vue_shared/components/memory_graph_spec.js
new file mode 100644
index 00000000000..d46a3f2328e
--- /dev/null
+++ b/spec/javascripts/vue_shared/components/memory_graph_spec.js
@@ -0,0 +1,143 @@
+import Vue from 'vue';
+import memoryGraphComponent from '~/vue_shared/components/memory_graph';
+import { mockMetrics, mockMedian, mockMedianIndex } from './mock_data';
+
+const defaultHeight = '25';
+const defaultWidth = '100';
+
+const createComponent = () => {
+ const Component = Vue.extend(memoryGraphComponent);
+
+ return new Component({
+ el: document.createElement('div'),
+ propsData: {
+ metrics: [],
+ deploymentTime: 0,
+ width: '',
+ height: '',
+ pathD: '',
+ pathViewBox: '',
+ dotX: '',
+ dotY: '',
+ },
+ });
+};
+
+describe('MemoryGraph', () => {
+ let vm;
+ let el;
+
+ beforeEach(() => {
+ vm = createComponent();
+ el = vm.$el;
+ });
+
+ describe('props', () => {
+ it('should have props with defaults', (done) => {
+ const { metrics, deploymentTime, width, height } = memoryGraphComponent.props;
+
+ Vue.nextTick(() => {
+ const typeClassMatcher = (propItem, expectedType) => {
+ const PropItemTypeClass = propItem.type;
+ expect(new PropItemTypeClass() instanceof expectedType).toBeTruthy();
+ expect(propItem.required).toBeTruthy();
+ };
+
+ typeClassMatcher(metrics, Array);
+ typeClassMatcher(deploymentTime, Number);
+ typeClassMatcher(width, String);
+ typeClassMatcher(height, String);
+ done();
+ });
+ });
+ });
+
+ describe('data', () => {
+ it('should have default data', () => {
+ const data = memoryGraphComponent.data();
+ const dataValidator = (dataItem, expectedType, defaultVal) => {
+ expect(typeof dataItem).toBe(expectedType);
+ expect(dataItem).toBe(defaultVal);
+ };
+
+ dataValidator(data.pathD, 'string', '');
+ dataValidator(data.pathViewBox, 'string', '');
+ dataValidator(data.dotX, 'string', '');
+ dataValidator(data.dotY, 'string', '');
+ });
+ });
+
+ describe('computed', () => {
+ describe('getFormattedMedian', () => {
+ it('should show human readable median value based on provided median timestamp', () => {
+ vm.deploymentTime = mockMedian;
+ const formattedMedian = vm.getFormattedMedian;
+ expect(formattedMedian.indexOf('Deployed') > -1).toBeTruthy();
+ expect(formattedMedian.indexOf('ago') > -1).toBeTruthy();
+ });
+ });
+ });
+
+ describe('methods', () => {
+ describe('getMedianMetricIndex', () => {
+ it('should return index of closest metric timestamp to that of median', () => {
+ const matchingIndex = vm.getMedianMetricIndex(mockMedian, mockMetrics);
+ expect(matchingIndex).toBe(mockMedianIndex);
+ });
+ });
+
+ describe('getGraphPlotValues', () => {
+ it('should return Object containing values to plot graph', () => {
+ const plotValues = vm.getGraphPlotValues(mockMedian, mockMetrics);
+ expect(plotValues.pathD).toBeDefined();
+ expect(Array.isArray(plotValues.pathD)).toBeTruthy();
+
+ expect(plotValues.pathViewBox).toBeDefined();
+ expect(typeof plotValues.pathViewBox).toBe('object');
+
+ expect(plotValues.dotX).toBeDefined();
+ expect(typeof plotValues.dotX).toBe('number');
+
+ expect(plotValues.dotY).toBeDefined();
+ expect(typeof plotValues.dotY).toBe('number');
+ });
+ });
+ });
+
+ describe('template', () => {
+ it('should render template elements correctly', () => {
+ expect(el.classList.contains('memory-graph-container')).toBeTruthy();
+ expect(el.querySelector('svg')).toBeDefined();
+ });
+
+ it('should render graph when renderGraph is called internally', (done) => {
+ const { pathD, pathViewBox, dotX, dotY } = vm.getGraphPlotValues(mockMedian, mockMetrics);
+ vm.height = defaultHeight;
+ vm.width = defaultWidth;
+ vm.pathD = `M ${pathD}`;
+ vm.pathViewBox = `0 0 ${pathViewBox.lineWidth} ${pathViewBox.diff}`;
+ vm.dotX = dotX;
+ vm.dotY = dotY;
+
+ Vue.nextTick(() => {
+ const svgEl = el.querySelector('svg');
+ expect(svgEl).toBeDefined();
+ expect(svgEl.getAttribute('height')).toBe(defaultHeight);
+ expect(svgEl.getAttribute('width')).toBe(defaultWidth);
+
+ const pathEl = el.querySelector('path');
+ expect(pathEl).toBeDefined();
+ expect(pathEl.getAttribute('d')).toBe(`M ${pathD}`);
+ expect(pathEl.getAttribute('viewBox')).toBe(`0 0 ${pathViewBox.lineWidth} ${pathViewBox.diff}`);
+
+ const circleEl = el.querySelector('circle');
+ expect(circleEl).toBeDefined();
+ expect(circleEl.getAttribute('r')).toBe('1.5');
+ expect(circleEl.getAttribute('tranform')).toBe('translate(0 -1)');
+ expect(circleEl.getAttribute('cx')).toBe(`${dotX}`);
+ expect(circleEl.getAttribute('cy')).toBe(`${dotY}`);
+ done();
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/vue_shared/components/mock_data.js b/spec/javascripts/vue_shared/components/mock_data.js
new file mode 100644
index 00000000000..0d781bdca74
--- /dev/null
+++ b/spec/javascripts/vue_shared/components/mock_data.js
@@ -0,0 +1,69 @@
+/* eslint-disable */
+
+export const mockMetrics = [
+ [1493716685, '4.30859375'],
+ [1493716745, '4.30859375'],
+ [1493716805, '4.30859375'],
+ [1493716865, '4.30859375'],
+ [1493716925, '4.30859375'],
+ [1493716985, '4.30859375'],
+ [1493717045, '4.30859375'],
+ [1493717105, '4.30859375'],
+ [1493717165, '4.30859375'],
+ [1493717225, '4.30859375'],
+ [1493717285, '4.30859375'],
+ [1493717345, '4.30859375'],
+ [1493717405, '4.30859375'],
+ [1493717465, '4.30859375'],
+ [1493717525, '4.30859375'],
+ [1493717585, '4.30859375'],
+ [1493717645, '4.30859375'],
+ [1493717705, '4.30859375'],
+ [1493717765, '4.30859375'],
+ [1493717825, '4.30859375'],
+ [1493717885, '4.30859375'],
+ [1493717945, '4.30859375'],
+ [1493718005, '4.30859375'],
+ [1493718065, '4.30859375'],
+ [1493718125, '4.30859375'],
+ [1493718185, '4.30859375'],
+ [1493718245, '4.30859375'],
+ [1493718305, '4.234375'],
+ [1493718365, '4.234375'],
+ [1493718425, '4.234375'],
+ [1493718485, '4.234375'],
+ [1493718545, '4.243489583333333'],
+ [1493718605, '4.2109375'],
+ [1493718665, '4.2109375'],
+ [1493718725, '4.2109375'],
+ [1493718785, '4.26171875'],
+ [1493718845, '4.26171875'],
+ [1493718905, '4.26171875'],
+ [1493718965, '4.26171875'],
+ [1493719025, '4.26171875'],
+ [1493719085, '4.26171875'],
+ [1493719145, '4.26171875'],
+ [1493719205, '4.26171875'],
+ [1493719265, '4.26171875'],
+ [1493719325, '4.26171875'],
+ [1493719385, '4.26171875'],
+ [1493719445, '4.26171875'],
+ [1493719505, '4.26171875'],
+ [1493719565, '4.26171875'],
+ [1493719625, '4.26171875'],
+ [1493719685, '4.26171875'],
+ [1493719745, '4.26171875'],
+ [1493719805, '4.26171875'],
+ [1493719865, '4.26171875'],
+ [1493719925, '4.26171875'],
+ [1493719985, '4.26171875'],
+ [1493720045, '4.26171875'],
+ [1493720105, '4.26171875'],
+ [1493720165, '4.26171875'],
+ [1493720225, '4.26171875'],
+ [1493720285, '4.26171875'],
+];
+
+export const mockMedian = 1493718485;
+
+export const mockMedianIndex = 30;
diff --git a/spec/javascripts/vue_shared/components/pipelines_table_row_spec.js b/spec/javascripts/vue_shared/components/pipelines_table_row_spec.js
index 699625cdbb7..14280751053 100644
--- a/spec/javascripts/vue_shared/components/pipelines_table_row_spec.js
+++ b/spec/javascripts/vue_shared/components/pipelines_table_row_spec.js
@@ -1,27 +1,47 @@
import Vue from 'vue';
import tableRowComp from '~/vue_shared/components/pipelines_table_row';
-import pipeline from '../../commit/pipelines/mock_data';
describe('Pipelines Table Row', () => {
- let component;
-
- beforeEach(() => {
+ const jsonFixtureName = 'pipelines/pipelines.json';
+ const buildComponent = (pipeline) => {
const PipelinesTableRowComponent = Vue.extend(tableRowComp);
-
- component = new PipelinesTableRowComponent({
+ return new PipelinesTableRowComponent({
el: document.querySelector('.test-dom-element'),
propsData: {
pipeline,
service: {},
},
}).$mount();
+ };
+
+ let component;
+ let pipeline;
+ let pipelineWithoutAuthor;
+ let pipelineWithoutCommit;
+
+ preloadFixtures(jsonFixtureName);
+
+ beforeEach(() => {
+ const pipelines = getJSONFixture(jsonFixtureName).pipelines;
+ pipeline = pipelines.find(p => p.id === 1);
+ pipelineWithoutAuthor = pipelines.find(p => p.id === 2);
+ pipelineWithoutCommit = pipelines.find(p => p.id === 3);
+ });
+
+ afterEach(() => {
+ component.$destroy();
});
it('should render a table row', () => {
+ component = buildComponent(pipeline);
expect(component.$el).toEqual('TR');
});
describe('status column', () => {
+ beforeEach(() => {
+ component = buildComponent(pipeline);
+ });
+
it('should render a pipeline link', () => {
expect(
component.$el.querySelector('td.commit-link a').getAttribute('href'),
@@ -36,6 +56,10 @@ describe('Pipelines Table Row', () => {
});
describe('information column', () => {
+ beforeEach(() => {
+ component = buildComponent(pipeline);
+ });
+
it('should render a pipeline link', () => {
expect(
component.$el.querySelector('td:nth-child(2) a').getAttribute('href'),
@@ -63,13 +87,59 @@ describe('Pipelines Table Row', () => {
describe('commit column', () => {
it('should render link to commit', () => {
- expect(
- component.$el.querySelector('td:nth-child(3) .commit-id').getAttribute('href'),
- ).toEqual(pipeline.commit.commit_path);
+ component = buildComponent(pipeline);
+
+ const commitLink = component.$el.querySelector('.branch-commit .commit-sha');
+ expect(commitLink.getAttribute('href')).toEqual(pipeline.commit.commit_path);
+ });
+
+ const findElements = () => {
+ const commitTitleElement = component.$el.querySelector('.branch-commit .commit-title');
+ const commitAuthorElement = commitTitleElement.querySelector('a.avatar-image-container');
+
+ if (!commitAuthorElement) {
+ return { commitAuthorElement };
+ }
+
+ const commitAuthorLink = commitAuthorElement.getAttribute('href');
+ const commitAuthorName = commitAuthorElement.querySelector('img.avatar').getAttribute('title');
+
+ return { commitAuthorElement, commitAuthorLink, commitAuthorName };
+ };
+
+ it('renders nothing without commit', () => {
+ expect(pipelineWithoutCommit.commit).toBe(null);
+ component = buildComponent(pipelineWithoutCommit);
+
+ const { commitAuthorElement } = findElements();
+
+ expect(commitAuthorElement).toBe(null);
+ });
+
+ it('renders commit author', () => {
+ component = buildComponent(pipeline);
+ const { commitAuthorLink, commitAuthorName } = findElements();
+
+ expect(commitAuthorLink).toEqual(pipeline.commit.author.web_url);
+ expect(commitAuthorName).toEqual(pipeline.commit.author.username);
+ });
+
+ it('renders commit with unregistered author', () => {
+ expect(pipelineWithoutAuthor.commit.author).toBe(null);
+ component = buildComponent(pipelineWithoutAuthor);
+
+ const { commitAuthorLink, commitAuthorName } = findElements();
+
+ expect(commitAuthorLink).toEqual(`mailto:${pipelineWithoutAuthor.commit.author_email}`);
+ expect(commitAuthorName).toEqual(pipelineWithoutAuthor.commit.author_name);
});
});
describe('stages column', () => {
+ beforeEach(() => {
+ component = buildComponent(pipeline);
+ });
+
it('should render an icon for each stage', () => {
expect(
component.$el.querySelectorAll('td:nth-child(4) .js-builds-dropdown-button').length,
@@ -78,6 +148,10 @@ describe('Pipelines Table Row', () => {
});
describe('actions column', () => {
+ beforeEach(() => {
+ component = buildComponent(pipeline);
+ });
+
it('should render the provided actions', () => {
expect(
component.$el.querySelectorAll('td:nth-child(6) ul li').length,
diff --git a/spec/javascripts/vue_shared/components/pipelines_table_spec.js b/spec/javascripts/vue_shared/components/pipelines_table_spec.js
index 4d3ced944d7..6cc178b8f1d 100644
--- a/spec/javascripts/vue_shared/components/pipelines_table_spec.js
+++ b/spec/javascripts/vue_shared/components/pipelines_table_spec.js
@@ -1,13 +1,19 @@
import Vue from 'vue';
import pipelinesTableComp from '~/vue_shared/components/pipelines_table';
import '~/lib/utils/datetime_utility';
-import pipeline from '../../commit/pipelines/mock_data';
describe('Pipelines Table', () => {
+ const jsonFixtureName = 'pipelines/pipelines.json';
+
+ let pipeline;
let PipelinesTableComponent;
+ preloadFixtures(jsonFixtureName);
+
beforeEach(() => {
PipelinesTableComponent = Vue.extend(pipelinesTableComp);
+ const pipelines = getJSONFixture(jsonFixtureName).pipelines;
+ pipeline = pipelines.find(p => p.id === 1);
});
describe('table', () => {
diff --git a/spec/javascripts/vue_shared/components/table_pagination_spec.js b/spec/javascripts/vue_shared/components/table_pagination_spec.js
index 96038718191..895e1c585b4 100644
--- a/spec/javascripts/vue_shared/components/table_pagination_spec.js
+++ b/spec/javascripts/vue_shared/components/table_pagination_spec.js
@@ -1,5 +1,5 @@
import Vue from 'vue';
-import paginationComp from '~/vue_shared/components/table_pagination';
+import paginationComp from '~/vue_shared/components/table_pagination.vue';
import '~/lib/utils/common_utils';
describe('Pagination component', () => {
diff --git a/spec/javascripts/vue_shared/translate_spec.js b/spec/javascripts/vue_shared/translate_spec.js
new file mode 100644
index 00000000000..cbb3cbdff46
--- /dev/null
+++ b/spec/javascripts/vue_shared/translate_spec.js
@@ -0,0 +1,90 @@
+import Vue from 'vue';
+import Translate from '~/vue_shared/translate';
+
+Vue.use(Translate);
+
+describe('Vue translate filter', () => {
+ let el;
+
+ beforeEach(() => {
+ el = document.createElement('div');
+
+ document.body.appendChild(el);
+ });
+
+ it('translate single text', (done) => {
+ const comp = new Vue({
+ el,
+ template: `
+ <span>
+ {{ __('testing') }}
+ </span>
+ `,
+ }).$mount();
+
+ Vue.nextTick(() => {
+ expect(
+ comp.$el.textContent.trim(),
+ ).toBe('testing');
+
+ done();
+ });
+ });
+
+ it('translate plural text with single count', (done) => {
+ const comp = new Vue({
+ el,
+ template: `
+ <span>
+ {{ n__('%d day', '%d days', 1) }}
+ </span>
+ `,
+ }).$mount();
+
+ Vue.nextTick(() => {
+ expect(
+ comp.$el.textContent.trim(),
+ ).toBe('1 day');
+
+ done();
+ });
+ });
+
+ it('translate plural text with multiple count', (done) => {
+ const comp = new Vue({
+ el,
+ template: `
+ <span>
+ {{ n__('%d day', '%d days', 2) }}
+ </span>
+ `,
+ }).$mount();
+
+ Vue.nextTick(() => {
+ expect(
+ comp.$el.textContent.trim(),
+ ).toBe('2 days');
+
+ done();
+ });
+ });
+
+ it('translate plural without replacing any text', (done) => {
+ const comp = new Vue({
+ el,
+ template: `
+ <span>
+ {{ n__('day', 'days', 2) }}
+ </span>
+ `,
+ }).$mount();
+
+ Vue.nextTick(() => {
+ expect(
+ comp.$el.textContent.trim(),
+ ).toBe('days');
+
+ done();
+ });
+ });
+});
diff --git a/spec/lib/banzai/filter/emoji_filter_spec.rb b/spec/lib/banzai/filter/emoji_filter_spec.rb
index 707212e07fd..086a006c45f 100644
--- a/spec/lib/banzai/filter/emoji_filter_spec.rb
+++ b/spec/lib/banzai/filter/emoji_filter_spec.rb
@@ -68,9 +68,9 @@ describe Banzai::Filter::EmojiFilter, lib: true do
expect(doc.css('gl-emoji').size).to eq 1
end
- it 'matches multiple emoji in a row' do
+ it 'does not match multiple emoji in a row' do
doc = filter(':see_no_evil::hear_no_evil::speak_no_evil:')
- expect(doc.css('gl-emoji').size).to eq 3
+ expect(doc.css('gl-emoji').size).to eq 0
end
it 'unicode matches multiple emoji in a row' do
@@ -83,6 +83,12 @@ describe Banzai::Filter::EmojiFilter, lib: true do
expect(doc.css('gl-emoji').size).to eq 6
end
+ it 'does not match emoji in a string' do
+ doc = filter("'2a00:a4c0:100::1'")
+
+ expect(doc.css('gl-emoji').size).to eq 0
+ end
+
it 'has a data-name attribute' do
doc = filter(':-1:')
expect(doc.css('gl-emoji').first.attr('data-name')).to eq 'thumbsdown'
diff --git a/spec/lib/banzai/filter/external_link_filter_spec.rb b/spec/lib/banzai/filter/external_link_filter_spec.rb
index d9e4525cb28..6f6c215be87 100644
--- a/spec/lib/banzai/filter/external_link_filter_spec.rb
+++ b/spec/lib/banzai/filter/external_link_filter_spec.rb
@@ -1,5 +1,22 @@
require 'spec_helper'
+shared_examples 'an external link with rel attribute' do
+ it 'adds rel="nofollow" to external links' do
+ expect(doc.at_css('a')).to have_attribute('rel')
+ expect(doc.at_css('a')['rel']).to include 'nofollow'
+ end
+
+ it 'adds rel="noreferrer" to external links' do
+ expect(doc.at_css('a')).to have_attribute('rel')
+ expect(doc.at_css('a')['rel']).to include 'noreferrer'
+ end
+
+ it 'adds rel="noopener" to external links' do
+ expect(doc.at_css('a')).to have_attribute('rel')
+ expect(doc.at_css('a')['rel']).to include 'noopener'
+ end
+end
+
describe Banzai::Filter::ExternalLinkFilter, lib: true do
include FilterSpecHelper
@@ -22,49 +39,51 @@ describe Banzai::Filter::ExternalLinkFilter, lib: true do
context 'for root links on document' do
let(:doc) { filter %q(<a href="https://google.com/">Google</a>) }
- it 'adds rel="nofollow" to external links' do
- expect(doc.at_css('a')).to have_attribute('rel')
- expect(doc.at_css('a')['rel']).to include 'nofollow'
- end
-
- it 'adds rel="noreferrer" to external links' do
- expect(doc.at_css('a')).to have_attribute('rel')
- expect(doc.at_css('a')['rel']).to include 'noreferrer'
- end
+ it_behaves_like 'an external link with rel attribute'
end
context 'for nested links on document' do
let(:doc) { filter %q(<p><a href="https://google.com/">Google</a></p>) }
- it 'adds rel="nofollow" to external links' do
- expect(doc.at_css('a')).to have_attribute('rel')
- expect(doc.at_css('a')['rel']).to include 'nofollow'
+ it_behaves_like 'an external link with rel attribute'
+ end
+
+ context 'for invalid urls' do
+ it 'skips broken hrefs' do
+ doc = filter %q(<p><a href="don't crash on broken urls">Google</a></p>)
+ expected = %q(<p><a href="don't%20crash%20on%20broken%20urls">Google</a></p>)
+
+ expect(doc.to_html).to eq(expected)
end
+ end
+
+ context 'for links with a username' do
+ context 'with a valid username' do
+ let(:doc) { filter %q(<a href="https://user@google.com/">Google</a>) }
- it 'adds rel="noreferrer" to external links' do
- expect(doc.at_css('a')).to have_attribute('rel')
- expect(doc.at_css('a')['rel']).to include 'noreferrer'
+ it_behaves_like 'an external link with rel attribute'
+ end
+
+ context 'with an impersonated username' do
+ let(:internal) { Gitlab.config.gitlab.url }
+
+ let(:doc) { filter %Q(<a href="https://#{internal}@example.com" target="_blank">Reverse Tabnabbing</a>) }
+
+ it_behaves_like 'an external link with rel attribute'
end
end
context 'for non-lowercase scheme links' do
- let(:doc_with_http) { filter %q(<p><a href="httP://google.com/">Google</a></p>) }
- let(:doc_with_https) { filter %q(<p><a href="hTTpS://google.com/">Google</a></p>) }
-
- it 'adds rel="nofollow" to external links' do
- expect(doc_with_http.at_css('a')).to have_attribute('rel')
- expect(doc_with_https.at_css('a')).to have_attribute('rel')
+ context 'with http' do
+ let(:doc) { filter %q(<p><a href="httP://google.com/">Google</a></p>) }
- expect(doc_with_http.at_css('a')['rel']).to include 'nofollow'
- expect(doc_with_https.at_css('a')['rel']).to include 'nofollow'
+ it_behaves_like 'an external link with rel attribute'
end
- it 'adds rel="noreferrer" to external links' do
- expect(doc_with_http.at_css('a')).to have_attribute('rel')
- expect(doc_with_https.at_css('a')).to have_attribute('rel')
+ context 'with https' do
+ let(:doc) { filter %q(<p><a href="hTTpS://google.com/">Google</a></p>) }
- expect(doc_with_http.at_css('a')['rel']).to include 'noreferrer'
- expect(doc_with_https.at_css('a')['rel']).to include 'noreferrer'
+ it_behaves_like 'an external link with rel attribute'
end
it 'skips internal links' do
@@ -84,14 +103,6 @@ describe Banzai::Filter::ExternalLinkFilter, lib: true do
context 'for protocol-relative links' do
let(:doc) { filter %q(<p><a href="//google.com/">Google</a></p>) }
- it 'adds rel="nofollow" to external links' do
- expect(doc.at_css('a')).to have_attribute('rel')
- expect(doc.at_css('a')['rel']).to include 'nofollow'
- end
-
- it 'adds rel="noreferrer" to external links' do
- expect(doc.at_css('a')).to have_attribute('rel')
- expect(doc.at_css('a')['rel']).to include 'noreferrer'
- end
+ it_behaves_like 'an external link with rel attribute'
end
end
diff --git a/spec/lib/banzai/filter/issuable_state_filter_spec.rb b/spec/lib/banzai/filter/issuable_state_filter_spec.rb
index 600f3c123ed..9c2399815b9 100644
--- a/spec/lib/banzai/filter/issuable_state_filter_spec.rb
+++ b/spec/lib/banzai/filter/issuable_state_filter_spec.rb
@@ -6,11 +6,23 @@ describe Banzai::Filter::IssuableStateFilter, lib: true do
let(:user) { create(:user) }
let(:context) { { current_user: user, issuable_state_filter_enabled: true } }
+ let(:closed_issue) { create_issue(:closed) }
+ let(:project) { create(:empty_project, :public) }
+ let(:other_project) { create(:empty_project, :public) }
def create_link(text, data)
link_to(text, '', class: 'gfm has-tooltip', data: data)
end
+ def create_issue(state)
+ create(:issue, state, project: project)
+ end
+
+ def create_merge_request(state)
+ create(:merge_request, state,
+ source_project: project, target_project: project)
+ end
+
it 'ignores non-GFM links' do
html = %(See <a href="https://google.com/">Google</a>)
doc = filter(html, current_user: user)
@@ -19,7 +31,6 @@ describe Banzai::Filter::IssuableStateFilter, lib: true do
end
it 'ignores non-issuable links' do
- project = create(:empty_project, :public)
link = create_link('text', project: project, reference_type: 'issue')
doc = filter(link, context)
@@ -27,27 +38,24 @@ describe Banzai::Filter::IssuableStateFilter, lib: true do
end
it 'ignores issuable links with empty content' do
- issue = create(:issue, :closed)
- link = create_link('', issue: issue.id, reference_type: 'issue')
+ link = create_link('', issue: closed_issue.id, reference_type: 'issue')
doc = filter(link, context)
expect(doc.css('a').last.text).to eq('')
end
it 'ignores issuable links with custom anchor' do
- issue = create(:issue, :closed)
- link = create_link('something', issue: issue.id, reference_type: 'issue')
+ link = create_link('something', issue: closed_issue.id, reference_type: 'issue')
doc = filter(link, context)
expect(doc.css('a').last.text).to eq('something')
end
it 'ignores issuable links to specific comments' do
- issue = create(:issue, :closed)
- link = create_link("#{issue.to_reference} (comment 1)", issue: issue.id, reference_type: 'issue')
+ link = create_link("#{closed_issue.to_reference} (comment 1)", issue: closed_issue.id, reference_type: 'issue')
doc = filter(link, context)
- expect(doc.css('a').last.text).to eq("#{issue.to_reference} (comment 1)")
+ expect(doc.css('a').last.text).to eq("#{closed_issue.to_reference} (comment 1)")
end
it 'ignores merge request links to diffs tab' do
@@ -63,26 +71,36 @@ describe Banzai::Filter::IssuableStateFilter, lib: true do
end
it 'handles cross project references' do
- issue = create(:issue, :closed)
- project = create(:empty_project)
- link = create_link(issue.to_reference(project), issue: issue.id, reference_type: 'issue')
- doc = filter(link, context.merge(project: project))
+ link = create_link(closed_issue.to_reference(other_project), issue: closed_issue.id, reference_type: 'issue')
+ doc = filter(link, context.merge(project: other_project))
- expect(doc.css('a').last.text).to eq("#{issue.to_reference(project)} (closed)")
+ expect(doc.css('a').last.text).to eq("#{closed_issue.to_reference(other_project)} (closed)")
end
it 'does not append state when filter is not enabled' do
- issue = create(:issue, :closed)
- link = create_link('text', issue: issue.id, reference_type: 'issue')
+ link = create_link('text', issue: closed_issue.id, reference_type: 'issue')
context = { current_user: user }
doc = filter(link, context)
expect(doc.css('a').last.text).to eq('text')
end
+ context 'when project is in pending delete' do
+ before do
+ project.update!(pending_delete: true)
+ end
+
+ it 'does not append issue state' do
+ link = create_link('text', issue: closed_issue.id, reference_type: 'issue')
+ doc = filter(link, context)
+
+ expect(doc.css('a').last.text).to eq('text')
+ end
+ end
+
context 'for issue references' do
it 'ignores open issue references' do
- issue = create(:issue)
+ issue = create_issue(:opened)
link = create_link(issue.to_reference, issue: issue.id, reference_type: 'issue')
doc = filter(link, context)
@@ -90,7 +108,7 @@ describe Banzai::Filter::IssuableStateFilter, lib: true do
end
it 'ignores reopened issue references' do
- issue = create(:issue, :reopened)
+ issue = create_issue(:reopened)
link = create_link(issue.to_reference, issue: issue.id, reference_type: 'issue')
doc = filter(link, context)
@@ -98,70 +116,79 @@ describe Banzai::Filter::IssuableStateFilter, lib: true do
end
it 'appends state to closed issue references' do
- issue = create(:issue, :closed)
- link = create_link(issue.to_reference, issue: issue.id, reference_type: 'issue')
+ link = create_link(closed_issue.to_reference, issue: closed_issue.id, reference_type: 'issue')
doc = filter(link, context)
- expect(doc.css('a').last.text).to eq("#{issue.to_reference} (closed)")
+ expect(doc.css('a').last.text).to eq("#{closed_issue.to_reference} (closed)")
end
end
context 'for merge request references' do
it 'ignores open merge request references' do
- merge_request = create(:merge_request)
+ merge_request = create_merge_request(:opened)
+
link = create_link(
merge_request.to_reference,
merge_request: merge_request.id,
reference_type: 'merge_request'
)
+
doc = filter(link, context)
expect(doc.css('a').last.text).to eq(merge_request.to_reference)
end
it 'ignores reopened merge request references' do
- merge_request = create(:merge_request, :reopened)
+ merge_request = create_merge_request(:reopened)
+
link = create_link(
merge_request.to_reference,
merge_request: merge_request.id,
reference_type: 'merge_request'
)
+
doc = filter(link, context)
expect(doc.css('a').last.text).to eq(merge_request.to_reference)
end
it 'ignores locked merge request references' do
- merge_request = create(:merge_request, :locked)
+ merge_request = create_merge_request(:locked)
+
link = create_link(
merge_request.to_reference,
merge_request: merge_request.id,
reference_type: 'merge_request'
)
+
doc = filter(link, context)
expect(doc.css('a').last.text).to eq(merge_request.to_reference)
end
it 'appends state to closed merge request references' do
- merge_request = create(:merge_request, :closed)
+ merge_request = create_merge_request(:closed)
+
link = create_link(
merge_request.to_reference,
merge_request: merge_request.id,
reference_type: 'merge_request'
)
+
doc = filter(link, context)
expect(doc.css('a').last.text).to eq("#{merge_request.to_reference} (closed)")
end
it 'appends state to merged merge request references' do
- merge_request = create(:merge_request, :merged)
+ merge_request = create_merge_request(:merged)
+
link = create_link(
merge_request.to_reference,
merge_request: merge_request.id,
reference_type: 'merge_request'
)
+
doc = filter(link, context)
expect(doc.css('a').last.text).to eq("#{merge_request.to_reference} (merged)")
diff --git a/spec/lib/banzai/filter/redactor_filter_spec.rb b/spec/lib/banzai/filter/redactor_filter_spec.rb
index 8a6fe1ad6a3..7c4a0f32c7b 100644
--- a/spec/lib/banzai/filter/redactor_filter_spec.rb
+++ b/spec/lib/banzai/filter/redactor_filter_spec.rb
@@ -113,7 +113,7 @@ describe Banzai::Filter::RedactorFilter, lib: true do
it 'allows references for assignee' do
assignee = create(:user)
project = create(:empty_project, :public)
- issue = create(:issue, :confidential, project: project, assignee: assignee)
+ issue = create(:issue, :confidential, project: project, assignees: [assignee])
link = reference_link(project: project.id, issue: issue.id, reference_type: 'issue')
doc = filter(link, current_user: assignee)
diff --git a/spec/lib/banzai/renderer_spec.rb b/spec/lib/banzai/renderer_spec.rb
index e6f8d2a1fed..0e094405e33 100644
--- a/spec/lib/banzai/renderer_spec.rb
+++ b/spec/lib/banzai/renderer_spec.rb
@@ -11,7 +11,7 @@ describe Banzai::Renderer do
end
describe '#render_field' do
- let(:renderer) { Banzai::Renderer }
+ let(:renderer) { described_class }
subject { renderer.render_field(object, :field) }
context 'with a stale cache' do
diff --git a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
index 53abc056602..fe2c00bb2ca 100644
--- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
+++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
@@ -225,7 +225,7 @@ module Ci
before_script: ["pwd"],
rspec: { script: "rspec", type: "test", only: %w(master deploy) },
staging: { script: "deploy", type: "deploy", only: %w(master deploy) },
- production: { script: "deploy", type: "deploy", only: ["master@path", "deploy"] },
+ production: { script: "deploy", type: "deploy", only: ["master@path", "deploy"] }
})
config_processor = GitlabCiYamlProcessor.new(config, 'fork')
@@ -381,7 +381,7 @@ module Ci
before_script: ["pwd"],
rspec: { script: "rspec", type: "test", except: ["master", "deploy", "test@fork"] },
staging: { script: "deploy", type: "deploy", except: ["master"] },
- production: { script: "deploy", type: "deploy", except: ["master@fork"] },
+ production: { script: "deploy", type: "deploy", except: ["master@fork"] }
})
config_processor = GitlabCiYamlProcessor.new(config, 'fork')
@@ -716,7 +716,7 @@ module Ci
expect(config_processor.builds_for_stage_and_ref("test", "master").first[:options][:cache]).to eq(
paths: ["logs/", "binaries/"],
untracked: true,
- key: 'key',
+ key: 'key'
)
end
@@ -734,7 +734,7 @@ module Ci
expect(config_processor.builds_for_stage_and_ref("test", "master").first[:options][:cache]).to eq(
paths: ["logs/", "binaries/"],
untracked: true,
- key: 'key',
+ key: 'key'
)
end
@@ -743,7 +743,7 @@ module Ci
cache: { paths: ["logs/", "binaries/"], untracked: true, key: 'global' },
rspec: {
script: "rspec",
- cache: { paths: ["test/"], untracked: false, key: 'local' },
+ cache: { paths: ["test/"], untracked: false, key: 'local' }
}
})
@@ -753,7 +753,7 @@ module Ci
expect(config_processor.builds_for_stage_and_ref("test", "master").first[:options][:cache]).to eq(
paths: ["test/"],
untracked: false,
- key: 'local',
+ key: 'local'
)
end
end
diff --git a/spec/lib/constraints/group_url_constrainer_spec.rb b/spec/lib/constraints/group_url_constrainer_spec.rb
index 96dacdc5cd2..db680489a8d 100644
--- a/spec/lib/constraints/group_url_constrainer_spec.rb
+++ b/spec/lib/constraints/group_url_constrainer_spec.rb
@@ -17,14 +17,49 @@ describe GroupUrlConstrainer, lib: true do
it { expect(subject.matches?(request)).to be_truthy }
end
+ context 'valid request for nested group with reserved top level name' do
+ let!(:nested_group) { create(:group, path: 'api', parent: group) }
+ let!(:request) { build_request('gitlab/api') }
+
+ it { expect(subject.matches?(request)).to be_truthy }
+ end
+
context 'invalid request' do
let(:request) { build_request('foo') }
it { expect(subject.matches?(request)).to be_falsey }
end
+
+ context 'when the request matches a redirect route' do
+ context 'for a root group' do
+ let!(:redirect_route) { group.redirect_routes.create!(path: 'gitlabb') }
+
+ context 'and is a GET request' do
+ let(:request) { build_request(redirect_route.path) }
+
+ it { expect(subject.matches?(request)).to be_truthy }
+ end
+
+ context 'and is NOT a GET request' do
+ let(:request) { build_request(redirect_route.path, 'POST') }
+
+ it { expect(subject.matches?(request)).to be_falsey }
+ end
+ end
+
+ context 'for a nested group' do
+ let!(:nested_group) { create(:group, path: 'nested', parent: group) }
+ let!(:redirect_route) { nested_group.redirect_routes.create!(path: 'gitlabb/nested') }
+ let(:request) { build_request(redirect_route.path) }
+
+ it { expect(subject.matches?(request)).to be_truthy }
+ end
+ end
end
- def build_request(path)
- double(:request, params: { id: path })
+ def build_request(path, method = 'GET')
+ double(:request,
+ 'get?': (method == 'GET'),
+ params: { id: path })
end
end
diff --git a/spec/lib/constraints/project_url_constrainer_spec.rb b/spec/lib/constraints/project_url_constrainer_spec.rb
index 4f25ad88960..b6884e37aa3 100644
--- a/spec/lib/constraints/project_url_constrainer_spec.rb
+++ b/spec/lib/constraints/project_url_constrainer_spec.rb
@@ -24,9 +24,26 @@ describe ProjectUrlConstrainer, lib: true do
it { expect(subject.matches?(request)).to be_falsey }
end
end
+
+ context 'when the request matches a redirect route' do
+ let(:old_project_path) { 'old_project_path' }
+ let!(:redirect_route) { project.redirect_routes.create!(path: "#{namespace.full_path}/#{old_project_path}") }
+
+ context 'and is a GET request' do
+ let(:request) { build_request(namespace.full_path, old_project_path) }
+ it { expect(subject.matches?(request)).to be_truthy }
+ end
+
+ context 'and is NOT a GET request' do
+ let(:request) { build_request(namespace.full_path, old_project_path, 'POST') }
+ it { expect(subject.matches?(request)).to be_falsey }
+ end
+ end
end
- def build_request(namespace, project)
- double(:request, params: { namespace_id: namespace, id: project })
+ def build_request(namespace, project, method = 'GET')
+ double(:request,
+ 'get?': (method == 'GET'),
+ params: { namespace_id: namespace, id: project })
end
end
diff --git a/spec/lib/constraints/user_url_constrainer_spec.rb b/spec/lib/constraints/user_url_constrainer_spec.rb
index 207b6fe6c9e..ed69b830979 100644
--- a/spec/lib/constraints/user_url_constrainer_spec.rb
+++ b/spec/lib/constraints/user_url_constrainer_spec.rb
@@ -15,9 +15,26 @@ describe UserUrlConstrainer, lib: true do
it { expect(subject.matches?(request)).to be_falsey }
end
+
+ context 'when the request matches a redirect route' do
+ let(:old_project_path) { 'old_project_path' }
+ let!(:redirect_route) { user.namespace.redirect_routes.create!(path: 'foo') }
+
+ context 'and is a GET request' do
+ let(:request) { build_request(redirect_route.path) }
+ it { expect(subject.matches?(request)).to be_truthy }
+ end
+
+ context 'and is NOT a GET request' do
+ let(:request) { build_request(redirect_route.path, 'POST') }
+ it { expect(subject.matches?(request)).to be_falsey }
+ end
+ end
end
- def build_request(username)
- double(:request, params: { username: username })
+ def build_request(username, method = 'GET')
+ double(:request,
+ 'get?': (method == 'GET'),
+ params: { username: username })
end
end
diff --git a/spec/lib/expand_variables_spec.rb b/spec/lib/expand_variables_spec.rb
index 90628917943..7faa0f31b68 100644
--- a/spec/lib/expand_variables_spec.rb
+++ b/spec/lib/expand_variables_spec.rb
@@ -25,7 +25,7 @@ describe ExpandVariables do
result: 'keyvalueresult',
variables: [
{ key: 'variable', value: 'value' },
- { key: 'variable2', value: 'result' },
+ { key: 'variable2', value: 'result' }
] },
{ value: 'key${variable}${variable2}',
result: 'keyvalueresult',
@@ -37,7 +37,7 @@ describe ExpandVariables do
result: 'keyresultvalue',
variables: [
{ key: 'variable', value: 'value' },
- { key: 'variable2', value: 'result' },
+ { key: 'variable2', value: 'result' }
] },
{ value: 'key${variable2}${variable}',
result: 'keyresultvalue',
@@ -49,7 +49,7 @@ describe ExpandVariables do
result: 'review/feature/add-review-apps',
variables: [
{ key: 'CI_COMMIT_REF_NAME', value: 'feature/add-review-apps' }
- ] },
+ ] }
]
tests.each do |test|
diff --git a/spec/lib/gitlab/asciidoc_spec.rb b/spec/lib/gitlab/asciidoc_spec.rb
index bca57105d1d..2c7ebb15fd7 100644
--- a/spec/lib/gitlab/asciidoc_spec.rb
+++ b/spec/lib/gitlab/asciidoc_spec.rb
@@ -22,26 +22,24 @@ module Gitlab
expect(Asciidoctor).to receive(:convert)
.with(input, expected_asciidoc_opts).and_return(html)
- expect( render(input, context) ).to eql html
+ expect(render(input, context)).to eq(html)
end
context "with asciidoc_opts" do
- let(:asciidoc_opts) { { safe: :safe, attributes: ['foo'] } }
-
it "merges the options with default ones" do
expected_asciidoc_opts = {
- safe: :safe,
+ safe: :secure,
backend: :gitlab_html5,
- attributes: described_class::DEFAULT_ADOC_ATTRS + ['foo']
+ attributes: described_class::DEFAULT_ADOC_ATTRS
}
expect(Asciidoctor).to receive(:convert)
.with(input, expected_asciidoc_opts).and_return(html)
- render(input, context, asciidoc_opts)
+ render(input, context)
end
end
-
+
context "XSS" do
links = {
'links' => {
@@ -50,7 +48,7 @@ module Gitlab
},
'images' => {
input: 'image:https://localhost.com/image.png[Alt text" onerror="alert(7)]',
- output: "<div>\n<p><span><img src=\"https://localhost.com/image.png\" alt=\"Alt text\"></span></p>\n</div>"
+ output: "<img src=\"https://localhost.com/image.png\" alt=\"Alt text\">"
},
'pre' => {
input: '```mypre"><script>alert(3)</script>',
@@ -60,10 +58,18 @@ module Gitlab
links.each do |name, data|
it "does not convert dangerous #{name} into HTML" do
- expect(render(data[:input], context)).to eql data[:output]
+ expect(render(data[:input], context)).to include(data[:output])
end
end
end
+
+ context 'external links' do
+ it 'adds the `rel` attribute to the link' do
+ output = render('link:https://google.com[Google]', context)
+
+ expect(output).to include('rel="nofollow noreferrer noopener"')
+ end
+ end
end
def render(*args)
diff --git a/spec/lib/gitlab/auth_spec.rb b/spec/lib/gitlab/auth_spec.rb
index 03c4879ed6f..50bc3ef1b7c 100644
--- a/spec/lib/gitlab/auth_spec.rb
+++ b/spec/lib/gitlab/auth_spec.rb
@@ -118,7 +118,7 @@ describe Gitlab::Auth, lib: true do
it 'succeeds for OAuth tokens with the `api` scope' do
expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: 'oauth2')
- expect(gl_auth.find_for_git_client("oauth2", token_w_api_scope.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(user, nil, :oauth, read_authentication_abilities))
+ expect(gl_auth.find_for_git_client("oauth2", token_w_api_scope.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(user, nil, :oauth, full_authentication_abilities))
end
it 'fails for OAuth tokens with other scopes' do
@@ -175,7 +175,7 @@ describe Gitlab::Auth, lib: true do
user = create(
:user,
username: 'normal_user',
- password: 'my-secret',
+ password: 'my-secret'
)
expect(gl_auth.find_for_git_client(user.username, user.password, project: nil, ip: 'ip'))
@@ -186,7 +186,7 @@ describe Gitlab::Auth, lib: true do
user = create(
:user,
username: 'oauth2',
- password: 'my-secret',
+ password: 'my-secret'
)
expect(gl_auth.find_for_git_client(user.username, user.password, project: nil, ip: 'ip'))
diff --git a/spec/lib/gitlab/backup/manager_spec.rb b/spec/lib/gitlab/backup/manager_spec.rb
index f84782ab440..c59ff7fb290 100644
--- a/spec/lib/gitlab/backup/manager_spec.rb
+++ b/spec/lib/gitlab/backup/manager_spec.rb
@@ -151,7 +151,7 @@ describe Backup::Manager, lib: true do
allow(Dir).to receive(:glob).and_return(
[
'1451606400_2016_01_01_gitlab_backup.tar',
- '1451520000_2015_12_31_gitlab_backup.tar',
+ '1451520000_2015_12_31_gitlab_backup.tar'
]
)
end
diff --git a/spec/lib/gitlab/cache/ci/project_pipeline_status_spec.rb b/spec/lib/gitlab/cache/ci/project_pipeline_status_spec.rb
index fced253dd01..b386852b196 100644
--- a/spec/lib/gitlab/cache/ci/project_pipeline_status_spec.rb
+++ b/spec/lib/gitlab/cache/ci/project_pipeline_status_spec.rb
@@ -1,8 +1,9 @@
require 'spec_helper'
-describe Gitlab::Cache::Ci::ProjectPipelineStatus do
+describe Gitlab::Cache::Ci::ProjectPipelineStatus, :redis do
let(:project) { create(:project) }
let(:pipeline_status) { described_class.new(project) }
+ let(:cache_key) { "projects/#{project.id}/pipeline_status" }
describe '.load_for_project' do
it "loads the status" do
@@ -12,12 +13,110 @@ describe Gitlab::Cache::Ci::ProjectPipelineStatus do
end
end
+ describe 'loading in batches' do
+ let(:status) { 'success' }
+ let(:sha) { '424d1b73bc0d3cb726eb7dc4ce17a4d48552f8c6' }
+ let(:ref) { 'master' }
+ let(:pipeline_info) { { sha: sha, status: status, ref: ref } }
+ let(:project_without_status) { create(:project) }
+
+ describe '.load_in_batch_for_projects' do
+ it 'preloads pipeline_status on projects' do
+ described_class.load_in_batch_for_projects([project])
+
+ # Don't call the accessor that would lazy load the variable
+ expect(project.instance_variable_get('@pipeline_status')).to be_a(described_class)
+ end
+
+ describe 'without a status in redis' do
+ it 'loads the status from a commit when it was not in redis' do
+ empty_status = { sha: nil, status: nil, ref: nil }
+ fake_pipeline = described_class.new(
+ project_without_status,
+ pipeline_info: empty_status,
+ loaded_from_cache: false
+ )
+
+ expect(described_class).to receive(:new).
+ with(project_without_status,
+ pipeline_info: empty_status,
+ loaded_from_cache: false).
+ and_return(fake_pipeline)
+ expect(fake_pipeline).to receive(:load_from_project)
+ expect(fake_pipeline).to receive(:store_in_cache)
+
+ described_class.load_in_batch_for_projects([project_without_status])
+ end
+
+ it 'only connects to redis twice' do
+ # Once to load, once to store in the cache
+ expect(Gitlab::Redis).to receive(:with).exactly(2).and_call_original
+
+ described_class.load_in_batch_for_projects([project_without_status])
+
+ expect(project_without_status.pipeline_status).not_to be_nil
+ end
+ end
+
+ describe 'when a status was cached in redis' do
+ before do
+ Gitlab::Redis.with do |redis|
+ redis.mapped_hmset(cache_key,
+ { sha: sha, status: status, ref: ref })
+ end
+ end
+
+ it 'loads the correct status' do
+ described_class.load_in_batch_for_projects([project])
+
+ pipeline_status = project.instance_variable_get('@pipeline_status')
+
+ expect(pipeline_status.sha).to eq(sha)
+ expect(pipeline_status.status).to eq(status)
+ expect(pipeline_status.ref).to eq(ref)
+ end
+
+ it 'only connects to redis once' do
+ expect(Gitlab::Redis).to receive(:with).exactly(1).and_call_original
+
+ described_class.load_in_batch_for_projects([project])
+
+ expect(project.pipeline_status).not_to be_nil
+ end
+
+ it "doesn't load the status separatly" do
+ expect_any_instance_of(described_class).not_to receive(:load_from_project)
+ expect_any_instance_of(described_class).not_to receive(:load_from_cache)
+
+ described_class.load_in_batch_for_projects([project])
+ end
+ end
+ end
+
+ describe '.cached_results_for_projects' do
+ it 'loads a status from redis for all projects' do
+ Gitlab::Redis.with do |redis|
+ redis.mapped_hmset(cache_key, { sha: sha, status: status, ref: ref })
+ end
+
+ result = [{ loaded_from_cache: false, pipeline_info: { sha: nil, status: nil, ref: nil } },
+ { loaded_from_cache: true, pipeline_info: pipeline_info }]
+
+ expect(described_class.cached_results_for_projects([project_without_status, project])).to eq(result)
+ end
+ end
+ end
+
describe '.update_for_pipeline' do
it 'refreshes the cache if nescessary' do
- pipeline = build_stubbed(:ci_pipeline, sha: '123456', status: 'success')
+ pipeline = build_stubbed(:ci_pipeline,
+ sha: '123456', status: 'success', ref: 'master')
fake_status = double
expect(described_class).to receive(:new).
- with(pipeline.project, sha: '123456', status: 'success', ref: 'master').
+ with(pipeline.project,
+ pipeline_info: {
+ sha: '123456', status: 'success', ref: 'master'
+ }).
and_return(fake_status)
expect(fake_status).to receive(:store_in_cache_if_needed)
@@ -110,7 +209,7 @@ describe Gitlab::Cache::Ci::ProjectPipelineStatus do
pipeline_status.status = 'failed'
pipeline_status.store_in_cache
- read_sha, read_status = Gitlab::Redis.with { |redis| redis.hmget("projects/#{project.id}/build_status", :sha, :status) }
+ read_sha, read_status = Gitlab::Redis.with { |redis| redis.hmget(cache_key, :sha, :status) }
expect(read_sha).to eq('123456')
expect(read_status).to eq('failed')
@@ -120,10 +219,10 @@ describe Gitlab::Cache::Ci::ProjectPipelineStatus do
describe '#store_in_cache_if_needed', :redis do
it 'stores the state in the cache when the sha is the HEAD of the project' do
create(:ci_pipeline, :success, project: project, sha: project.commit.sha)
- build_status = described_class.load_for_project(project)
+ pipeline_status = described_class.load_for_project(project)
- build_status.store_in_cache_if_needed
- sha, status, ref = Gitlab::Redis.with { |redis| redis.hmget("projects/#{project.id}/build_status", :sha, :status, :ref) }
+ pipeline_status.store_in_cache_if_needed
+ sha, status, ref = Gitlab::Redis.with { |redis| redis.hmget(cache_key, :sha, :status, :ref) }
expect(sha).not_to be_nil
expect(status).not_to be_nil
@@ -131,10 +230,13 @@ describe Gitlab::Cache::Ci::ProjectPipelineStatus do
end
it "doesn't store the status in redis when the sha is not the head of the project" do
- other_status = described_class.new(project, sha: "123456", status: "failed")
+ other_status = described_class.new(
+ project,
+ pipeline_info: { sha: "123456", status: "failed" }
+ )
other_status.store_in_cache_if_needed
- sha, status = Gitlab::Redis.with { |redis| redis.hmget("projects/#{project.id}/build_status", :sha, :status) }
+ sha, status = Gitlab::Redis.with { |redis| redis.hmget(cache_key, :sha, :status) }
expect(sha).to be_nil
expect(status).to be_nil
@@ -142,11 +244,18 @@ describe Gitlab::Cache::Ci::ProjectPipelineStatus do
it "deletes the cache if the repository doesn't have a head commit" do
empty_project = create(:empty_project)
- Gitlab::Redis.with { |redis| redis.mapped_hmset("projects/#{empty_project.id}/build_status", { sha: "sha", status: "pending", ref: 'master' }) }
- other_status = described_class.new(empty_project, sha: "123456", status: "failed")
+ Gitlab::Redis.with do |redis|
+ redis.mapped_hmset(cache_key,
+ { sha: 'sha', status: 'pending', ref: 'master' })
+ end
+
+ other_status = described_class.new(empty_project,
+ pipeline_info: {
+ sha: "123456", status: "failed"
+ })
other_status.store_in_cache_if_needed
- sha, status, ref = Gitlab::Redis.with { |redis| redis.hmget("projects/#{empty_project.id}/build_status", :sha, :status, :ref) }
+ sha, status, ref = Gitlab::Redis.with { |redis| redis.hmget("projects/#{empty_project.id}/pipeline_status", :sha, :status, :ref) }
expect(sha).to be_nil
expect(status).to be_nil
@@ -157,9 +266,13 @@ describe Gitlab::Cache::Ci::ProjectPipelineStatus do
describe "with a status in redis", :redis do
let(:status) { 'success' }
let(:sha) { '424d1b73bc0d3cb726eb7dc4ce17a4d48552f8c6' }
+ let(:ref) { 'master' }
before do
- Gitlab::Redis.with { |redis| redis.mapped_hmset("projects/#{project.id}/build_status", { sha: sha, status: status }) }
+ Gitlab::Redis.with do |redis|
+ redis.mapped_hmset(cache_key,
+ { sha: sha, status: status, ref: ref })
+ end
end
describe '#load_from_cache' do
@@ -168,6 +281,7 @@ describe Gitlab::Cache::Ci::ProjectPipelineStatus do
expect(pipeline_status.sha).to eq(sha)
expect(pipeline_status.status).to eq(status)
+ expect(pipeline_status.ref).to eq(ref)
end
end
@@ -181,7 +295,7 @@ describe Gitlab::Cache::Ci::ProjectPipelineStatus do
it 'deletes values from redis' do
pipeline_status.delete_from_cache
- key_exists = Gitlab::Redis.with { |redis| redis.exists("projects/#{project.id}/build_status") }
+ key_exists = Gitlab::Redis.with { |redis| redis.exists(cache_key) }
expect(key_exists).to be_falsy
end
diff --git a/spec/lib/gitlab/changes_list_spec.rb b/spec/lib/gitlab/changes_list_spec.rb
index 69d86144e32..464508fcd73 100644
--- a/spec/lib/gitlab/changes_list_spec.rb
+++ b/spec/lib/gitlab/changes_list_spec.rb
@@ -5,7 +5,7 @@ describe Gitlab::ChangesList do
let(:invalid_changes) { 1 }
context 'when changes is a valid string' do
- let(:changes_list) { Gitlab::ChangesList.new(valid_changes_string) }
+ let(:changes_list) { described_class.new(valid_changes_string) }
it 'splits elements by newline character' do
expect(changes_list).to contain_exactly({
diff --git a/spec/lib/gitlab/chat_commands/command_spec.rb b/spec/lib/gitlab/chat_commands/command_spec.rb
index b6e924d67be..eb4f06b371c 100644
--- a/spec/lib/gitlab/chat_commands/command_spec.rb
+++ b/spec/lib/gitlab/chat_commands/command_spec.rb
@@ -40,11 +40,15 @@ describe Gitlab::ChatCommands::Command, service: true do
context 'when trying to do deployment' do
let(:params) { { text: 'deploy staging to production' } }
- let!(:build) { create(:ci_build, project: project) }
+ let!(:build) { create(:ci_build, pipeline: pipeline) }
+ let!(:pipeline) { create(:ci_pipeline, project: project) }
let!(:staging) { create(:environment, name: 'staging', project: project) }
let!(:deployment) { create(:deployment, environment: staging, deployable: build) }
+
let!(:manual) do
- create(:ci_build, :manual, project: project, pipeline: build.pipeline, name: 'first', environment: 'production')
+ create(:ci_build, :manual, pipeline: pipeline,
+ name: 'first',
+ environment: 'production')
end
context 'and user can not create deployment' do
@@ -56,7 +60,7 @@ describe Gitlab::ChatCommands::Command, service: true do
context 'and user does have deployment permission' do
before do
- project.team << [user, :developer]
+ build.project.add_master(user)
end
it 'returns action' do
@@ -66,7 +70,9 @@ describe Gitlab::ChatCommands::Command, service: true do
context 'when duplicate action exists' do
let!(:manual2) do
- create(:ci_build, :manual, project: project, pipeline: build.pipeline, name: 'second', environment: 'production')
+ create(:ci_build, :manual, pipeline: pipeline,
+ name: 'second',
+ environment: 'production')
end
it 'returns error' do
diff --git a/spec/lib/gitlab/chat_commands/deploy_spec.rb b/spec/lib/gitlab/chat_commands/deploy_spec.rb
index b3358a32161..b33389d959e 100644
--- a/spec/lib/gitlab/chat_commands/deploy_spec.rb
+++ b/spec/lib/gitlab/chat_commands/deploy_spec.rb
@@ -7,7 +7,7 @@ describe Gitlab::ChatCommands::Deploy, service: true do
let(:regex_match) { described_class.match('deploy staging to production') }
before do
- project.team << [user, :master]
+ project.add_master(user)
end
subject do
@@ -23,7 +23,8 @@ describe Gitlab::ChatCommands::Deploy, service: true do
context 'with environment' do
let!(:staging) { create(:environment, name: 'staging', project: project) }
- let!(:build) { create(:ci_build, project: project) }
+ let!(:pipeline) { create(:ci_pipeline, project: project) }
+ let!(:build) { create(:ci_build, pipeline: pipeline) }
let!(:deployment) { create(:deployment, environment: staging, deployable: build) }
context 'without actions' do
@@ -35,7 +36,9 @@ describe Gitlab::ChatCommands::Deploy, service: true do
context 'with action' do
let!(:manual1) do
- create(:ci_build, :manual, project: project, pipeline: build.pipeline, name: 'first', environment: 'production')
+ create(:ci_build, :manual, pipeline: pipeline,
+ name: 'first',
+ environment: 'production')
end
it 'returns success result' do
@@ -45,7 +48,9 @@ describe Gitlab::ChatCommands::Deploy, service: true do
context 'when duplicate action exists' do
let!(:manual2) do
- create(:ci_build, :manual, project: project, pipeline: build.pipeline, name: 'second', environment: 'production')
+ create(:ci_build, :manual, pipeline: pipeline,
+ name: 'second',
+ environment: 'production')
end
it 'returns error' do
@@ -57,8 +62,7 @@ describe Gitlab::ChatCommands::Deploy, service: true do
context 'when teardown action exists' do
let!(:teardown) do
create(:ci_build, :manual, :teardown_environment,
- project: project, pipeline: build.pipeline,
- name: 'teardown', environment: 'production')
+ pipeline: pipeline, name: 'teardown', environment: 'production')
end
it 'returns the success message' do
diff --git a/spec/lib/gitlab/checks/change_access_spec.rb b/spec/lib/gitlab/checks/change_access_spec.rb
index 959ae02c222..8d81ed5856e 100644
--- a/spec/lib/gitlab/checks/change_access_spec.rb
+++ b/spec/lib/gitlab/checks/change_access_spec.rb
@@ -96,40 +96,77 @@ describe Gitlab::Checks::ChangeAccess, lib: true do
end
end
- context 'protected branches check' do
- before do
- allow(ProtectedBranch).to receive(:protected?).with(project, '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)
+ context 'branches check' do
+ context 'trying to delete the default branch' do
+ let(:newrev) { '0000000000000000000000000000000000000000' }
+ let(:ref) { 'refs/heads/master' }
- 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.')
+ it 'returns an error' do
+ expect(subject.status).to be(false)
+ expect(subject.message).to eq('The default branch of a project cannot be deleted.')
+ end
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)
+ context 'protected branches check' do
+ before do
+ allow(ProtectedBranch).to receive(:protected?).with(project, 'master').and_return(true)
+ allow(ProtectedBranch).to receive(:protected?).with(project, 'feature').and_return(true)
+ end
- 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 do forced pushes to protected branches' do
+ expect(Gitlab::Checks::ForcePush).to receive(:force_push?).and_return(true)
- 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 force push code to a protected branch on this project.')
+ end
- 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
+ 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)
- context 'branch deletion' do
- let(:newrev) { '0000000000000000000000000000000000000000' }
+ 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)
- it 'returns an error if the user is not allowed to delete protected branches' do
expect(subject.status).to be(false)
- expect(subject.message).to eq('You are not allowed to delete protected branches from this project.')
+ expect(subject.message).to eq('You are not allowed to push code to protected branches on this project.')
+ end
+
+ context 'branch deletion' do
+ let(:newrev) { '0000000000000000000000000000000000000000' }
+ let(:ref) { 'refs/heads/feature' }
+
+ context 'if the user is not allowed to delete protected branches' do
+ it 'returns an error' do
+ expect(subject.status).to be(false)
+ expect(subject.message).to eq('You are not allowed to delete protected branches from this project. Only a project master or owner can delete a protected branch.')
+ end
+ end
+
+ context 'if the user is allowed to delete protected branches' do
+ before do
+ project.add_master(user)
+ end
+
+ context 'through the web interface' do
+ let(:protocol) { 'web' }
+
+ it 'allows branch deletion' do
+ expect(subject.status).to be(true)
+ end
+ end
+
+ context 'over SSH or HTTP' do
+ it 'returns an error' do
+ expect(subject.status).to be(false)
+ expect(subject.message).to eq('You can only delete protected branches using the web interface.')
+ end
+ end
+ end
end
end
end
diff --git a/spec/lib/gitlab/checks/force_push_spec.rb b/spec/lib/gitlab/checks/force_push_spec.rb
index 7a84bbebd02..bc66ce83d4a 100644
--- a/spec/lib/gitlab/checks/force_push_spec.rb
+++ b/spec/lib/gitlab/checks/force_push_spec.rb
@@ -1,19 +1,19 @@
require 'spec_helper'
-describe Gitlab::Checks::ChangeAccess, lib: true do
+describe Gitlab::Checks::ForcePush, lib: true do
let(:project) { create(:project, :repository) }
context "exit code checking" do
it "does not raise a runtime error if the `popen` call to git returns a zero exit code" do
allow(Gitlab::Popen).to receive(:popen).and_return(['normal output', 0])
- expect { Gitlab::Checks::ForcePush.force_push?(project, 'oldrev', 'newrev') }.not_to raise_error
+ expect { described_class.force_push?(project, 'oldrev', 'newrev') }.not_to raise_error
end
it "raises a runtime error if the `popen` call to git returns a non-zero exit code" do
allow(Gitlab::Popen).to receive(:popen).and_return(['error', 1])
- expect { Gitlab::Checks::ForcePush.force_push?(project, 'oldrev', 'newrev') }.to raise_error(RuntimeError)
+ expect { described_class.force_push?(project, 'oldrev', 'newrev') }.to raise_error(RuntimeError)
end
end
end
diff --git a/spec/lib/gitlab/ci/build/artifacts/metadata/entry_spec.rb b/spec/lib/gitlab/ci/build/artifacts/metadata/entry_spec.rb
index abc93e1b44a..3b905611467 100644
--- a/spec/lib/gitlab/ci/build/artifacts/metadata/entry_spec.rb
+++ b/spec/lib/gitlab/ci/build/artifacts/metadata/entry_spec.rb
@@ -135,6 +135,17 @@ describe Gitlab::Ci::Build::Artifacts::Metadata::Entry do
subject { |example| path(example).nodes }
it { is_expected.to eq 4 }
end
+
+ describe '#blob' do
+ let(:file_entry) { |example| path(example) }
+ subject { file_entry.blob }
+
+ it 'returns a blob representing the entry data' do
+ expect(subject).to be_a(Blob)
+ expect(subject.path).to eq(file_entry.path)
+ expect(subject.size).to eq(file_entry.metadata[:size])
+ end
+ end
end
describe 'non-existent/', path: 'non-existent/' do
diff --git a/spec/lib/gitlab/ci/build/credentials/factory_spec.rb b/spec/lib/gitlab/ci/build/credentials/factory_spec.rb
index 10b4b7a8826..d53db05e5e6 100644
--- a/spec/lib/gitlab/ci/build/credentials/factory_spec.rb
+++ b/spec/lib/gitlab/ci/build/credentials/factory_spec.rb
@@ -3,14 +3,14 @@ require 'spec_helper'
describe Gitlab::Ci::Build::Credentials::Factory do
let(:build) { create(:ci_build, name: 'spinach', stage: 'test', stage_idx: 0) }
- subject { Gitlab::Ci::Build::Credentials::Factory.new(build).create! }
+ subject { described_class.new(build).create! }
class TestProvider
def initialize(build); end
end
before do
- allow_any_instance_of(Gitlab::Ci::Build::Credentials::Factory).to receive(:providers).and_return([TestProvider])
+ allow_any_instance_of(described_class).to receive(:providers).and_return([TestProvider])
end
context 'when provider is valid' do
diff --git a/spec/lib/gitlab/ci/build/credentials/registry_spec.rb b/spec/lib/gitlab/ci/build/credentials/registry_spec.rb
index 84e44dd53e2..c6054138cde 100644
--- a/spec/lib/gitlab/ci/build/credentials/registry_spec.rb
+++ b/spec/lib/gitlab/ci/build/credentials/registry_spec.rb
@@ -4,14 +4,14 @@ describe Gitlab::Ci::Build::Credentials::Registry do
let(:build) { create(:ci_build, name: 'spinach', stage: 'test', stage_idx: 0) }
let(:registry_url) { 'registry.example.com:5005' }
- subject { Gitlab::Ci::Build::Credentials::Registry.new(build) }
+ subject { described_class.new(build) }
before do
stub_container_registry_config(host_port: registry_url)
end
it 'contains valid DockerRegistry credentials' do
- expect(subject).to be_kind_of(Gitlab::Ci::Build::Credentials::Registry)
+ expect(subject).to be_kind_of(described_class)
expect(subject.username).to eq 'gitlab-ci-token'
expect(subject.password).to eq build.token
@@ -20,7 +20,7 @@ describe Gitlab::Ci::Build::Credentials::Registry do
end
describe '.valid?' do
- subject { Gitlab::Ci::Build::Credentials::Registry.new(build).valid? }
+ subject { described_class.new(build).valid? }
context 'when registry is enabled' do
before do
diff --git a/spec/lib/gitlab/ci/config/entry/global_spec.rb b/spec/lib/gitlab/ci/config/entry/global_spec.rb
index 7678d6ebe30..23270ad5053 100644
--- a/spec/lib/gitlab/ci/config/entry/global_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/global_spec.rb
@@ -167,7 +167,7 @@ describe Gitlab::Ci::Config::Entry::Global do
cache: { key: 'k', untracked: true, paths: ['public/'] },
variables: {},
ignore: false,
- after_script: ['make clean'] },
+ after_script: ['make clean'] }
)
end
end
diff --git a/spec/lib/gitlab/ci/cron_parser_spec.rb b/spec/lib/gitlab/ci/cron_parser_spec.rb
index 0864bc7258d..809fda11879 100644
--- a/spec/lib/gitlab/ci/cron_parser_spec.rb
+++ b/spec/lib/gitlab/ci/cron_parser_spec.rb
@@ -60,14 +60,60 @@ describe Gitlab::Ci::CronParser do
end
end
- context 'when cron_timezone is US/Pacific' do
- let(:cron) { '0 0 * * *' }
- let(:cron_timezone) { 'US/Pacific' }
+ context 'when cron_timezone is TZInfo format' do
+ before do
+ allow(Time).to receive(:zone)
+ .and_return(ActiveSupport::TimeZone['UTC'])
+ end
- it_behaves_like "returns time in the future"
+ let(:hour_in_utc) do
+ ActiveSupport::TimeZone[cron_timezone]
+ .now.change(hour: 0).in_time_zone('UTC').hour
+ end
+
+ context 'when cron_timezone is US/Pacific' do
+ let(:cron) { '* 0 * * *' }
+ let(:cron_timezone) { 'US/Pacific' }
+
+ it_behaves_like "returns time in the future"
+
+ it 'converts time in server time zone' do
+ expect(subject.hour).to eq(hour_in_utc)
+ end
+ end
+ end
+
+ context 'when cron_timezone is ActiveSupport::TimeZone format' do
+ before do
+ allow(Time).to receive(:zone)
+ .and_return(ActiveSupport::TimeZone['UTC'])
+ end
+
+ let(:hour_in_utc) do
+ ActiveSupport::TimeZone[cron_timezone]
+ .now.change(hour: 0).in_time_zone('UTC').hour
+ end
+
+ context 'when cron_timezone is Berlin' do
+ let(:cron) { '* 0 * * *' }
+ let(:cron_timezone) { 'Berlin' }
+
+ it_behaves_like "returns time in the future"
+
+ it 'converts time in server time zone' do
+ expect(subject.hour).to eq(hour_in_utc)
+ end
+ end
- it 'converts time in server time zone' do
- expect(subject.hour).to eq((Time.zone.now.in_time_zone(cron_timezone).utc_offset / 60 / 60).abs)
+ context 'when cron_timezone is Eastern Time (US & Canada)' do
+ let(:cron) { '* 0 * * *' }
+ let(:cron_timezone) { 'Eastern Time (US & Canada)' }
+
+ it_behaves_like "returns time in the future"
+
+ it 'converts time in server time zone' do
+ expect(subject.hour).to eq(hour_in_utc)
+ end
end
end
end
@@ -76,9 +122,21 @@ describe Gitlab::Ci::CronParser do
let(:cron) { 'invalid_cron' }
let(:cron_timezone) { 'invalid_cron_timezone' }
- it 'returns nil' do
- is_expected.to be_nil
- end
+ it { is_expected.to be_nil }
+ end
+
+ context 'when cron syntax is quoted' do
+ let(:cron) { "'0 * * * *'" }
+ let(:cron_timezone) { 'UTC' }
+
+ it { expect(subject).to be_nil }
+ end
+
+ context 'when cron syntax is rufus-scheduler syntax' do
+ let(:cron) { 'every 3h' }
+ let(:cron_timezone) { 'UTC' }
+
+ it { expect(subject).to be_nil }
end
end
@@ -96,6 +154,12 @@ describe Gitlab::Ci::CronParser do
it { is_expected.to eq(false) }
end
+
+ context 'when cron syntax is quoted' do
+ let(:cron) { "'0 * * * *'" }
+
+ it { is_expected.to eq(false) }
+ end
end
describe '#cron_timezone_valid?' do
@@ -112,5 +176,11 @@ describe Gitlab::Ci::CronParser do
it { is_expected.to eq(false) }
end
+
+ context 'when cron_timezone is ActiveSupport::TimeZone format' do
+ let(:cron_timezone) { 'Eastern Time (US & Canada)' }
+
+ it { is_expected.to eq(true) }
+ end
end
end
diff --git a/spec/lib/gitlab/ci/status/build/action_spec.rb b/spec/lib/gitlab/ci/status/build/action_spec.rb
new file mode 100644
index 00000000000..8c25f72804b
--- /dev/null
+++ b/spec/lib/gitlab/ci/status/build/action_spec.rb
@@ -0,0 +1,56 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Status::Build::Action do
+ let(:status) { double('core status') }
+ let(:user) { double('user') }
+
+ subject do
+ described_class.new(status)
+ end
+
+ describe '#label' do
+ before do
+ allow(status).to receive(:label).and_return('label')
+ end
+
+ context 'when status has action' do
+ before do
+ allow(status).to receive(:has_action?).and_return(true)
+ end
+
+ it 'does not append text' do
+ expect(subject.label).to eq 'label'
+ end
+ end
+
+ context 'when status does not have action' do
+ before do
+ allow(status).to receive(:has_action?).and_return(false)
+ end
+
+ it 'appends text about action not allowed' do
+ expect(subject.label).to eq 'label (not allowed)'
+ end
+ end
+ end
+
+ describe '.matches?' do
+ subject { described_class.matches?(build, user) }
+
+ context 'when build is an action' do
+ let(:build) { create(:ci_build, :manual) }
+
+ it 'is a correct match' do
+ expect(subject).to be true
+ end
+ end
+
+ context 'when build is not manual' do
+ let(:build) { create(:ci_build) }
+
+ it 'does not match' do
+ expect(subject).to be false
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/status/build/factory_spec.rb b/spec/lib/gitlab/ci/status/build/factory_spec.rb
index e648a3ac3a2..185bb9098da 100644
--- a/spec/lib/gitlab/ci/status/build/factory_spec.rb
+++ b/spec/lib/gitlab/ci/status/build/factory_spec.rb
@@ -204,11 +204,12 @@ describe Gitlab::Ci::Status::Build::Factory do
it 'matches correct extended statuses' do
expect(factory.extended_statuses)
- .to eq [Gitlab::Ci::Status::Build::Play]
+ .to eq [Gitlab::Ci::Status::Build::Play,
+ Gitlab::Ci::Status::Build::Action]
end
- it 'fabricates a play detailed status' do
- expect(status).to be_a Gitlab::Ci::Status::Build::Play
+ it 'fabricates action detailed status' do
+ expect(status).to be_a Gitlab::Ci::Status::Build::Action
end
it 'fabricates status with correct details' do
@@ -216,11 +217,26 @@ describe Gitlab::Ci::Status::Build::Factory do
expect(status.group).to eq 'manual'
expect(status.icon).to eq 'icon_status_manual'
expect(status.favicon).to eq 'favicon_status_manual'
- expect(status.label).to eq 'manual play action'
+ expect(status.label).to include 'manual play action'
expect(status).to have_details
- expect(status).to have_action
expect(status.action_path).to include 'play'
end
+
+ context 'when user has ability to play action' do
+ before do
+ build.project.add_master(user)
+ end
+
+ it 'fabricates status that has action' do
+ expect(status).to have_action
+ end
+ end
+
+ context 'when user does not have ability to play action' do
+ it 'fabricates status that has no action' do
+ expect(status).not_to have_action
+ end
+ end
end
context 'when build is an environment stop action' do
@@ -232,21 +248,24 @@ describe Gitlab::Ci::Status::Build::Factory do
it 'matches correct extended statuses' do
expect(factory.extended_statuses)
- .to eq [Gitlab::Ci::Status::Build::Stop]
+ .to eq [Gitlab::Ci::Status::Build::Stop,
+ Gitlab::Ci::Status::Build::Action]
end
- it 'fabricates a stop detailed status' do
- expect(status).to be_a Gitlab::Ci::Status::Build::Stop
+ it 'fabricates action detailed status' do
+ expect(status).to be_a Gitlab::Ci::Status::Build::Action
end
- it 'fabricates status with correct details' do
- expect(status.text).to eq 'manual'
- expect(status.group).to eq 'manual'
- expect(status.icon).to eq 'icon_status_manual'
- expect(status.favicon).to eq 'favicon_status_manual'
- expect(status.label).to eq 'manual stop action'
- expect(status).to have_details
- expect(status).to have_action
+ context 'when user is not allowed to execute manual action' do
+ it 'fabricates status with correct details' do
+ expect(status.text).to eq 'manual'
+ expect(status.group).to eq 'manual'
+ expect(status.icon).to eq 'icon_status_manual'
+ expect(status.favicon).to eq 'favicon_status_manual'
+ expect(status.label).to eq 'manual stop action (not allowed)'
+ expect(status).to have_details
+ expect(status).not_to have_action
+ end
end
end
end
diff --git a/spec/lib/gitlab/ci/status/build/play_spec.rb b/spec/lib/gitlab/ci/status/build/play_spec.rb
index 6c97a4fe5ca..f5d0f977768 100644
--- a/spec/lib/gitlab/ci/status/build/play_spec.rb
+++ b/spec/lib/gitlab/ci/status/build/play_spec.rb
@@ -1,43 +1,48 @@
require 'spec_helper'
describe Gitlab::Ci::Status::Build::Play do
- let(:status) { double('core') }
- let(:user) { double('user') }
+ let(:user) { create(:user) }
+ let(:build) { create(:ci_build, :manual) }
+ let(:status) { Gitlab::Ci::Status::Core.new(build, user) }
subject { described_class.new(status) }
describe '#label' do
- it { expect(subject.label).to eq 'manual play action' }
+ it 'has a label that says it is a manual action' do
+ expect(subject.label).to eq 'manual play action'
+ end
end
- describe 'action details' do
- let(:user) { create(:user) }
- let(:build) { create(:ci_build) }
- let(:status) { Gitlab::Ci::Status::Core.new(build, user) }
-
- describe '#has_action?' do
- context 'when user is allowed to update build' do
- before { build.project.team << [user, :developer] }
+ describe '#has_action?' do
+ context 'when user is allowed to update build' do
+ context 'when user can push to branch' do
+ before { build.project.add_master(user) }
it { is_expected.to have_action }
end
- context 'when user is not allowed to update build' do
+ context 'when user can not push to the branch' do
+ before { build.project.add_developer(user) }
+
it { is_expected.not_to have_action }
end
end
- describe '#action_path' do
- it { expect(subject.action_path).to include "#{build.id}/play" }
+ context 'when user is not allowed to update build' do
+ it { is_expected.not_to have_action }
end
+ end
- describe '#action_icon' do
- it { expect(subject.action_icon).to eq 'icon_action_play' }
- end
+ describe '#action_path' do
+ it { expect(subject.action_path).to include "#{build.id}/play" }
+ end
- describe '#action_title' do
- it { expect(subject.action_title).to eq 'Play' }
- end
+ describe '#action_icon' do
+ it { expect(subject.action_icon).to eq 'icon_action_play' }
+ end
+
+ describe '#action_title' do
+ it { expect(subject.action_title).to eq 'Play' }
end
describe '.matches?' do
diff --git a/spec/lib/gitlab/ci/status/extended_spec.rb b/spec/lib/gitlab/ci/status/extended_spec.rb
index c2d74ca5cde..6eacb07078b 100644
--- a/spec/lib/gitlab/ci/status/extended_spec.rb
+++ b/spec/lib/gitlab/ci/status/extended_spec.rb
@@ -1,12 +1,8 @@
require 'spec_helper'
describe Gitlab::Ci::Status::Extended do
- subject do
- Class.new.include(described_class)
- end
-
it 'requires subclass to implement matcher' do
- expect { subject.matches?(double, double) }
+ expect { described_class.matches?(double, double) }
.to raise_error(NotImplementedError)
end
end
diff --git a/spec/lib/gitlab/ci/status/group/common_spec.rb b/spec/lib/gitlab/ci/status/group/common_spec.rb
new file mode 100644
index 00000000000..c0ca05881f5
--- /dev/null
+++ b/spec/lib/gitlab/ci/status/group/common_spec.rb
@@ -0,0 +1,20 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Status::Group::Common do
+ subject do
+ Gitlab::Ci::Status::Core.new(double, double)
+ .extend(described_class)
+ end
+
+ it 'does not have action' do
+ expect(subject).not_to have_action
+ end
+
+ it 'has details' do
+ expect(subject).not_to have_details
+ end
+
+ it 'has no details_path' do
+ expect(subject.details_path).to be_falsy
+ end
+end
diff --git a/spec/lib/gitlab/ci/status/group/factory_spec.rb b/spec/lib/gitlab/ci/status/group/factory_spec.rb
new file mode 100644
index 00000000000..0cd83123938
--- /dev/null
+++ b/spec/lib/gitlab/ci/status/group/factory_spec.rb
@@ -0,0 +1,13 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Status::Group::Factory do
+ it 'inherits from the core factory' do
+ expect(described_class)
+ .to be < Gitlab::Ci::Status::Factory
+ end
+
+ it 'exposes group helpers' do
+ expect(described_class.common_helpers)
+ .to eq Gitlab::Ci::Status::Group::Common
+ end
+end
diff --git a/spec/lib/gitlab/conflict/file_collection_spec.rb b/spec/lib/gitlab/conflict/file_collection_spec.rb
index 39d892c18c0..27f23ea70dc 100644
--- a/spec/lib/gitlab/conflict/file_collection_spec.rb
+++ b/spec/lib/gitlab/conflict/file_collection_spec.rb
@@ -2,7 +2,7 @@ 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) }
+ let(:file_collection) { described_class.read_only(merge_request) }
describe '#files' do
it 'returns an array of Conflict::Files' do
diff --git a/spec/lib/gitlab/contributions_calendar_spec.rb b/spec/lib/gitlab/contributions_calendar_spec.rb
index e18a219ef36..79632e2b6a3 100644
--- a/spec/lib/gitlab/contributions_calendar_spec.rb
+++ b/spec/lib/gitlab/contributions_calendar_spec.rb
@@ -47,7 +47,7 @@ describe Gitlab::ContributionsCalendar do
action: Event::CREATED,
target: @targets[project],
author: contributor,
- created_at: day,
+ created_at: day
)
end
diff --git a/spec/lib/gitlab/current_settings_spec.rb b/spec/lib/gitlab/current_settings_spec.rb
index b01c4805a34..c796c98ec9f 100644
--- a/spec/lib/gitlab/current_settings_spec.rb
+++ b/spec/lib/gitlab/current_settings_spec.rb
@@ -10,7 +10,7 @@ describe Gitlab::CurrentSettings do
describe '#current_application_settings' do
context 'with DB available' do
before do
- allow_any_instance_of(Gitlab::CurrentSettings).to receive(:connect_to_db?).and_return(true)
+ allow_any_instance_of(described_class).to receive(:connect_to_db?).and_return(true)
end
it 'attempts to use cached values first' do
@@ -36,7 +36,7 @@ describe Gitlab::CurrentSettings do
context 'with DB unavailable' do
before do
- allow_any_instance_of(Gitlab::CurrentSettings).to receive(:connect_to_db?).and_return(false)
+ allow_any_instance_of(described_class).to receive(:connect_to_db?).and_return(false)
end
it 'returns an in-memory ApplicationSetting object' do
diff --git a/spec/lib/gitlab/cycle_analytics/base_event_fetcher_spec.rb b/spec/lib/gitlab/cycle_analytics/base_event_fetcher_spec.rb
index c455cd9b942..d8757c601ab 100644
--- a/spec/lib/gitlab/cycle_analytics/base_event_fetcher_spec.rb
+++ b/spec/lib/gitlab/cycle_analytics/base_event_fetcher_spec.rb
@@ -20,7 +20,7 @@ describe Gitlab::CycleAnalytics::BaseEventFetcher do
before do
allow_any_instance_of(Gitlab::ReferenceExtractor).to receive(:issues).and_return(Issue.all)
- allow_any_instance_of(Gitlab::CycleAnalytics::BaseEventFetcher).to receive(:serialize) do |event|
+ allow_any_instance_of(described_class).to receive(:serialize) do |event|
event
end
diff --git a/spec/lib/gitlab/cycle_analytics/events_spec.rb b/spec/lib/gitlab/cycle_analytics/events_spec.rb
index 9d2ba481919..3610a0354e8 100644
--- a/spec/lib/gitlab/cycle_analytics/events_spec.rb
+++ b/spec/lib/gitlab/cycle_analytics/events_spec.rb
@@ -11,8 +11,6 @@ describe 'cycle analytics events' do
end
before do
- allow_any_instance_of(Gitlab::ReferenceExtractor).to receive(:issues).and_return([context])
-
setup(context)
end
@@ -132,6 +130,8 @@ describe 'cycle analytics events' do
end
before do
+ merge_request.update(head_pipeline: pipeline)
+
create(:ci_build, pipeline: pipeline, status: :success, author: user)
create(:ci_build, pipeline: pipeline, status: :success, author: user)
@@ -228,6 +228,8 @@ describe 'cycle analytics events' do
end
before do
+ merge_request.update(head_pipeline: pipeline)
+
create(:ci_build, pipeline: pipeline, status: :success, author: user)
create(:ci_build, pipeline: pipeline, status: :success, author: user)
@@ -332,7 +334,7 @@ describe 'cycle analytics events' do
def setup(context)
milestone = create(:milestone, project: project)
context.update(milestone: milestone)
- mr = create_merge_request_closing_issue(context)
+ mr = create_merge_request_closing_issue(context, commit_message: "References #{context.to_reference}")
ProcessCommitWorker.new.perform(project.id, user.id, mr.commits.last.to_hash)
end
diff --git a/spec/lib/gitlab/data_builder/push_spec.rb b/spec/lib/gitlab/data_builder/push_spec.rb
index dbcfb9b7400..e59cba35b2f 100644
--- a/spec/lib/gitlab/data_builder/push_spec.rb
+++ b/spec/lib/gitlab/data_builder/push_spec.rb
@@ -35,6 +35,7 @@ describe Gitlab::DataBuilder::Push, lib: true do
it { expect(data[:ref]).to eq('refs/tags/v1.1.0') }
it { expect(data[:user_id]).to eq(user.id) }
it { expect(data[:user_name]).to eq(user.name) }
+ it { expect(data[:user_username]).to eq(user.username) }
it { expect(data[:user_email]).to eq(user.email) }
it { expect(data[:user_avatar]).to eq(user.avatar_url) }
it { expect(data[:project_id]).to eq(project.id) }
diff --git a/spec/lib/gitlab/database/migration_helpers_spec.rb b/spec/lib/gitlab/database/migration_helpers_spec.rb
index a044b871730..d6535f97665 100644
--- a/spec/lib/gitlab/database/migration_helpers_spec.rb
+++ b/spec/lib/gitlab/database/migration_helpers_spec.rb
@@ -382,11 +382,13 @@ describe Gitlab::Database::MigrationHelpers, lib: true do
expect(model).to receive(:add_column).
with(:users, :new, :integer,
limit: old_column.limit,
- default: old_column.default,
null: old_column.null,
precision: old_column.precision,
scale: old_column.scale)
+ expect(model).to receive(:change_column_default).
+ with(:users, :new, old_column.default)
+
expect(model).to receive(:update_column_in_batches)
expect(model).to receive(:copy_indexes).with(:users, :old, :new)
@@ -406,11 +408,13 @@ describe Gitlab::Database::MigrationHelpers, lib: true do
expect(model).to receive(:add_column).
with(:users, :new, :integer,
limit: old_column.limit,
- default: old_column.default,
null: old_column.null,
precision: old_column.precision,
scale: old_column.scale)
+ expect(model).to receive(:change_column_default).
+ with(:users, :new, old_column.default)
+
expect(model).to receive(:update_column_in_batches)
expect(model).to receive(:copy_indexes).with(:users, :old, :new)
@@ -726,4 +730,37 @@ describe Gitlab::Database::MigrationHelpers, lib: true do
expect(model.column_for(:users, :kittens)).to be_nil
end
end
+
+ describe '#replace_sql' do
+ context 'using postgres' do
+ before do
+ allow(Gitlab::Database).to receive(:mysql?).and_return(false)
+ end
+
+ it 'builds the sql with correct functions' do
+ expect(model.replace_sql(Arel::Table.new(:users)[:first_name], "Alice", "Eve").to_s).
+ to include('regexp_replace')
+ end
+ end
+
+ context 'using mysql' do
+ before do
+ allow(Gitlab::Database).to receive(:mysql?).and_return(true)
+ end
+
+ it 'builds the sql with the correct functions' do
+ expect(model.replace_sql(Arel::Table.new(:users)[:first_name], "Alice", "Eve").to_s).
+ to include('locate', 'insert')
+ end
+ end
+
+ describe 'results' do
+ let!(:user) { create(:user, name: 'Kathy Alice Aliceson') }
+
+ it 'replaces the correct part of the string' do
+ model.update_column_in_batches(:users, :name, model.replace_sql(Arel::Table.new(:users)[:name], 'Alice', 'Eve'))
+ expect(user.reload.name).to eq('Kathy Eve Aliceson')
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base_spec.rb b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base_spec.rb
new file mode 100644
index 00000000000..64bc5fc0429
--- /dev/null
+++ b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base_spec.rb
@@ -0,0 +1,197 @@
+require 'spec_helper'
+
+describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameBase do
+ let(:migration) { FakeRenameReservedPathMigrationV1.new }
+ let(:subject) { described_class.new(['the-path'], migration) }
+
+ before do
+ allow(migration).to receive(:say)
+ end
+
+ def migration_namespace(namespace)
+ Gitlab::Database::RenameReservedPathsMigration::V1::MigrationClasses::
+ Namespace.find(namespace.id)
+ end
+
+ def migration_project(project)
+ Gitlab::Database::RenameReservedPathsMigration::V1::MigrationClasses::
+ Project.find(project.id)
+ end
+
+ describe "#remove_last_ocurrence" do
+ it "removes only the last occurance of a string" do
+ input = "this/is/a-word-to-replace/namespace/with/a-word-to-replace"
+
+ expect(subject.remove_last_occurrence(input, "a-word-to-replace"))
+ .to eq("this/is/a-word-to-replace/namespace/with/")
+ end
+ end
+
+ describe '#remove_cached_html_for_projects' do
+ let(:project) { create(:empty_project, description_html: 'Project description') }
+
+ it 'removes description_html from projects' do
+ subject.remove_cached_html_for_projects([project.id])
+
+ expect(project.reload.description_html).to be_nil
+ end
+
+ it 'removes issue descriptions' do
+ issue = create(:issue, project: project, description_html: 'Issue description')
+
+ subject.remove_cached_html_for_projects([project.id])
+
+ expect(issue.reload.description_html).to be_nil
+ end
+
+ it 'removes merge request descriptions' do
+ merge_request = create(:merge_request,
+ source_project: project,
+ target_project: project,
+ description_html: 'MergeRequest description')
+
+ subject.remove_cached_html_for_projects([project.id])
+
+ expect(merge_request.reload.description_html).to be_nil
+ end
+
+ it 'removes note html' do
+ note = create(:note,
+ project: project,
+ noteable: create(:issue, project: project),
+ note_html: 'note description')
+
+ subject.remove_cached_html_for_projects([project.id])
+
+ expect(note.reload.note_html).to be_nil
+ end
+
+ it 'removes milestone description' do
+ milestone = create(:milestone,
+ project: project,
+ description_html: 'milestone description')
+
+ subject.remove_cached_html_for_projects([project.id])
+
+ expect(milestone.reload.description_html).to be_nil
+ end
+ end
+
+ describe '#rename_path_for_routable' do
+ context 'for namespaces' do
+ let(:namespace) { create(:namespace, path: 'the-path') }
+ it "renames namespaces called the-path" do
+ subject.rename_path_for_routable(migration_namespace(namespace))
+
+ expect(namespace.reload.path).to eq("the-path0")
+ end
+
+ it "renames the route to the namespace" do
+ subject.rename_path_for_routable(migration_namespace(namespace))
+
+ expect(Namespace.find(namespace.id).full_path).to eq("the-path0")
+ end
+
+ it "renames the route for projects of the namespace" do
+ project = create(:project, path: "project-path", namespace: namespace)
+
+ subject.rename_path_for_routable(migration_namespace(namespace))
+
+ expect(project.route.reload.path).to eq("the-path0/project-path")
+ end
+
+ it 'returns the old & the new path' do
+ old_path, new_path = subject.rename_path_for_routable(migration_namespace(namespace))
+
+ expect(old_path).to eq('the-path')
+ expect(new_path).to eq('the-path0')
+ end
+
+ context "the-path namespace -> subgroup -> the-path0 project" do
+ it "updates the route of the project correctly" do
+ subgroup = create(:group, path: "subgroup", parent: namespace)
+ project = create(:project, path: "the-path0", namespace: subgroup)
+
+ subject.rename_path_for_routable(migration_namespace(namespace))
+
+ expect(project.route.reload.path).to eq("the-path0/subgroup/the-path0")
+ end
+ end
+ end
+
+ context 'for projects' do
+ let(:parent) { create(:namespace, path: 'the-parent') }
+ let(:project) { create(:empty_project, path: 'the-path', namespace: parent) }
+
+ it 'renames the project called `the-path`' do
+ subject.rename_path_for_routable(migration_project(project))
+
+ expect(project.reload.path).to eq('the-path0')
+ end
+
+ it 'renames the route for the project' do
+ subject.rename_path_for_routable(project)
+
+ expect(project.reload.route.path).to eq('the-parent/the-path0')
+ end
+
+ it 'returns the old & new path' do
+ old_path, new_path = subject.rename_path_for_routable(migration_project(project))
+
+ expect(old_path).to eq('the-parent/the-path')
+ expect(new_path).to eq('the-parent/the-path0')
+ end
+ end
+ end
+
+ describe '#move_pages' do
+ it 'moves the pages directory' do
+ expect(subject).to receive(:move_folders)
+ .with(TestEnv.pages_path, 'old-path', 'new-path')
+
+ subject.move_pages('old-path', 'new-path')
+ end
+ end
+
+ describe "#move_uploads" do
+ let(:test_dir) { File.join(Rails.root, 'tmp', 'tests', 'rename_reserved_paths') }
+ let(:uploads_dir) { File.join(test_dir, 'public', 'uploads') }
+
+ it 'moves subdirectories in the uploads folder' do
+ expect(subject).to receive(:uploads_dir).and_return(uploads_dir)
+ expect(subject).to receive(:move_folders).with(uploads_dir, 'old_path', 'new_path')
+
+ subject.move_uploads('old_path', 'new_path')
+ end
+
+ it "doesn't move uploads when they are stored in object storage" do
+ expect(subject).to receive(:file_storage?).and_return(false)
+ expect(subject).not_to receive(:move_folders)
+
+ subject.move_uploads('old_path', 'new_path')
+ end
+ end
+
+ describe '#move_folders' do
+ let(:test_dir) { File.join(Rails.root, 'tmp', 'tests', 'rename_reserved_paths') }
+ let(:uploads_dir) { File.join(test_dir, 'public', 'uploads') }
+
+ before do
+ FileUtils.remove_dir(test_dir) if File.directory?(test_dir)
+ FileUtils.mkdir_p(uploads_dir)
+ allow(subject).to receive(:uploads_dir).and_return(uploads_dir)
+ end
+
+ it 'moves a folder with files' do
+ source = File.join(uploads_dir, 'parent-group', 'sub-group')
+ FileUtils.mkdir_p(source)
+ destination = File.join(uploads_dir, 'parent-group', 'moved-group')
+ FileUtils.touch(File.join(source, 'test.txt'))
+ expected_file = File.join(destination, 'test.txt')
+
+ subject.move_folders(uploads_dir, File.join('parent-group', 'sub-group'), File.join('parent-group', 'moved-group'))
+
+ expect(File.exist?(expected_file)).to be(true)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces_spec.rb b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces_spec.rb
new file mode 100644
index 00000000000..ec444942804
--- /dev/null
+++ b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces_spec.rb
@@ -0,0 +1,202 @@
+require 'spec_helper'
+
+describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameNamespaces do
+ let(:migration) { FakeRenameReservedPathMigrationV1.new }
+ let(:subject) { described_class.new(['the-path'], migration) }
+
+ before do
+ allow(migration).to receive(:say)
+ end
+
+ def migration_namespace(namespace)
+ Gitlab::Database::RenameReservedPathsMigration::V1::MigrationClasses::
+ Namespace.find(namespace.id)
+ end
+
+ describe '#namespaces_for_paths' do
+ context 'nested namespaces' do
+ let(:subject) { described_class.new(['parent/the-Path'], migration) }
+
+ it 'includes the namespace' do
+ parent = create(:namespace, path: 'parent')
+ child = create(:namespace, path: 'the-path', parent: parent)
+
+ found_ids = subject.namespaces_for_paths(type: :child).
+ map(&:id)
+
+ expect(found_ids).to contain_exactly(child.id)
+ end
+ end
+
+ context 'for child namespaces' do
+ it 'only returns child namespaces with the correct path' do
+ _root_namespace = create(:namespace, path: 'THE-path')
+ _other_path = create(:namespace,
+ path: 'other',
+ parent: create(:namespace))
+ namespace = create(:namespace,
+ path: 'the-path',
+ parent: create(:namespace))
+
+ found_ids = subject.namespaces_for_paths(type: :child).
+ map(&:id)
+
+ expect(found_ids).to contain_exactly(namespace.id)
+ end
+
+ it 'has no namespaces that look the same' do
+ _root_namespace = create(:namespace, path: 'THE-path')
+ _similar_path = create(:namespace,
+ path: 'not-really-the-path',
+ parent: create(:namespace))
+ namespace = create(:namespace,
+ path: 'the-path',
+ parent: create(:namespace))
+
+ found_ids = subject.namespaces_for_paths(type: :child).
+ map(&:id)
+
+ expect(found_ids).to contain_exactly(namespace.id)
+ end
+ end
+
+ context 'for top levelnamespaces' do
+ it 'only returns child namespaces with the correct path' do
+ root_namespace = create(:namespace, path: 'the-path')
+ _other_path = create(:namespace, path: 'other')
+ _child_namespace = create(:namespace,
+ path: 'the-path',
+ parent: create(:namespace))
+
+ found_ids = subject.namespaces_for_paths(type: :top_level).
+ map(&:id)
+
+ expect(found_ids).to contain_exactly(root_namespace.id)
+ end
+
+ it 'has no namespaces that just look the same' do
+ root_namespace = create(:namespace, path: 'the-path')
+ _similar_path = create(:namespace, path: 'not-really-the-path')
+ _child_namespace = create(:namespace,
+ path: 'the-path',
+ parent: create(:namespace))
+
+ found_ids = subject.namespaces_for_paths(type: :top_level).
+ map(&:id)
+
+ expect(found_ids).to contain_exactly(root_namespace.id)
+ end
+ end
+ end
+
+ describe '#move_repositories' do
+ let(:namespace) { create(:group, name: 'hello-group') }
+ it 'moves a project for a namespace' do
+ create(:project, namespace: namespace, path: 'hello-project')
+ expected_path = File.join(TestEnv.repos_path, 'bye-group', 'hello-project.git')
+
+ subject.move_repositories(namespace, 'hello-group', 'bye-group')
+
+ expect(File.directory?(expected_path)).to be(true)
+ end
+
+ it 'moves a namespace in a subdirectory correctly' do
+ child_namespace = create(:group, name: 'sub-group', parent: namespace)
+ create(:project, namespace: child_namespace, path: 'hello-project')
+
+ expected_path = File.join(TestEnv.repos_path, 'hello-group', 'renamed-sub-group', 'hello-project.git')
+
+ subject.move_repositories(child_namespace, 'hello-group/sub-group', 'hello-group/renamed-sub-group')
+
+ expect(File.directory?(expected_path)).to be(true)
+ end
+
+ it 'moves a parent namespace with subdirectories' do
+ child_namespace = create(:group, name: 'sub-group', parent: namespace)
+ create(:project, namespace: child_namespace, path: 'hello-project')
+ expected_path = File.join(TestEnv.repos_path, 'renamed-group', 'sub-group', 'hello-project.git')
+
+ subject.move_repositories(child_namespace, 'hello-group', 'renamed-group')
+
+ expect(File.directory?(expected_path)).to be(true)
+ end
+ end
+
+ describe "#child_ids_for_parent" do
+ it "collects child ids for all levels" do
+ parent = create(:namespace)
+ first_child = create(:namespace, parent: parent)
+ second_child = create(:namespace, parent: parent)
+ third_child = create(:namespace, parent: second_child)
+ all_ids = [parent.id, first_child.id, second_child.id, third_child.id]
+
+ collected_ids = subject.child_ids_for_parent(parent, ids: [parent.id])
+
+ expect(collected_ids).to contain_exactly(*all_ids)
+ end
+ end
+
+ describe "#rename_namespace" do
+ let(:namespace) { create(:namespace, path: 'the-path') }
+
+ it 'renames paths & routes for the namespace' do
+ expect(subject).to receive(:rename_path_for_routable).
+ with(namespace).
+ and_call_original
+
+ subject.rename_namespace(namespace)
+
+ expect(namespace.reload.path).to eq('the-path0')
+ end
+
+ it "moves the the repository for a project in the namespace" do
+ create(:project, namespace: namespace, path: "the-path-project")
+ expected_repo = File.join(TestEnv.repos_path, "the-path0", "the-path-project.git")
+
+ subject.rename_namespace(namespace)
+
+ expect(File.directory?(expected_repo)).to be(true)
+ end
+
+ it "moves the uploads for the namespace" do
+ expect(subject).to receive(:move_uploads).with("the-path", "the-path0")
+
+ subject.rename_namespace(namespace)
+ end
+
+ it "moves the pages for the namespace" do
+ expect(subject).to receive(:move_pages).with("the-path", "the-path0")
+
+ subject.rename_namespace(namespace)
+ end
+
+ it 'invalidates the markdown cache of related projects' do
+ project = create(:empty_project, namespace: namespace, path: "the-path-project")
+
+ expect(subject).to receive(:remove_cached_html_for_projects).with([project.id])
+
+ subject.rename_namespace(namespace)
+ end
+ end
+
+ describe '#rename_namespaces' do
+ let!(:top_level_namespace) { create(:namespace, path: 'the-path') }
+ let!(:child_namespace) do
+ create(:namespace, path: 'the-path', parent: create(:namespace))
+ end
+
+ it 'renames top level namespaces the namespace' do
+ expect(subject).to receive(:rename_namespace).
+ with(migration_namespace(top_level_namespace))
+
+ subject.rename_namespaces(type: :top_level)
+ end
+
+ it 'renames child namespaces' do
+ expect(subject).to receive(:rename_namespace).
+ with(migration_namespace(child_namespace))
+
+ subject.rename_namespaces(type: :child)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects_spec.rb b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects_spec.rb
new file mode 100644
index 00000000000..59e8de2712d
--- /dev/null
+++ b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects_spec.rb
@@ -0,0 +1,102 @@
+require 'spec_helper'
+
+describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameProjects do
+ let(:migration) { FakeRenameReservedPathMigrationV1.new }
+ let(:subject) { described_class.new(['the-path'], migration) }
+
+ before do
+ allow(migration).to receive(:say)
+ end
+
+ describe '#projects_for_paths' do
+ it 'searches using nested paths' do
+ namespace = create(:namespace, path: 'hello')
+ project = create(:empty_project, path: 'THE-path', namespace: namespace)
+
+ result_ids = described_class.new(['Hello/the-path'], migration).
+ projects_for_paths.map(&:id)
+
+ expect(result_ids).to contain_exactly(project.id)
+ end
+
+ it 'includes the correct projects' do
+ project = create(:empty_project, path: 'THE-path')
+ _other_project = create(:empty_project)
+
+ result_ids = subject.projects_for_paths.map(&:id)
+
+ expect(result_ids).to contain_exactly(project.id)
+ end
+ end
+
+ describe '#rename_projects' do
+ let!(:projects) { create_list(:empty_project, 2, path: 'the-path') }
+
+ it 'renames each project' do
+ expect(subject).to receive(:rename_project).twice
+
+ subject.rename_projects
+ end
+
+ it 'invalidates the markdown cache of related projects' do
+ expect(subject).to receive(:remove_cached_html_for_projects).
+ with(projects.map(&:id))
+
+ subject.rename_projects
+ end
+ end
+
+ describe '#rename_project' do
+ let(:project) do
+ create(:empty_project,
+ path: 'the-path',
+ namespace: create(:namespace, path: 'known-parent' ))
+ end
+
+ it 'renames path & route for the project' do
+ expect(subject).to receive(:rename_path_for_routable).
+ with(project).
+ and_call_original
+
+ subject.rename_project(project)
+
+ expect(project.reload.path).to eq('the-path0')
+ end
+
+ it 'moves the wiki & the repo' do
+ expect(subject).to receive(:move_repository).
+ with(project, 'known-parent/the-path.wiki', 'known-parent/the-path0.wiki')
+ expect(subject).to receive(:move_repository).
+ with(project, 'known-parent/the-path', 'known-parent/the-path0')
+
+ subject.rename_project(project)
+ end
+
+ it 'moves uploads' do
+ expect(subject).to receive(:move_uploads).
+ with('known-parent/the-path', 'known-parent/the-path0')
+
+ subject.rename_project(project)
+ end
+
+ it 'moves pages' do
+ expect(subject).to receive(:move_pages).
+ with('known-parent/the-path', 'known-parent/the-path0')
+
+ subject.rename_project(project)
+ end
+ end
+
+ describe '#move_repository' do
+ let(:known_parent) { create(:namespace, path: 'known-parent') }
+ let(:project) { create(:project, path: 'the-path', namespace: known_parent) }
+
+ it 'moves the repository for a project' do
+ expected_path = File.join(TestEnv.repos_path, 'known-parent', 'new-repo.git')
+
+ subject.move_repository(project, 'known-parent/the-path', 'known-parent/new-repo')
+
+ expect(File.directory?(expected_path)).to be(true)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/database/rename_reserved_paths_migration/v1_spec.rb b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1_spec.rb
new file mode 100644
index 00000000000..f8cc1eb91ec
--- /dev/null
+++ b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1_spec.rb
@@ -0,0 +1,54 @@
+require 'spec_helper'
+
+shared_examples 'renames child namespaces' do |type|
+ it 'renames namespaces' do
+ rename_namespaces = double
+ expect(described_class::RenameNamespaces).
+ to receive(:new).with(['first-path', 'second-path'], subject).
+ and_return(rename_namespaces)
+ expect(rename_namespaces).to receive(:rename_namespaces).
+ with(type: :child)
+
+ subject.rename_wildcard_paths(['first-path', 'second-path'])
+ end
+end
+
+describe Gitlab::Database::RenameReservedPathsMigration::V1 do
+ let(:subject) { FakeRenameReservedPathMigrationV1.new }
+
+ before do
+ allow(subject).to receive(:say)
+ end
+
+ describe '#rename_child_paths' do
+ it_behaves_like 'renames child namespaces'
+ end
+
+ describe '#rename_wildcard_paths' do
+ it_behaves_like 'renames child namespaces'
+
+ it 'should rename projects' do
+ rename_projects = double
+ expect(described_class::RenameProjects).
+ to receive(:new).with(['the-path'], subject).
+ and_return(rename_projects)
+
+ expect(rename_projects).to receive(:rename_projects)
+
+ subject.rename_wildcard_paths(['the-path'])
+ end
+ end
+
+ describe '#rename_root_paths' do
+ it 'should rename namespaces' do
+ rename_namespaces = double
+ expect(described_class::RenameNamespaces).
+ to receive(:new).with(['the-path'], subject).
+ and_return(rename_namespaces)
+ expect(rename_namespaces).to receive(:rename_namespaces).
+ with(type: :top_level)
+
+ subject.rename_root_paths('the-path')
+ end
+ end
+end
diff --git a/spec/lib/gitlab/dependency_linker/gemfile_linker_spec.rb b/spec/lib/gitlab/dependency_linker/gemfile_linker_spec.rb
new file mode 100644
index 00000000000..2e52097a946
--- /dev/null
+++ b/spec/lib/gitlab/dependency_linker/gemfile_linker_spec.rb
@@ -0,0 +1,60 @@
+require 'rails_helper'
+
+describe Gitlab::DependencyLinker::GemfileLinker, lib: true do
+ describe '.support?' do
+ it 'supports Gemfile' do
+ expect(described_class.support?('Gemfile')).to be_truthy
+ end
+
+ it 'supports gems.rb' do
+ expect(described_class.support?('gems.rb')).to be_truthy
+ end
+
+ it 'does not support other files' do
+ expect(described_class.support?('Gemfile.lock')).to be_falsey
+ end
+ end
+
+ describe '#link' do
+ let(:file_name) { 'Gemfile' }
+
+ let(:file_content) do
+ <<-CONTENT.strip_heredoc
+ source 'https://rubygems.org'
+
+ gem "rails", '4.2.6', github: "rails/rails"
+ gem 'rails-deprecated_sanitizer', '~> 1.0.3'
+ gem 'responders', '~> 2.0', :github => 'rails/responders'
+ gem 'sprockets', '~> 3.6.0', git: 'https://gitlab.example.com/gems/sprockets'
+ gem 'default_value_for', '~> 3.0.0'
+ CONTENT
+ end
+
+ subject { Gitlab::Highlight.highlight(file_name, file_content) }
+
+ def link(name, url)
+ %{<a href="#{url}" rel="noopener noreferrer" target="_blank">#{name}</a>}
+ end
+
+ it 'links sources' do
+ expect(subject).to include(link('https://rubygems.org', 'https://rubygems.org'))
+ end
+
+ it 'links dependencies' do
+ expect(subject).to include(link('rails', 'https://rubygems.org/gems/rails'))
+ expect(subject).to include(link('rails-deprecated_sanitizer', 'https://rubygems.org/gems/rails-deprecated_sanitizer'))
+ expect(subject).to include(link('responders', 'https://rubygems.org/gems/responders'))
+ expect(subject).to include(link('sprockets', 'https://rubygems.org/gems/sprockets'))
+ expect(subject).to include(link('default_value_for', 'https://rubygems.org/gems/default_value_for'))
+ end
+
+ it 'links GitHub repos' do
+ expect(subject).to include(link('rails/rails', 'https://github.com/rails/rails'))
+ expect(subject).to include(link('rails/responders', 'https://github.com/rails/responders'))
+ end
+
+ it 'links Git repos' do
+ expect(subject).to include(link('https://gitlab.example.com/gems/sprockets', 'https://gitlab.example.com/gems/sprockets'))
+ end
+ end
+end
diff --git a/spec/lib/gitlab/dependency_linker_spec.rb b/spec/lib/gitlab/dependency_linker_spec.rb
new file mode 100644
index 00000000000..03d5b61d70c
--- /dev/null
+++ b/spec/lib/gitlab/dependency_linker_spec.rb
@@ -0,0 +1,13 @@
+require 'rails_helper'
+
+describe Gitlab::DependencyLinker, lib: true do
+ describe '.link' do
+ it 'links using GemfileLinker' do
+ blob_name = 'Gemfile'
+
+ expect(described_class::GemfileLinker).to receive(:link)
+
+ described_class.link(blob_name, nil, nil)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/diff/highlight_spec.rb b/spec/lib/gitlab/diff/highlight_spec.rb
index c6bd4e81f4f..7d7d4a55e63 100644
--- a/spec/lib/gitlab/diff/highlight_spec.rb
+++ b/spec/lib/gitlab/diff/highlight_spec.rb
@@ -34,7 +34,7 @@ describe Gitlab::Diff::Highlight, lib: true do
end
it 'highlights and marks added lines' do
- code = %Q{+<span id="LC9" class="line" lang="ruby"> <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}
+ code = %Q{+<span id="LC9" class="line" lang="ruby"> <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
@@ -67,7 +67,7 @@ describe Gitlab::Diff::Highlight, lib: true do
end
it 'marks added lines' do
- code = %q{+ raise <span class='idiff left right'>RuntimeError, </span>&quot;System commands must be given as an array of strings&quot;}
+ code = %q{+ raise <span class="idiff left right">RuntimeError, </span>&quot;System commands must be given as an array of strings&quot;}
expect(subject[5].text).to eq(code)
expect(subject[5].text).to be_html_safe
diff --git a/spec/lib/gitlab/diff/inline_diff_markdown_marker_spec.rb b/spec/lib/gitlab/diff/inline_diff_markdown_marker_spec.rb
new file mode 100644
index 00000000000..d6e8b8ac4b2
--- /dev/null
+++ b/spec/lib/gitlab/diff/inline_diff_markdown_marker_spec.rb
@@ -0,0 +1,14 @@
+require 'spec_helper'
+
+describe Gitlab::Diff::InlineDiffMarkdownMarker, lib: true do
+ describe '#mark' do
+ let(:raw) { "abc 'def'" }
+ let(:inline_diffs) { [2..5] }
+ let(:subject) { described_class.new(raw).mark(inline_diffs, mode: :deletion) }
+
+ it 'marks the range' do
+ expect(subject).to eq("ab{-c &#39;d-}ef&#39;")
+ expect(subject).to be_html_safe
+ end
+ end
+end
diff --git a/spec/lib/gitlab/diff/inline_diff_marker_spec.rb b/spec/lib/gitlab/diff/inline_diff_marker_spec.rb
index 198ff977f24..95da344802d 100644
--- a/spec/lib/gitlab/diff/inline_diff_marker_spec.rb
+++ b/spec/lib/gitlab/diff/inline_diff_marker_spec.rb
@@ -1,26 +1,26 @@
require 'spec_helper'
describe Gitlab::Diff::InlineDiffMarker, lib: true do
- describe '#inline_diffs' do
+ describe '#mark' do
context "when the rich text is html safe" do
- let(:raw) { "abc 'def'" }
+ let(:raw) { "abc 'def'" }
let(:rich) { %{<span class="abc">abc</span><span class="space"> </span><span class="def">&#39;def&#39;</span>}.html_safe }
let(:inline_diffs) { [2..5] }
- let(:subject) { Gitlab::Diff::InlineDiffMarker.new(raw, rich).mark(inline_diffs) }
+ let(:subject) { described_class.new(raw, rich).mark(inline_diffs) }
- it 'marks the inline diffs' do
- expect(subject).to eq(%{<span class="abc">ab<span class='idiff left'>c</span></span><span class="space"><span class='idiff'> </span></span><span class="def"><span class='idiff right'>&#39;d</span>ef&#39;</span>})
+ it 'marks the range' do
+ expect(subject).to eq(%{<span class="abc">ab<span class="idiff left">c</span></span><span class="space"><span class="idiff"> </span></span><span class="def"><span class="idiff right">&#39;d</span>ef&#39;</span>})
expect(subject).to be_html_safe
end
end
context "when the text text is not html safe" do
- let(:raw) { "abc 'def'" }
+ let(:raw) { "abc 'def'" }
let(:inline_diffs) { [2..5] }
- let(:subject) { Gitlab::Diff::InlineDiffMarker.new(raw).mark(inline_diffs) }
+ let(:subject) { described_class.new(raw).mark(inline_diffs) }
- it 'marks the inline diffs' do
- expect(subject).to eq(%{ab<span class='idiff left right'>c &#39;d</span>ef&#39;})
+ it 'marks the range' do
+ expect(subject).to eq(%{ab<span class="idiff left right">c &#39;d</span>ef&#39;})
expect(subject).to be_html_safe
end
end
diff --git a/spec/lib/gitlab/diff/position_tracer_spec.rb b/spec/lib/gitlab/diff/position_tracer_spec.rb
index c166f83664a..4d202a76e1b 100644
--- a/spec/lib/gitlab/diff/position_tracer_spec.rb
+++ b/spec/lib/gitlab/diff/position_tracer_spec.rb
@@ -569,13 +569,8 @@ describe Gitlab::Diff::PositionTracer, lib: true do
# 1 1 BB
# 2 2 A
- it "returns the new position" do
- expect_new_position(
- old_path: file_name,
- new_path: new_file_name,
- old_line: old_position.new_line,
- new_line: old_position.new_line
- )
+ it "returns nil since the line doesn't exist in the new diffs anymore" do
+ expect(subject).to be_nil
end
end
@@ -1377,7 +1372,7 @@ describe Gitlab::Diff::PositionTracer, lib: true do
nil,
{ old_path: file_name, new_path: file_name, old_line: 5, new_line: 5 },
{ old_path: file_name, old_line: 6 },
- { new_path: file_name, new_line: 7 },
+ { new_path: file_name, new_line: 7 }
]
expect_positions(old_position_attrs, new_position_attrs)
@@ -1449,7 +1444,7 @@ describe Gitlab::Diff::PositionTracer, lib: true do
nil,
{ old_path: file_name, new_path: file_name, old_line: 5, new_line: 5 },
{ old_path: file_name, old_line: 6 },
- { new_path: file_name, new_line: 7 },
+ { new_path: file_name, new_line: 7 }
]
expect_positions(old_position_attrs, new_position_attrs)
@@ -1503,7 +1498,7 @@ describe Gitlab::Diff::PositionTracer, lib: true do
{ old_path: file_name, new_path: file_name, old_line: 5, new_line: 4 },
{ old_path: file_name, new_path: file_name, old_line: 6, new_line: 5 },
nil,
- { new_path: file_name, new_line: 6 },
+ { new_path: file_name, new_line: 6 }
]
expect_positions(old_position_attrs, new_position_attrs)
@@ -1751,7 +1746,7 @@ describe Gitlab::Diff::PositionTracer, lib: true do
{ old_path: file_name, new_path: file_name, old_line: 4, new_line: 5 },
{ old_path: file_name, old_line: 5 },
{ new_path: file_name, new_line: 6 },
- { new_path: file_name, new_line: 7 },
+ { new_path: file_name, new_line: 7 }
]
expect_positions(old_position_attrs, new_position_attrs)
diff --git a/spec/lib/gitlab/email/receiver_spec.rb b/spec/lib/gitlab/email/receiver_spec.rb
index 2a86b427806..c6e3524f743 100644
--- a/spec/lib/gitlab/email/receiver_spec.rb
+++ b/spec/lib/gitlab/email/receiver_spec.rb
@@ -4,12 +4,38 @@ require_relative 'email_shared_blocks'
describe Gitlab::Email::Receiver, lib: true do
include_context :email_shared_context
+ context "when the email contains a valid email address in a Delivered-To header" do
+ let(:email_raw) { fixture_file('emails/forwarded_new_issue.eml') }
+ let(:handler) { double(:handler) }
+
+ before do
+ stub_incoming_email_setting(enabled: true, address: "incoming+%{key}@appmail.adventuretime.ooo")
+
+ allow(handler).to receive(:execute)
+ allow(handler).to receive(:metrics_params)
+ end
+
+ it "finds the mail key" do
+ expect(Gitlab::Email::Handler).to receive(:for).with(an_instance_of(Mail::Message), 'gitlabhq/gitlabhq+auth_token').and_return(handler)
+
+ receiver.execute
+ end
+ end
+
context "when we cannot find a capable handler" do
let(:email_raw) { fixture_file('emails/valid_reply.eml').gsub(mail_key, "!!!") }
- it "raises a UnknownIncomingEmail" do
+ it "raises an UnknownIncomingEmail error" do
expect { receiver.execute }.to raise_error(Gitlab::Email::UnknownIncomingEmail)
end
+
+ context "and the email contains no references header" do
+ let(:email_raw) { fixture_file("emails/auto_reply.eml").gsub(mail_key, "!!!") }
+
+ it "raises an UnknownIncomingEmail error" do
+ expect { receiver.execute }.to raise_error(Gitlab::Email::UnknownIncomingEmail)
+ end
+ end
end
context "when the email is blank" do
diff --git a/spec/lib/gitlab/file_finder_spec.rb b/spec/lib/gitlab/file_finder_spec.rb
new file mode 100644
index 00000000000..5a32ffd462c
--- /dev/null
+++ b/spec/lib/gitlab/file_finder_spec.rb
@@ -0,0 +1,21 @@
+require 'spec_helper'
+
+describe Gitlab::FileFinder, lib: true do
+ describe '#find' do
+ let(:project) { create(:project, :public, :repository) }
+ let(:finder) { described_class.new(project, project.default_branch) }
+
+ it 'finds by name' do
+ results = finder.find('files')
+ expect(results.map(&:first)).to include('files/images/wm.svg')
+ end
+
+ it 'finds by content' do
+ results = finder.find('files')
+
+ blob = results.select { |result| result.first == "CHANGELOG" }.flatten.last
+
+ expect(blob.filename).to eq("CHANGELOG")
+ end
+ end
+end
diff --git a/spec/lib/gitlab/git/diff_spec.rb b/spec/lib/gitlab/git/diff_spec.rb
index 7253a2edeff..4189aaef643 100644
--- a/spec/lib/gitlab/git/diff_spec.rb
+++ b/spec/lib/gitlab/git/diff_spec.rb
@@ -120,7 +120,7 @@ EOT
new_mode: 0100644,
from_id: '357406f3075a57708d0163752905cc1576fceacc',
to_id: '8e5177d718c561d36efde08bad36b43687ee6bf0',
- raw_chunks: raw_chunks,
+ raw_chunks: raw_chunks
)
)
end
diff --git a/spec/lib/gitlab/git/encoding_helper_spec.rb b/spec/lib/gitlab/git/encoding_helper_spec.rb
index f6ac7b23d1d..1a3bf802a07 100644
--- a/spec/lib/gitlab/git/encoding_helper_spec.rb
+++ b/spec/lib/gitlab/git/encoding_helper_spec.rb
@@ -19,8 +19,8 @@ describe Gitlab::Git::EncodingHelper do
[
'removes invalid bytes from ASCII-8bit encoded multibyte string. This can occur when a git diff match line truncates in the middle of a multibyte character. This occurs after the second word in this example. The test string is as short as we can get while still triggering the error condition when not looking at `detect[:confidence]`.',
"mu ns\xC3\n Lorem ipsum dolor sit amet, consectetur adipisicing ut\xC3\xA0y\xC3\xB9abcd\xC3\xB9efg kia elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non p\n {: .normal_pn}\n \n-Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in\n# *Lorem ipsum\xC3\xB9l\xC3\xB9l\xC3\xA0 dolor\xC3\xB9k\xC3\xB9 sit\xC3\xA8b\xC3\xA8 N\xC3\xA8 amet b\xC3\xA0d\xC3\xAC*\n+# *consectetur\xC3\xB9l\xC3\xB9l\xC3\xA0 adipisicing\xC3\xB9k\xC3\xB9 elit\xC3\xA8b\xC3\xA8 N\xC3\xA8 sed do\xC3\xA0d\xC3\xAC*{: .italic .smcaps}\n \n \xEF\x9B\xA1 eiusmod tempor incididunt, ut\xC3\xAAn\xC3\xB9 labore et dolore. Tw\xC4\x83nj\xC3\xAC magna aliqua. Ut enim ad minim veniam\n {: .normal}\n@@ -9,5 +9,5 @@ quis nostrud\xC3\xAAt\xC3\xB9 exercitiation ullamco laboris m\xC3\xB9s\xC3\xB9k\xC3\xB9abc\xC3\xB9 nisi ".force_encoding('ASCII-8BIT'),
- "mu ns\n Lorem ipsum dolor sit amet, consectetur adipisicing ut\xC3\xA0y\xC3\xB9abcd\xC3\xB9efg kia elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non p\n {: .normal_pn}\n \n-Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in\n# *Lorem ipsum\xC3\xB9l\xC3\xB9l\xC3\xA0 dolor\xC3\xB9k\xC3\xB9 sit\xC3\xA8b\xC3\xA8 N\xC3\xA8 amet b\xC3\xA0d\xC3\xAC*\n+# *consectetur\xC3\xB9l\xC3\xB9l\xC3\xA0 adipisicing\xC3\xB9k\xC3\xB9 elit\xC3\xA8b\xC3\xA8 N\xC3\xA8 sed do\xC3\xA0d\xC3\xAC*{: .italic .smcaps}\n \n \xEF\x9B\xA1 eiusmod tempor incididunt, ut\xC3\xAAn\xC3\xB9 labore et dolore. Tw\xC4\x83nj\xC3\xAC magna aliqua. Ut enim ad minim veniam\n {: .normal}\n@@ -9,5 +9,5 @@ quis nostrud\xC3\xAAt\xC3\xB9 exercitiation ullamco laboris m\xC3\xB9s\xC3\xB9k\xC3\xB9abc\xC3\xB9 nisi ",
- ],
+ "mu ns\n Lorem ipsum dolor sit amet, consectetur adipisicing ut\xC3\xA0y\xC3\xB9abcd\xC3\xB9efg kia elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non p\n {: .normal_pn}\n \n-Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in\n# *Lorem ipsum\xC3\xB9l\xC3\xB9l\xC3\xA0 dolor\xC3\xB9k\xC3\xB9 sit\xC3\xA8b\xC3\xA8 N\xC3\xA8 amet b\xC3\xA0d\xC3\xAC*\n+# *consectetur\xC3\xB9l\xC3\xB9l\xC3\xA0 adipisicing\xC3\xB9k\xC3\xB9 elit\xC3\xA8b\xC3\xA8 N\xC3\xA8 sed do\xC3\xA0d\xC3\xAC*{: .italic .smcaps}\n \n \xEF\x9B\xA1 eiusmod tempor incididunt, ut\xC3\xAAn\xC3\xB9 labore et dolore. Tw\xC4\x83nj\xC3\xAC magna aliqua. Ut enim ad minim veniam\n {: .normal}\n@@ -9,5 +9,5 @@ quis nostrud\xC3\xAAt\xC3\xB9 exercitiation ullamco laboris m\xC3\xB9s\xC3\xB9k\xC3\xB9abc\xC3\xB9 nisi "
+ ]
].each do |description, test_string, xpect|
it description do
expect(ext_class.encode!(test_string)).to eq(xpect)
@@ -37,18 +37,18 @@ describe Gitlab::Git::EncodingHelper do
[
"encodes valid utf8 encoded string to utf8",
"λ, λ, λ".encode("UTF-8"),
- "λ, λ, λ".encode("UTF-8"),
+ "λ, λ, λ".encode("UTF-8")
],
[
"encodes valid ASCII-8BIT encoded string to utf8",
"ascii only".encode("ASCII-8BIT"),
- "ascii only".encode("UTF-8"),
+ "ascii only".encode("UTF-8")
],
[
"encodes valid ISO-8859-1 encoded string to utf8",
"Rüby ist eine Programmiersprache. Wir verlängern den text damit ICU die Sprache erkennen kann.".encode("ISO-8859-1", "UTF-8"),
- "Rüby ist eine Programmiersprache. Wir verlängern den text damit ICU die Sprache erkennen kann.".encode("UTF-8"),
- ],
+ "Rüby ist eine Programmiersprache. Wir verlängern den text damit ICU die Sprache erkennen kann.".encode("UTF-8")
+ ]
].each do |description, test_string, xpect|
it description do
r = ext_class.encode_utf8(test_string.force_encoding('UTF-8'))
@@ -77,8 +77,8 @@ describe Gitlab::Git::EncodingHelper do
[
'removes invalid bytes from ASCII-8bit encoded multibyte string.',
"Lorem ipsum\xC3\n dolor sit amet, xy\xC3\xA0y\xC3\xB9abcd\xC3\xB9efg".force_encoding('ASCII-8BIT'),
- "Lorem ipsum\n dolor sit amet, xyàyùabcdùefg",
- ],
+ "Lorem ipsum\n dolor sit amet, xyàyùabcdùefg"
+ ]
].each do |description, test_string, xpect|
it description do
expect(ext_class.encode!(test_string)).to eq(xpect)
diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb
index 3d6d7292b42..fea186fd4f4 100644
--- a/spec/lib/gitlab/git/repository_spec.rb
+++ b/spec/lib/gitlab/git/repository_spec.rb
@@ -24,21 +24,26 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
end
- # TODO: Uncomment when feature is reenabled
- # context 'with gitaly enabled' do
- # before { stub_gitaly }
- #
- # it 'gets the branch name from GitalyClient' do
- # expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:default_branch_name)
- # repository.root_ref
- # end
- #
- # it 'wraps GRPC exceptions' do
- # expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:default_branch_name).
- # and_raise(GRPC::Unknown)
- # expect { repository.root_ref }.to raise_error(Gitlab::Git::CommandError)
- # end
- # end
+ context 'with gitaly enabled' do
+ before { stub_gitaly }
+
+ it 'gets the branch name from GitalyClient' do
+ expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:default_branch_name)
+ repository.root_ref
+ end
+
+ it 'wraps GRPC not found' do
+ expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:default_branch_name).
+ and_raise(GRPC::NotFound)
+ expect { repository.root_ref }.to raise_error(Gitlab::Git::Repository::NoRepository)
+ end
+
+ it 'wraps GRPC exceptions' do
+ expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:default_branch_name).
+ and_raise(GRPC::Unknown)
+ expect { repository.root_ref }.to raise_error(Gitlab::Git::CommandError)
+ end
+ end
end
describe "#rugged" do
@@ -113,21 +118,26 @@ describe Gitlab::Git::Repository, seed_helper: true do
it { is_expected.to include("master") }
it { is_expected.not_to include("branch-from-space") }
- # TODO: Uncomment when feature is reenabled
- # context 'with gitaly enabled' do
- # before { stub_gitaly }
- #
- # it 'gets the branch names from GitalyClient' do
- # expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:branch_names)
- # subject
- # end
- #
- # it 'wraps GRPC exceptions' do
- # expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:branch_names).
- # and_raise(GRPC::Unknown)
- # expect { subject }.to raise_error(Gitlab::Git::CommandError)
- # end
- # end
+ context 'with gitaly enabled' do
+ before { stub_gitaly }
+
+ it 'gets the branch names from GitalyClient' do
+ expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:branch_names)
+ subject
+ end
+
+ it 'wraps GRPC not found' do
+ expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:branch_names).
+ and_raise(GRPC::NotFound)
+ expect { subject }.to raise_error(Gitlab::Git::Repository::NoRepository)
+ end
+
+ it 'wraps GRPC other exceptions' do
+ expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:branch_names).
+ and_raise(GRPC::Unknown)
+ expect { subject }.to raise_error(Gitlab::Git::CommandError)
+ end
+ end
end
describe '#tag_names' do
@@ -145,21 +155,26 @@ describe Gitlab::Git::Repository, seed_helper: true do
it { is_expected.to include("v1.0.0") }
it { is_expected.not_to include("v5.0.0") }
- # TODO: Uncomment when feature is reenabled
- # context 'with gitaly enabled' do
- # before { stub_gitaly }
- #
- # it 'gets the tag names from GitalyClient' do
- # expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:tag_names)
- # subject
- # end
- #
- # it 'wraps GRPC exceptions' do
- # expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:tag_names).
- # and_raise(GRPC::Unknown)
- # expect { subject }.to raise_error(Gitlab::Git::CommandError)
- # end
- # end
+ context 'with gitaly enabled' do
+ before { stub_gitaly }
+
+ it 'gets the tag names from GitalyClient' do
+ expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:tag_names)
+ subject
+ end
+
+ it 'wraps GRPC not found' do
+ expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:tag_names).
+ and_raise(GRPC::NotFound)
+ expect { subject }.to raise_error(Gitlab::Git::Repository::NoRepository)
+ end
+
+ it 'wraps GRPC exceptions' do
+ expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:tag_names).
+ and_raise(GRPC::Unknown)
+ expect { subject }.to raise_error(Gitlab::Git::CommandError)
+ end
+ end
end
shared_examples 'archive check' do |extenstion|
@@ -1031,6 +1046,35 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
end
+ describe '#find_commits' do
+ it 'should return a return a collection of commits' do
+ commits = repository.find_commits
+
+ expect(commits).not_to be_empty
+ expect(commits).to all( be_a_kind_of(Gitlab::Git::Commit) )
+ end
+
+ context 'while applying a sort order based on the `order` option' do
+ it "allows ordering topologically (no parents shown before their children)" do
+ expect_any_instance_of(Rugged::Walker).to receive(:sorting).with(Rugged::SORT_TOPO)
+
+ repository.find_commits(order: :topo)
+ end
+
+ it "allows ordering by date" do
+ expect_any_instance_of(Rugged::Walker).to receive(:sorting).with(Rugged::SORT_DATE | Rugged::SORT_TOPO)
+
+ repository.find_commits(order: :date)
+ end
+
+ it "applies no sorting by default" do
+ expect_any_instance_of(Rugged::Walker).to receive(:sorting).with(Rugged::SORT_NONE)
+
+ repository.find_commits
+ end
+ end
+ end
+
describe '#branches with deleted branch' do
before(:each) do
ref = double()
@@ -1045,20 +1089,8 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
describe '#branch_count' do
- before(:each) do
- valid_ref = double(:ref)
- invalid_ref = double(:ref)
-
- allow(valid_ref).to receive_messages(name: 'master', target: double(:target))
-
- allow(invalid_ref).to receive_messages(name: 'bad-branch')
- allow(invalid_ref).to receive(:target) { raise Rugged::ReferenceError }
-
- allow(repository.rugged).to receive_messages(branches: [valid_ref, invalid_ref])
- end
-
it 'returns the number of branches' do
- expect(repository.branch_count).to eq(1)
+ expect(repository.branch_count).to eq(9)
end
end
diff --git a/spec/lib/gitlab/git/util_spec.rb b/spec/lib/gitlab/git/util_spec.rb
index bcca4d4c746..88c871855df 100644
--- a/spec/lib/gitlab/git/util_spec.rb
+++ b/spec/lib/gitlab/git/util_spec.rb
@@ -6,10 +6,10 @@ describe Gitlab::Git::Util do
["", 0],
["foo", 1],
["foo\n", 1],
- ["foo\n\n", 2],
+ ["foo\n\n", 2]
].each do |string, line_count|
it "counts #{line_count} lines in #{string.inspect}" do
- expect(Gitlab::Git::Util.count_lines(string)).to eq(line_count)
+ expect(described_class.count_lines(string)).to eq(line_count)
end
end
end
diff --git a/spec/lib/gitlab/git_access_spec.rb b/spec/lib/gitlab/git_access_spec.rb
index d8b72615fab..25769977f24 100644
--- a/spec/lib/gitlab/git_access_spec.rb
+++ b/spec/lib/gitlab/git_access_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe Gitlab::GitAccess, lib: true do
- let(:access) { Gitlab::GitAccess.new(actor, project, 'web', authentication_abilities: authentication_abilities) }
+ let(:access) { Gitlab::GitAccess.new(actor, project, 'ssh', authentication_abilities: authentication_abilities) }
let(:project) { create(:project, :repository) }
let(:user) { create(:user) }
let(:actor) { user }
diff --git a/spec/lib/git_ref_validator_spec.rb b/spec/lib/gitlab/git_ref_validator_spec.rb
index cc8daa535d6..cc8daa535d6 100644
--- a/spec/lib/git_ref_validator_spec.rb
+++ b/spec/lib/gitlab/git_ref_validator_spec.rb
diff --git a/spec/lib/gitlab/gitaly_client/commit_spec.rb b/spec/lib/gitlab/gitaly_client/commit_spec.rb
index 58f11ff8906..cf1bc74779e 100644
--- a/spec/lib/gitlab/gitaly_client/commit_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/commit_spec.rb
@@ -1,28 +1,24 @@
require 'spec_helper'
describe Gitlab::GitalyClient::Commit do
- describe '.diff_from_parent' do
- let(:diff_stub) { double('Gitaly::Diff::Stub') }
- let(:project) { create(:project, :repository) }
- let(:repository_message) { project.repository.gitaly_repository }
- let(:commit) { project.commit('913c66a37b4a45b9769037c55c2d238bd0942d2e') }
-
- before do
- allow(Gitaly::Diff::Stub).to receive(:new).and_return(diff_stub)
- allow(diff_stub).to receive(:commit_diff).and_return([])
- end
+ let(:diff_stub) { double('Gitaly::Diff::Stub') }
+ let(:project) { create(:project, :repository) }
+ let(:repository) { project.repository }
+ let(:repository_message) { repository.gitaly_repository }
+ let(:commit) { project.commit('913c66a37b4a45b9769037c55c2d238bd0942d2e') }
+ describe '#diff_from_parent' do
context 'when a commit has a parent' do
it 'sends an RPC request with the parent ID as left commit' do
request = Gitaly::CommitDiffRequest.new(
repository: repository_message,
left_commit_id: 'cfe32cf61b73a0d5e9f13e774abde7ff789b1660',
- right_commit_id: commit.id,
+ right_commit_id: commit.id
)
- expect(diff_stub).to receive(:commit_diff).with(request)
+ expect_any_instance_of(Gitaly::Diff::Stub).to receive(:commit_diff).with(request)
- described_class.diff_from_parent(commit)
+ described_class.new(repository).diff_from_parent(commit)
end
end
@@ -32,17 +28,17 @@ describe Gitlab::GitalyClient::Commit do
request = Gitaly::CommitDiffRequest.new(
repository: repository_message,
left_commit_id: '4b825dc642cb6eb9a060e54bf8d69288fbee4904',
- right_commit_id: initial_commit.id,
+ right_commit_id: initial_commit.id
)
- expect(diff_stub).to receive(:commit_diff).with(request)
+ expect_any_instance_of(Gitaly::Diff::Stub).to receive(:commit_diff).with(request)
- described_class.diff_from_parent(initial_commit)
+ described_class.new(repository).diff_from_parent(initial_commit)
end
end
it 'returns a Gitlab::Git::DiffCollection' do
- ret = described_class.diff_from_parent(commit)
+ ret = described_class.new(repository).diff_from_parent(commit)
expect(ret).to be_kind_of(Gitlab::Git::DiffCollection)
end
@@ -50,9 +46,40 @@ describe Gitlab::GitalyClient::Commit do
it 'passes options to Gitlab::Git::DiffCollection' do
options = { max_files: 31, max_lines: 13 }
- expect(Gitlab::Git::DiffCollection).to receive(:new).with([], options)
+ expect(Gitlab::Git::DiffCollection).to receive(:new).with(kind_of(Enumerable), options)
+
+ described_class.new(repository).diff_from_parent(commit, options)
+ end
+ end
+
+ describe '#commit_deltas' do
+ context 'when a commit has a parent' do
+ it 'sends an RPC request with the parent ID as left commit' do
+ request = Gitaly::CommitDeltaRequest.new(
+ repository: repository_message,
+ left_commit_id: 'cfe32cf61b73a0d5e9f13e774abde7ff789b1660',
+ right_commit_id: commit.id
+ )
+
+ expect_any_instance_of(Gitaly::Diff::Stub).to receive(:commit_delta).with(request).and_return([])
- described_class.diff_from_parent(commit, options)
+ described_class.new(repository).commit_deltas(commit)
+ end
+ end
+
+ context 'when a commit does not have a parent' do
+ it 'sends an RPC request with empty tree ref as left commit' do
+ initial_commit = project.commit('1a0b36b3cdad1d2ee32457c102a8c0b7056fa863')
+ request = Gitaly::CommitDeltaRequest.new(
+ repository: repository_message,
+ left_commit_id: '4b825dc642cb6eb9a060e54bf8d69288fbee4904',
+ right_commit_id: initial_commit.id
+ )
+
+ expect_any_instance_of(Gitaly::Diff::Stub).to receive(:commit_delta).with(request).and_return([])
+
+ described_class.new(repository).commit_deltas(initial_commit)
+ end
end
end
end
diff --git a/spec/lib/gitlab/gitaly_client/ref_spec.rb b/spec/lib/gitlab/gitaly_client/ref_spec.rb
index 5405eafd281..255f23e6270 100644
--- a/spec/lib/gitlab/gitaly_client/ref_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/ref_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
describe Gitlab::GitalyClient::Ref do
let(:project) { create(:empty_project) }
let(:repo_path) { project.repository.path_to_repo }
- let(:client) { Gitlab::GitalyClient::Ref.new(project.repository) }
+ let(:client) { described_class.new(project.repository) }
before do
allow(Gitlab.config.gitaly).to receive(:enabled).and_return(true)
diff --git a/spec/lib/gitlab/github_import/issue_formatter_spec.rb b/spec/lib/gitlab/github_import/issue_formatter_spec.rb
index f34d09f2c1d..a4089592cf2 100644
--- a/spec/lib/gitlab/github_import/issue_formatter_spec.rb
+++ b/spec/lib/gitlab/github_import/issue_formatter_spec.rb
@@ -43,7 +43,7 @@ describe Gitlab::GithubImport::IssueFormatter, lib: true do
description: "*Created by: octocat*\n\nI'm having a problem with this.",
state: 'opened',
author_id: project.creator_id,
- assignee_id: nil,
+ assignee_ids: [],
created_at: created_at,
updated_at: updated_at
}
@@ -64,7 +64,7 @@ describe Gitlab::GithubImport::IssueFormatter, lib: true do
description: "*Created by: octocat*\n\nI'm having a problem with this.",
state: 'closed',
author_id: project.creator_id,
- assignee_id: nil,
+ assignee_ids: [],
created_at: created_at,
updated_at: updated_at
}
@@ -77,19 +77,19 @@ describe Gitlab::GithubImport::IssueFormatter, lib: true do
let(:raw_data) { double(base_data.merge(assignee: octocat)) }
it 'returns nil as assignee_id when is not a GitLab user' do
- expect(issue.attributes.fetch(:assignee_id)).to be_nil
+ expect(issue.attributes.fetch(:assignee_ids)).to be_empty
end
it 'returns GitLab user id associated with GitHub id as assignee_id' do
gl_user = create(:omniauth_user, extern_uid: octocat.id, provider: 'github')
- expect(issue.attributes.fetch(:assignee_id)).to eq gl_user.id
+ expect(issue.attributes.fetch(:assignee_ids)).to eq [gl_user.id]
end
it 'returns GitLab user id associated with GitHub email as assignee_id' do
gl_user = create(:user, email: octocat.email)
- expect(issue.attributes.fetch(:assignee_id)).to eq gl_user.id
+ expect(issue.attributes.fetch(:assignee_ids)).to eq [gl_user.id]
end
end
diff --git a/spec/lib/gitlab/gl_repository_spec.rb b/spec/lib/gitlab/gl_repository_spec.rb
new file mode 100644
index 00000000000..ac3558ab386
--- /dev/null
+++ b/spec/lib/gitlab/gl_repository_spec.rb
@@ -0,0 +1,19 @@
+require 'spec_helper'
+
+describe ::Gitlab::GlRepository do
+ describe '.parse' do
+ set(:project) { create(:project) }
+
+ it 'parses a project gl_repository' do
+ expect(described_class.parse("project-#{project.id}")).to eq([project, false])
+ end
+
+ it 'parses a wiki gl_repository' do
+ expect(described_class.parse("wiki-#{project.id}")).to eq([project, true])
+ end
+
+ it 'throws an argument error on an invalid gl_repository' do
+ expect { described_class.parse("badformat-#{project.id}") }.to raise_error(ArgumentError)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/google_code_import/importer_spec.rb b/spec/lib/gitlab/google_code_import/importer_spec.rb
index ccaa88a5c79..622a0f513f4 100644
--- a/spec/lib/gitlab/google_code_import/importer_spec.rb
+++ b/spec/lib/gitlab/google_code_import/importer_spec.rb
@@ -49,7 +49,7 @@ describe Gitlab::GoogleCodeImport::Importer, lib: true do
expect(issue).not_to be_nil
expect(issue.iid).to eq(169)
expect(issue.author).to eq(project.creator)
- expect(issue.assignee).to eq(mapped_user)
+ expect(issue.assignees).to eq([mapped_user])
expect(issue.state).to eq("closed")
expect(issue.label_names).to include("Priority: Medium")
expect(issue.label_names).to include("Status: Fixed")
diff --git a/spec/lib/gitlab/healthchecks/db_check_spec.rb b/spec/lib/gitlab/health_checks/db_check_spec.rb
index 33c6c24449c..33c6c24449c 100644
--- a/spec/lib/gitlab/healthchecks/db_check_spec.rb
+++ b/spec/lib/gitlab/health_checks/db_check_spec.rb
diff --git a/spec/lib/gitlab/healthchecks/fs_shards_check_spec.rb b/spec/lib/gitlab/health_checks/fs_shards_check_spec.rb
index 4cd8cf313a5..45ccd3d6459 100644
--- a/spec/lib/gitlab/healthchecks/fs_shards_check_spec.rb
+++ b/spec/lib/gitlab/health_checks/fs_shards_check_spec.rb
@@ -82,9 +82,9 @@ describe Gitlab::HealthChecks::FsShardsCheck do
it { is_expected.to include(metric_class.new(:filesystem_readable, 0, shard: :default)) }
it { is_expected.to include(metric_class.new(:filesystem_writable, 0, shard: :default)) }
- it { is_expected.to include(have_attributes(name: :filesystem_access_latency, value: be > 0, labels: { shard: :default })) }
- it { is_expected.to include(have_attributes(name: :filesystem_read_latency, value: be > 0, labels: { shard: :default })) }
- it { is_expected.to include(have_attributes(name: :filesystem_write_latency, value: be > 0, labels: { shard: :default })) }
+ it { is_expected.to include(have_attributes(name: :filesystem_access_latency, value: be >= 0, labels: { shard: :default })) }
+ it { is_expected.to include(have_attributes(name: :filesystem_read_latency, value: be >= 0, labels: { shard: :default })) }
+ it { is_expected.to include(have_attributes(name: :filesystem_write_latency, value: be >= 0, labels: { shard: :default })) }
end
context 'storage points to directory that has both read and write rights' do
@@ -96,9 +96,9 @@ describe Gitlab::HealthChecks::FsShardsCheck do
it { is_expected.to include(metric_class.new(:filesystem_readable, 1, shard: :default)) }
it { is_expected.to include(metric_class.new(:filesystem_writable, 1, shard: :default)) }
- it { is_expected.to include(have_attributes(name: :filesystem_access_latency, value: be > 0, labels: { shard: :default })) }
- it { is_expected.to include(have_attributes(name: :filesystem_read_latency, value: be > 0, labels: { shard: :default })) }
- it { is_expected.to include(have_attributes(name: :filesystem_write_latency, value: be > 0, labels: { shard: :default })) }
+ it { is_expected.to include(have_attributes(name: :filesystem_access_latency, value: be >= 0, labels: { shard: :default })) }
+ it { is_expected.to include(have_attributes(name: :filesystem_read_latency, value: be >= 0, labels: { shard: :default })) }
+ it { is_expected.to include(have_attributes(name: :filesystem_write_latency, value: be >= 0, labels: { shard: :default })) }
end
end
end
diff --git a/spec/lib/gitlab/healthchecks/redis_check_spec.rb b/spec/lib/gitlab/health_checks/redis_check_spec.rb
index 734cdcb893e..734cdcb893e 100644
--- a/spec/lib/gitlab/healthchecks/redis_check_spec.rb
+++ b/spec/lib/gitlab/health_checks/redis_check_spec.rb
diff --git a/spec/lib/gitlab/healthchecks/simple_check_shared.rb b/spec/lib/gitlab/health_checks/simple_check_shared.rb
index 1fa6d0faef9..3f871d66034 100644
--- a/spec/lib/gitlab/healthchecks/simple_check_shared.rb
+++ b/spec/lib/gitlab/health_checks/simple_check_shared.rb
@@ -8,7 +8,7 @@ shared_context 'simple_check' do |metrics_prefix, check_name, success_result|
it { is_expected.to include(have_attributes(name: "#{metrics_prefix}_success", value: 1)) }
it { is_expected.to include(have_attributes(name: "#{metrics_prefix}_timeout", value: 0)) }
- it { is_expected.to include(have_attributes(name: "#{metrics_prefix}_latency", value: be > 0)) }
+ it { is_expected.to include(have_attributes(name: "#{metrics_prefix}_latency", value: be >= 0)) }
end
context 'Check is misbehaving' do
@@ -18,7 +18,7 @@ shared_context 'simple_check' do |metrics_prefix, check_name, success_result|
it { is_expected.to include(have_attributes(name: "#{metrics_prefix}_success", value: 0)) }
it { is_expected.to include(have_attributes(name: "#{metrics_prefix}_timeout", value: 0)) }
- it { is_expected.to include(have_attributes(name: "#{metrics_prefix}_latency", value: be > 0)) }
+ it { is_expected.to include(have_attributes(name: "#{metrics_prefix}_latency", value: be >= 0)) }
end
context 'Check is timeouting' do
@@ -28,7 +28,7 @@ shared_context 'simple_check' do |metrics_prefix, check_name, success_result|
it { is_expected.to include(have_attributes(name: "#{metrics_prefix}_success", value: 0)) }
it { is_expected.to include(have_attributes(name: "#{metrics_prefix}_timeout", value: 1)) }
- it { is_expected.to include(have_attributes(name: "#{metrics_prefix}_latency", value: be > 0)) }
+ it { is_expected.to include(have_attributes(name: "#{metrics_prefix}_latency", value: be >= 0)) }
end
end
diff --git a/spec/lib/gitlab/highlight_spec.rb b/spec/lib/gitlab/highlight_spec.rb
index e49799ad105..e57b3053871 100644
--- a/spec/lib/gitlab/highlight_spec.rb
+++ b/spec/lib/gitlab/highlight_spec.rb
@@ -57,4 +57,15 @@ describe Gitlab::Highlight, lib: true do
end
end
end
+
+ describe '#highlight' do
+ subject { described_class.highlight(file_name, file_content, nowrap: false) }
+
+ it 'links dependencies via DependencyLinker' do
+ expect(Gitlab::DependencyLinker).to receive(:link).
+ with('file.name', 'Contents', anything).and_call_original
+
+ described_class.highlight('file.name', 'Contents')
+ end
+ end
end
diff --git a/spec/lib/gitlab/i18n_spec.rb b/spec/lib/gitlab/i18n_spec.rb
new file mode 100644
index 00000000000..52f2614d5ca
--- /dev/null
+++ b/spec/lib/gitlab/i18n_spec.rb
@@ -0,0 +1,27 @@
+require 'spec_helper'
+
+module Gitlab
+ describe I18n, lib: true do
+ let(:user) { create(:user, preferred_language: 'es') }
+
+ describe '.set_locale' do
+ it 'sets the locale based on current user preferred language' do
+ Gitlab::I18n.set_locale(user)
+
+ expect(FastGettext.locale).to eq('es')
+ expect(::I18n.locale).to eq(:es)
+ end
+ end
+
+ describe '.reset_locale' do
+ it 'resets the locale to the default language' do
+ Gitlab::I18n.set_locale(user)
+
+ Gitlab::I18n.reset_locale
+
+ expect(FastGettext.locale).to eq('en')
+ expect(::I18n.locale).to eq(:en)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index 0abf89d060c..34f617e23a5 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -3,12 +3,13 @@ issues:
- subscriptions
- award_emoji
- author
-- assignee
+- assignees
- updated_by
- milestone
- notes
- label_links
- labels
+- last_edited_by
- todos
- user_agent_detail
- moved_to
@@ -16,6 +17,7 @@ issues:
- merge_requests_closing_issues
- metrics
- timelogs
+- issue_assignees
events:
- author
- project
@@ -26,6 +28,7 @@ notes:
- noteable
- author
- updated_by
+- last_edited_by
- resolved_by
- todos
- events
@@ -71,6 +74,7 @@ merge_requests:
- notes
- label_links
- labels
+- last_edited_by
- todos
- target_project
- source_project
@@ -81,6 +85,7 @@ merge_requests:
- merge_requests_closing_issues
- metrics
- timelogs
+- head_pipeline
merge_request_diff:
- merge_request
pipelines:
@@ -97,6 +102,8 @@ pipelines:
- cancelable_statuses
- manual_actions
- artifacts
+- pipeline_schedule
+- merge_requests
statuses:
- project
- pipeline
@@ -108,9 +115,13 @@ triggers:
- project
- trigger_requests
- owner
-- trigger_schedule
-trigger_schedule:
-- trigger
+pipeline_schedules:
+- project
+- owner
+- pipelines
+- last_pipeline
+pipeline_schedule:
+- pipelines
deploy_keys:
- user
- deploy_keys_projects
@@ -217,7 +228,7 @@ project:
- active_runners
- variables
- triggers
-- trigger_schedules
+- pipeline_schedules
- environments
- deployments
- project_feature
@@ -225,6 +236,7 @@ project:
- authorized_users
- project_authorizations
- route
+- redirect_routes
- statistics
- container_repositories
- uploads
diff --git a/spec/lib/gitlab/import_export/fork_spec.rb b/spec/lib/gitlab/import_export/fork_spec.rb
index c5ce06afd73..42f3fc59f04 100644
--- a/spec/lib/gitlab/import_export/fork_spec.rb
+++ b/spec/lib/gitlab/import_export/fork_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
describe 'forked project import', services: true do
let(:user) { create(:user) }
let!(:project_with_repo) { create(:project, :test_repo, name: 'test-repo-restorer', path: 'test-repo-restorer') }
- let!(:project) { create(:empty_project) }
+ let!(:project) { create(:empty_project, name: 'test-repo-restorer-no-repo', path: 'test-repo-restorer-no-repo') }
let(:export_path) { "#{Dir.tmpdir}/project_tree_saver_spec" }
let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: project.path_with_namespace) }
let(:forked_from_project) { create(:project) }
diff --git a/spec/lib/gitlab/import_export/project.json b/spec/lib/gitlab/import_export/project.json
index 7a0b0b06d4b..fdbb6a0556d 100644
--- a/spec/lib/gitlab/import_export/project.json
+++ b/spec/lib/gitlab/import_export/project.json
@@ -2,6 +2,7 @@
"description": "Nisi et repellendus ut enim quo accusamus vel magnam.",
"visibility_level": 10,
"archived": false,
+ "description_html": "description",
"labels": [
{
"id": 2,
@@ -6981,28 +6982,6 @@
],
"services": [
{
- "id": 164,
- "title": null,
- "project_id": 5,
- "created_at": "2016-06-14T15:02:07.372Z",
- "updated_at": "2016-06-14T15:02:07.372Z",
- "active": false,
- "properties": {
-
- },
- "template": false,
- "push_events": true,
- "issues_events": true,
- "merge_requests_events": true,
- "tag_push_events": true,
- "note_events": true,
- "build_events": true,
- "category": "issue_tracker",
- "type": "CustomIssueTrackerService",
- "default": true,
- "wiki_page_events": true
- },
- {
"id": 100,
"title": "JetBrains TeamCity CI",
"project_id": 5,
@@ -7019,6 +6998,7 @@
"tag_push_events": true,
"note_events": true,
"build_events": true,
+ "type": "TeamcityService",
"category": "ci",
"default": false,
"wiki_page_events": true
@@ -7040,6 +7020,7 @@
"tag_push_events": true,
"note_events": true,
"pipeline_events": true,
+ "type": "SlackService",
"category": "common",
"default": false,
"wiki_page_events": true
@@ -7061,6 +7042,7 @@
"tag_push_events": true,
"note_events": true,
"build_events": true,
+ "type": "RedmineService",
"category": "issue_tracker",
"default": false,
"wiki_page_events": true
@@ -7082,6 +7064,7 @@
"tag_push_events": true,
"note_events": true,
"build_events": true,
+ "type": "PushoverService",
"category": "common",
"default": false,
"wiki_page_events": true
@@ -7103,6 +7086,7 @@
"tag_push_events": true,
"note_events": true,
"build_events": true,
+ "type": "PivotalTrackerService",
"category": "common",
"default": false,
"wiki_page_events": true
@@ -7125,6 +7109,7 @@
"tag_push_events": true,
"note_events": true,
"build_events": true,
+ "type": "JiraService",
"category": "issue_tracker",
"default": false,
"wiki_page_events": true
@@ -7146,6 +7131,7 @@
"tag_push_events": true,
"note_events": true,
"build_events": true,
+ "type": "IrkerService",
"category": "common",
"default": false,
"wiki_page_events": true
@@ -7167,6 +7153,7 @@
"tag_push_events": true,
"note_events": true,
"pipeline_events": true,
+ "type": "HipchatService",
"category": "common",
"default": false,
"wiki_page_events": true
@@ -7188,6 +7175,7 @@
"tag_push_events": true,
"note_events": true,
"build_events": true,
+ "type": "GemnasiumService",
"category": "common",
"default": false,
"wiki_page_events": true
@@ -7209,6 +7197,7 @@
"tag_push_events": true,
"note_events": true,
"build_events": true,
+ "type": "FlowdockService",
"category": "common",
"default": false,
"wiki_page_events": true
@@ -7230,6 +7219,7 @@
"tag_push_events": true,
"note_events": true,
"build_events": true,
+ "type": "ExternalWikiService",
"category": "common",
"default": false,
"wiki_page_events": true
@@ -7251,6 +7241,7 @@
"tag_push_events": true,
"note_events": true,
"build_events": true,
+ "type": "EmailsOnPushService",
"category": "common",
"default": false,
"wiki_page_events": true
@@ -7272,6 +7263,7 @@
"tag_push_events": true,
"note_events": true,
"build_events": true,
+ "type": "DroneCiService",
"category": "ci",
"default": false,
"wiki_page_events": true
@@ -7293,6 +7285,7 @@
"tag_push_events": true,
"note_events": true,
"build_events": true,
+ "type": "CustomIssueTrackerService",
"category": "issue_tracker",
"default": false,
"wiki_page_events": true
@@ -7314,6 +7307,7 @@
"tag_push_events": true,
"note_events": true,
"build_events": true,
+ "type": "CampfireService",
"category": "common",
"default": false,
"wiki_page_events": true
@@ -7335,6 +7329,7 @@
"tag_push_events": true,
"note_events": true,
"build_events": true,
+ "type": "BuildkiteService",
"category": "ci",
"default": false,
"wiki_page_events": true
@@ -7356,6 +7351,7 @@
"tag_push_events": true,
"note_events": true,
"build_events": true,
+ "type": "BambooService",
"category": "ci",
"default": false,
"wiki_page_events": true
@@ -7377,6 +7373,7 @@
"tag_push_events": true,
"note_events": true,
"build_events": true,
+ "type": "AssemblaService",
"category": "common",
"default": false,
"wiki_page_events": true
@@ -7398,6 +7395,7 @@
"tag_push_events": true,
"note_events": true,
"build_events": true,
+ "type": "AssemblaService",
"category": "common",
"default": false,
"wiki_page_events": true
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 0e9607c5bd3..14338515892 100644
--- a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
@@ -30,6 +30,10 @@ describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do
expect(project.project_feature.merge_requests_access_level).to eq(ProjectFeature::ENABLED)
end
+ it 'has the project html description' do
+ expect(Project.find_by_path('project').description_html).to eq('description')
+ end
+
it 'has the same label associated to two issues' do
expect(ProjectLabel.find_by_title('test2').issues.count).to eq(2)
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 d2d89e3b019..5aeb29b7fec 100644
--- a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb
+++ b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb
@@ -6,7 +6,7 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do
let(:project_tree_saver) { described_class.new(project: project, current_user: user, shared: shared) }
let(:export_path) { "#{Dir.tmpdir}/project_tree_saver_spec" }
let(:user) { create(:user) }
- let(:project) { setup_project }
+ let!(:project) { setup_project }
before do
project.team << [user, :master]
@@ -189,11 +189,21 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do
end
end
end
+
+ context 'project attributes' do
+ it 'contains the html description' do
+ expect(saved_project_json).to include("description_html" => 'description')
+ end
+
+ it 'does not contain the runners token' do
+ expect(saved_project_json).not_to include("runners_token" => 'token')
+ end
+ end
end
end
def setup_project
- issue = create(:issue, assignee: user)
+ issue = create(:issue, assignees: [user])
snippet = create(:project_snippet)
release = create(:release)
group = create(:group)
@@ -209,6 +219,7 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do
releases: [release],
group: group
)
+ project.update_column(:description_html, 'description')
project_label = create(:label, project: project)
group_label = create(:group_label, group: group)
create(:label_link, label: project_label, target: issue)
diff --git a/spec/lib/gitlab/import_export/reader_spec.rb b/spec/lib/gitlab/import_export/reader_spec.rb
index 48d74b07e27..d700af142be 100644
--- a/spec/lib/gitlab/import_export/reader_spec.rb
+++ b/spec/lib/gitlab/import_export/reader_spec.rb
@@ -5,7 +5,7 @@ describe Gitlab::ImportExport::Reader, lib: true do
let(:test_config) { 'spec/support/import_export/import_export.yml' }
let(:project_tree_hash) do
{
- only: [:name, :path],
+ except: [:id, :created_at],
include: [:issues, :labels,
{ merge_requests: {
only: [:id],
diff --git a/spec/lib/gitlab/import_export/relation_factory_spec.rb b/spec/lib/gitlab/import_export/relation_factory_spec.rb
index fcc23a75ca1..744fed44925 100644
--- a/spec/lib/gitlab/import_export/relation_factory_spec.rb
+++ b/spec/lib/gitlab/import_export/relation_factory_spec.rb
@@ -60,7 +60,7 @@ describe Gitlab::ImportExport::RelationFactory, lib: true do
end
context 'original service exists' do
- let(:service_id) { Service.create(project: project).id }
+ let(:service_id) { create(:service, project: project).id }
it 'does not have the original service_id' do
expect(created_object.service_id).not_to eq(service_id)
@@ -95,7 +95,7 @@ describe Gitlab::ImportExport::RelationFactory, lib: true do
'random_id' => 99,
'milestone_id' => 99,
'project_id' => 99,
- 'user_id' => 99,
+ 'user_id' => 99
}
end
diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml
index 0372e3f7dbf..2b95f76e045 100644
--- a/spec/lib/gitlab/import_export/safe_model_attributes.yml
+++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml
@@ -23,6 +23,8 @@ Issue:
- weight
- time_estimate
- relative_position
+- last_edited_at
+- last_edited_by_id
Event:
- id
- target_type
@@ -154,6 +156,9 @@ MergeRequest:
- approvals_before_merge
- rebase_commit_sha
- time_estimate
+- last_edited_at
+- last_edited_by_id
+- head_pipeline_id
MergeRequestDiff:
- id
- state
@@ -184,6 +189,7 @@ Ci::Pipeline:
- user_id
- lock_version
- auto_canceled_by_id
+- pipeline_schedule_id
CommitStatus:
- id
- project_id
@@ -225,6 +231,7 @@ CommitStatus:
- lock_version
- coverage_regex
- auto_canceled_by_id
+- retried
Ci::Variable:
- id
- project_id
@@ -243,18 +250,19 @@ Ci::Trigger:
- owner_id
- description
- ref
-Ci::TriggerSchedule:
+Ci::PipelineSchedule:
- id
-- project_id
-- trigger_id
-- deleted_at
-- created_at
-- updated_at
+- description
+- ref
- cron
- cron_timezone
- next_run_at
-- ref
+- project_id
+- owner_id
- active
+- deleted_at
+- created_at
+- updated_at
DeployKey:
- id
- user_id
@@ -309,6 +317,7 @@ ProjectHook:
- token
- group_id
- confidential_issues_events
+- repository_update_events
ProtectedBranch:
- id
- project_id
@@ -329,6 +338,29 @@ Project:
- snippets_enabled
- visibility_level
- archived
+- created_at
+- updated_at
+- last_activity_at
+- star_count
+- ci_id
+- shared_runners_enabled
+- build_coverage_regex
+- build_allow_git_fetchs
+- build_timeout
+- pending_delete
+- public_builds
+- last_repository_check_failed
+- last_repository_check_at
+- container_registry_enabled
+- only_allow_merge_if_pipeline_succeeds
+- has_external_issue_tracker
+- request_access_enabled
+- has_external_wiki
+- only_allow_merge_if_all_discussions_are_resolved
+- auto_cancel_pending_pipelines
+- printing_merge_request_link_enabled
+- build_allow_git_fetch
+- last_repository_updated_at
Author:
- name
ProjectFeature:
diff --git a/spec/lib/gitlab/import_export/wiki_repo_bundler_spec.rb b/spec/lib/gitlab/import_export/wiki_repo_saver_spec.rb
index 071e5fac3f0..071e5fac3f0 100644
--- a/spec/lib/gitlab/import_export/wiki_repo_bundler_spec.rb
+++ b/spec/lib/gitlab/import_export/wiki_repo_saver_spec.rb
diff --git a/spec/lib/gitlab/issuable_sorter_spec.rb b/spec/lib/gitlab/issuable_sorter_spec.rb
new file mode 100644
index 00000000000..c9a434b2bcf
--- /dev/null
+++ b/spec/lib/gitlab/issuable_sorter_spec.rb
@@ -0,0 +1,62 @@
+require 'spec_helper'
+
+describe Gitlab::IssuableSorter, lib: true do
+ let(:namespace1) { build(:namespace, id: 1) }
+ let(:project1) { build(:project, id: 1, namespace: namespace1) }
+
+ let(:project2) { build(:project, id: 2, path: "a", namespace: project1.namespace) }
+ let(:project3) { build(:project, id: 3, path: "b", namespace: project1.namespace) }
+
+ let(:namespace2) { build(:namespace, id: 2, path: "a") }
+ let(:namespace3) { build(:namespace, id: 3, path: "b") }
+ let(:project4) { build(:project, id: 4, path: "a", namespace: namespace2) }
+ let(:project5) { build(:project, id: 5, path: "b", namespace: namespace2) }
+ let(:project6) { build(:project, id: 6, path: "a", namespace: namespace3) }
+
+ let(:unsorted) { [sorted[2], sorted[3], sorted[0], sorted[1]] }
+
+ let(:sorted) do
+ [build(:issue, iid: 1, project: project1),
+ build(:issue, iid: 2, project: project1),
+ build(:issue, iid: 10, project: project1),
+ build(:issue, iid: 20, project: project1)]
+ end
+
+ it 'sorts references by a given key' do
+ expect(described_class.sort(project1, unsorted)).to eq(sorted)
+ end
+
+ context 'for JIRA issues' do
+ let(:sorted) do
+ [ExternalIssue.new('JIRA-1', project1),
+ ExternalIssue.new('JIRA-2', project1),
+ ExternalIssue.new('JIRA-10', project1),
+ ExternalIssue.new('JIRA-20', project1)]
+ end
+
+ it 'sorts references by a given key' do
+ expect(described_class.sort(project1, unsorted)).to eq(sorted)
+ end
+ end
+
+ context 'for references from multiple projects and namespaces' do
+ let(:sorted) do
+ [build(:issue, iid: 1, project: project1),
+ build(:issue, iid: 2, project: project1),
+ build(:issue, iid: 10, project: project1),
+ build(:issue, iid: 1, project: project2),
+ build(:issue, iid: 1, project: project3),
+ build(:issue, iid: 1, project: project4),
+ build(:issue, iid: 1, project: project5),
+ build(:issue, iid: 1, project: project6)]
+ end
+ let(:unsorted) do
+ [sorted[3], sorted[1], sorted[4], sorted[2],
+ sorted[6], sorted[5], sorted[0], sorted[7]]
+ end
+
+ it 'sorts references by project and then by a given key' do
+ expect(subject.sort(project1, unsorted)).to eq(sorted)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ldap/person_spec.rb b/spec/lib/gitlab/ldap/person_spec.rb
index 9a556cde5d5..087c4d8c92c 100644
--- a/spec/lib/gitlab/ldap/person_spec.rb
+++ b/spec/lib/gitlab/ldap/person_spec.rb
@@ -20,7 +20,7 @@ describe Gitlab::LDAP::Person do
it 'uses the configured name attribute and handles values as an array' do
name = 'John Doe'
entry['cn'] = [name]
- person = Gitlab::LDAP::Person.new(entry, 'ldapmain')
+ person = described_class.new(entry, 'ldapmain')
expect(person.name).to eq(name)
end
@@ -30,7 +30,7 @@ describe Gitlab::LDAP::Person do
it 'returns the value of mail, if present' do
mail = 'john@example.com'
entry['mail'] = mail
- person = Gitlab::LDAP::Person.new(entry, 'ldapmain')
+ person = described_class.new(entry, 'ldapmain')
expect(person.email).to eq([mail])
end
@@ -38,7 +38,7 @@ describe Gitlab::LDAP::Person do
it 'returns the value of userPrincipalName, if mail and email are not present' do
user_principal_name = 'john.doe@example.com'
entry['userPrincipalName'] = user_principal_name
- person = Gitlab::LDAP::Person.new(entry, 'ldapmain')
+ person = described_class.new(entry, 'ldapmain')
expect(person.email).to eq([user_principal_name])
end
diff --git a/spec/lib/gitlab/ldap/user_spec.rb b/spec/lib/gitlab/ldap/user_spec.rb
index 346cf0d117c..f4aab429931 100644
--- a/spec/lib/gitlab/ldap/user_spec.rb
+++ b/spec/lib/gitlab/ldap/user_spec.rb
@@ -108,6 +108,31 @@ describe Gitlab::LDAP::User, lib: true do
it "creates a new user if not found" do
expect{ ldap_user.save }.to change{ User.count }.by(1)
end
+
+ context 'when signup is disabled' do
+ before do
+ stub_application_setting signup_enabled: false
+ end
+
+ it 'creates the user' do
+ ldap_user.save
+
+ expect(gl_user).to be_persisted
+ end
+ end
+
+ context 'when user confirmation email is enabled' do
+ before do
+ stub_application_setting send_user_confirmation_email: true
+ end
+
+ it 'creates and confirms the user anyway' do
+ ldap_user.save
+
+ expect(gl_user).to be_persisted
+ expect(gl_user).to be_confirmed
+ end
+ end
end
describe 'updating email' do
diff --git a/spec/lib/gitlab/metrics_spec.rb b/spec/lib/gitlab/metrics_spec.rb
index ab6e311b1e8..208a8d028cd 100644
--- a/spec/lib/gitlab/metrics_spec.rb
+++ b/spec/lib/gitlab/metrics_spec.rb
@@ -20,7 +20,7 @@ describe Gitlab::Metrics do
expect(pool).to receive(:with).and_yield(connection)
expect(connection).to receive(:write_points).with(an_instance_of(Array))
- expect(Gitlab::Metrics).to receive(:pool).and_return(pool)
+ expect(described_class).to receive(:pool).and_return(pool)
described_class.submit_metrics([{ 'series' => 'kittens', 'tags' => {} }])
end
@@ -64,7 +64,7 @@ describe Gitlab::Metrics do
describe '.measure' do
context 'without a transaction' do
it 'returns the return value of the block' do
- val = Gitlab::Metrics.measure(:foo) { 10 }
+ val = described_class.measure(:foo) { 10 }
expect(val).to eq(10)
end
@@ -74,7 +74,7 @@ describe Gitlab::Metrics do
let(:transaction) { Gitlab::Metrics::Transaction.new }
before do
- allow(Gitlab::Metrics).to receive(:current_transaction).
+ allow(described_class).to receive(:current_transaction).
and_return(transaction)
end
@@ -88,11 +88,11 @@ describe Gitlab::Metrics do
expect(transaction).to receive(:increment).
with('foo_call_count', 1)
- Gitlab::Metrics.measure(:foo) { 10 }
+ described_class.measure(:foo) { 10 }
end
it 'returns the return value of the block' do
- val = Gitlab::Metrics.measure(:foo) { 10 }
+ val = described_class.measure(:foo) { 10 }
expect(val).to eq(10)
end
@@ -105,7 +105,7 @@ describe Gitlab::Metrics do
expect_any_instance_of(Gitlab::Metrics::Transaction).
not_to receive(:add_tag)
- Gitlab::Metrics.tag_transaction(:foo, 'bar')
+ described_class.tag_transaction(:foo, 'bar')
end
end
@@ -113,13 +113,13 @@ describe Gitlab::Metrics do
let(:transaction) { Gitlab::Metrics::Transaction.new }
it 'adds the tag to the transaction' do
- expect(Gitlab::Metrics).to receive(:current_transaction).
+ expect(described_class).to receive(:current_transaction).
and_return(transaction)
expect(transaction).to receive(:add_tag).
with(:foo, 'bar')
- Gitlab::Metrics.tag_transaction(:foo, 'bar')
+ described_class.tag_transaction(:foo, 'bar')
end
end
end
@@ -130,7 +130,7 @@ describe Gitlab::Metrics do
expect_any_instance_of(Gitlab::Metrics::Transaction).
not_to receive(:action=)
- Gitlab::Metrics.action = 'foo'
+ described_class.action = 'foo'
end
end
@@ -138,12 +138,12 @@ describe Gitlab::Metrics do
it 'sets the action of a transaction' do
trans = Gitlab::Metrics::Transaction.new
- expect(Gitlab::Metrics).to receive(:current_transaction).
+ expect(described_class).to receive(:current_transaction).
and_return(trans)
expect(trans).to receive(:action=).with('foo')
- Gitlab::Metrics.action = 'foo'
+ described_class.action = 'foo'
end
end
end
@@ -160,7 +160,7 @@ describe Gitlab::Metrics do
expect_any_instance_of(Gitlab::Metrics::Transaction).
not_to receive(:add_event)
- Gitlab::Metrics.add_event(:meow)
+ described_class.add_event(:meow)
end
end
@@ -170,10 +170,10 @@ describe Gitlab::Metrics do
expect(transaction).to receive(:add_event).with(:meow)
- expect(Gitlab::Metrics).to receive(:current_transaction).
+ expect(described_class).to receive(:current_transaction).
and_return(transaction)
- Gitlab::Metrics.add_event(:meow)
+ described_class.add_event(:meow)
end
end
end
diff --git a/spec/lib/gitlab/o_auth/user_spec.rb b/spec/lib/gitlab/o_auth/user_spec.rb
index 8f09266c3b3..828c953197d 100644
--- a/spec/lib/gitlab/o_auth/user_spec.rb
+++ b/spec/lib/gitlab/o_auth/user_spec.rb
@@ -40,6 +40,35 @@ describe Gitlab::OAuth::User, lib: true do
let(:provider) { 'twitter' }
describe 'signup' do
+ context 'when signup is disabled' do
+ before do
+ stub_application_setting signup_enabled: false
+ end
+
+ it 'creates the user' do
+ stub_omniauth_config(allow_single_sign_on: ['twitter'])
+
+ oauth_user.save
+
+ expect(gl_user).to be_persisted
+ end
+ end
+
+ context 'when user confirmation email is enabled' do
+ before do
+ stub_application_setting send_user_confirmation_email: true
+ end
+
+ it 'creates and confirms the user anyway' do
+ stub_omniauth_config(allow_single_sign_on: ['twitter'])
+
+ oauth_user.save
+
+ expect(gl_user).to be_persisted
+ expect(gl_user).to be_confirmed
+ end
+ end
+
it 'marks user as having password_automatically_set' do
stub_omniauth_config(allow_single_sign_on: ['twitter'], external_providers: ['twitter'])
diff --git a/spec/lib/gitlab/other_markup.rb b/spec/lib/gitlab/other_markup_spec.rb
index 8f5a353b381..c0f5fa9dc1f 100644
--- a/spec/lib/gitlab/other_markup.rb
+++ b/spec/lib/gitlab/other_markup_spec.rb
@@ -1,17 +1,19 @@
require 'spec_helper'
describe Gitlab::OtherMarkup, lib: true do
+ let(:context) { {} }
+
context "XSS Checks" do
links = {
'links' => {
file: 'file.rdoc',
input: 'XSS[JaVaScriPt:alert(1)]',
- output: '<p><a>XSS</a></p>'
+ output: "\n" + '<p><a>XSS</a></p>' + "\n"
}
}
links.each do |name, data|
it "does not convert dangerous #{name} into HTML" do
- expect(render(data[:file], data[:input], context)).to eql data[:output]
+ expect(render(data[:file], data[:input], context)).to eq(data[:output])
end
end
end
diff --git a/spec/lib/gitlab/project_search_results_spec.rb b/spec/lib/gitlab/project_search_results_spec.rb
index e0ebea63eb4..1b8690ba613 100644
--- a/spec/lib/gitlab/project_search_results_spec.rb
+++ b/spec/lib/gitlab/project_search_results_spec.rb
@@ -22,11 +22,40 @@ describe Gitlab::ProjectSearchResults, lib: true do
end
describe 'blob search' do
- let(:project) { create(:project, :repository) }
- let(:results) { described_class.new(user, project, 'files').objects('blobs') }
+ let(:project) { create(:project, :public, :repository) }
+
+ subject(:results) { described_class.new(user, project, 'files').objects('blobs') }
+
+ context 'when repository is disabled' do
+ let(:project) { create(:project, :public, :repository, :repository_disabled) }
+
+ it 'hides blobs from members' do
+ project.add_reporter(user)
+
+ is_expected.to be_empty
+ end
+
+ it 'hides blobs from non-members' do
+ is_expected.to be_empty
+ end
+ end
+
+ context 'when repository is internal' do
+ let(:project) { create(:project, :public, :repository, :repository_private) }
+
+ it 'finds blobs for members' do
+ project.add_reporter(user)
+
+ is_expected.not_to be_empty
+ end
+
+ it 'hides blobs from non-members' do
+ is_expected.to be_empty
+ end
+ end
it 'finds by name' do
- expect(results).to include(["files/images/wm.svg", nil])
+ expect(results.map(&:first)).to include('files/images/wm.svg')
end
it 'finds by content' do
@@ -70,6 +99,46 @@ describe Gitlab::ProjectSearchResults, lib: true do
end
end
+ describe 'wiki search' do
+ let(:project) { create(:project, :public) }
+ let(:wiki) { build(:project_wiki, project: project) }
+ let!(:wiki_page) { wiki.create_page('Title', 'Content') }
+
+ subject(:results) { described_class.new(user, project, 'Content').objects('wiki_blobs') }
+
+ context 'when wiki is disabled' do
+ let(:project) { create(:project, :public, :wiki_disabled) }
+
+ it 'hides wiki blobs from members' do
+ project.add_reporter(user)
+
+ is_expected.to be_empty
+ end
+
+ it 'hides wiki blobs from non-members' do
+ is_expected.to be_empty
+ end
+ end
+
+ context 'when wiki is internal' do
+ let(:project) { create(:project, :public, :wiki_private) }
+
+ it 'finds wiki blobs for members' do
+ project.add_reporter(user)
+
+ is_expected.not_to be_empty
+ end
+
+ it 'hides wiki blobs from non-members' do
+ is_expected.to be_empty
+ end
+ end
+
+ it 'finds by content' do
+ expect(results).to include("master:Title.md:1:Content\n")
+ end
+ end
+
it 'does not list issues on private projects' do
issue = create(:issue, project: project)
@@ -79,7 +148,6 @@ describe Gitlab::ProjectSearchResults, lib: true do
end
describe 'confidential issues' do
- let(:project) { create(:empty_project) }
let(:query) { 'issue' }
let(:author) { create(:user) }
let(:assignee) { create(:user) }
@@ -89,7 +157,7 @@ describe Gitlab::ProjectSearchResults, lib: true do
let(:project) { create(:empty_project, :internal) }
let!(:issue) { create(:issue, project: project, title: 'Issue 1') }
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) }
+ let!(:security_issue_2) { create(:issue, :confidential, title: 'Security issue 2', project: project, assignees: [assignee]) }
it 'does not list project confidential issues for non project members' do
results = described_class.new(non_member, project, query)
@@ -277,6 +345,7 @@ describe Gitlab::ProjectSearchResults, lib: true do
context 'by commit hash' do
let(:project) { create(:project, :public, :repository) }
let(:commit) { project.repository.commit('0b4bc9a') }
+
commit_hashes = { short: '0b4bc9a', full: '0b4bc9a49b562e85de7cc9e834518ea6828729b9' }
commit_hashes.each do |type, commit_hash|
diff --git a/spec/lib/gitlab/prometheus/queries/deployment_query_spec.rb b/spec/lib/gitlab/prometheus/queries/deployment_query_spec.rb
new file mode 100644
index 00000000000..cc7d7e57f06
--- /dev/null
+++ b/spec/lib/gitlab/prometheus/queries/deployment_query_spec.rb
@@ -0,0 +1,36 @@
+require 'spec_helper'
+
+describe Gitlab::Prometheus::Queries::DeploymentQuery, lib: true do
+ let(:environment) { create(:environment, slug: 'environment-slug') }
+ let(:deployment) { create(:deployment, environment: environment) }
+
+ let(:client) { double('prometheus_client') }
+ subject { described_class.new(client) }
+
+ around do |example|
+ Timecop.freeze { example.run }
+ end
+
+ it 'sends appropriate queries to prometheus' do
+ start_time_matcher = be_within(0.5).of((deployment.created_at - 30.minutes).to_f)
+ stop_time_matcher = be_within(0.5).of((deployment.created_at + 30.minutes).to_f)
+ created_at_matcher = be_within(0.5).of(deployment.created_at.to_f)
+
+ expect(client).to receive(:query_range).with('avg(container_memory_usage_bytes{container_name!="POD",environment="environment-slug"}) / 2^20',
+ start: start_time_matcher, stop: stop_time_matcher)
+ expect(client).to receive(:query).with('avg(avg_over_time(container_memory_usage_bytes{container_name!="POD",environment="environment-slug"}[30m]))',
+ time: created_at_matcher)
+ expect(client).to receive(:query).with('avg(avg_over_time(container_memory_usage_bytes{container_name!="POD",environment="environment-slug"}[30m]))',
+ time: stop_time_matcher)
+
+ expect(client).to receive(:query_range).with('avg(rate(container_cpu_usage_seconds_total{container_name!="POD",environment="environment-slug"}[2m])) * 100',
+ start: start_time_matcher, stop: stop_time_matcher)
+ expect(client).to receive(:query).with('avg(rate(container_cpu_usage_seconds_total{container_name!="POD",environment="environment-slug"}[30m])) * 100',
+ time: created_at_matcher)
+ expect(client).to receive(:query).with('avg(rate(container_cpu_usage_seconds_total{container_name!="POD",environment="environment-slug"}[30m])) * 100',
+ time: stop_time_matcher)
+
+ expect(subject.query(deployment.id)).to eq(memory_values: nil, memory_before: nil, memory_after: nil,
+ cpu_values: nil, cpu_before: nil, cpu_after: nil)
+ end
+end
diff --git a/spec/lib/gitlab/prometheus_spec.rb b/spec/lib/gitlab/prometheus_client_spec.rb
index 280264188e2..2d8bd2f6b97 100644
--- a/spec/lib/gitlab/prometheus_spec.rb
+++ b/spec/lib/gitlab/prometheus_client_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Gitlab::Prometheus, lib: true do
+describe Gitlab::PrometheusClient, lib: true do
include PrometheusHelpers
subject { described_class.new(api_url: 'https://prometheus.example.com') }
@@ -49,9 +49,43 @@ describe Gitlab::Prometheus, lib: true do
end
end
+ describe 'failure to reach a provided prometheus url' do
+ let(:prometheus_url) {"https://prometheus.invalid.example.com"}
+
+ context 'exceptions are raised' do
+ it 'raises a Gitlab::PrometheusError error when a SocketError is rescued' do
+ req_stub = stub_prometheus_request_with_exception(prometheus_url, SocketError)
+
+ expect { subject.send(:get, prometheus_url) }
+ .to raise_error(Gitlab::PrometheusError, "Can't connect to #{prometheus_url}")
+ expect(req_stub).to have_been_requested
+ end
+
+ it 'raises a Gitlab::PrometheusError error when a SSLError is rescued' do
+ req_stub = stub_prometheus_request_with_exception(prometheus_url, OpenSSL::SSL::SSLError)
+
+ expect { subject.send(:get, prometheus_url) }
+ .to raise_error(Gitlab::PrometheusError, "#{prometheus_url} contains invalid SSL data")
+ expect(req_stub).to have_been_requested
+ end
+
+ it 'raises a Gitlab::PrometheusError error when a HTTParty::Error is rescued' do
+ req_stub = stub_prometheus_request_with_exception(prometheus_url, HTTParty::Error)
+
+ expect { subject.send(:get, prometheus_url) }
+ .to raise_error(Gitlab::PrometheusError, "Network connection error")
+ expect(req_stub).to have_been_requested
+ end
+ end
+ end
+
describe '#query' do
let(:prometheus_query) { prometheus_cpu_query('env-slug') }
- let(:query_url) { prometheus_query_url(prometheus_query) }
+ let(:query_url) { prometheus_query_with_time_url(prometheus_query, Time.now.utc) }
+
+ around do |example|
+ Timecop.freeze { example.run }
+ end
context 'when request returns vector results' do
it 'returns data from the API call' do
@@ -93,6 +127,20 @@ describe Gitlab::Prometheus, lib: true do
Timecop.freeze { example.run }
end
+ context 'when non utc time is passed' do
+ let(:time_stop) { Time.now.in_time_zone("Warsaw") }
+ let(:time_start) { time_stop - 8.hours }
+
+ let(:query_url) { prometheus_query_range_url(prometheus_query, start: time_start.utc.to_f, stop: time_stop.utc.to_f) }
+
+ it 'passed dates are properly converted to utc' do
+ req_stub = stub_prometheus_request(query_url, body: prometheus_values_body('vector'))
+
+ subject.query_range(prometheus_query, start: time_start, stop: time_stop)
+ expect(req_stub).to have_been_requested
+ end
+ end
+
context 'when a start time is passed' do
let(:query_url) { prometheus_query_range_url(prometheus_query, start: 2.hours.ago) }
diff --git a/spec/lib/gitlab/regex_spec.rb b/spec/lib/gitlab/regex_spec.rb
index 127cd8c78d8..72e947f2cc2 100644
--- a/spec/lib/gitlab/regex_spec.rb
+++ b/spec/lib/gitlab/regex_spec.rb
@@ -45,8 +45,8 @@ describe Gitlab::Regex, lib: true do
it { is_expected.not_to match('foo-') }
end
- describe 'FULL_NAMESPACE_REGEX_STR' do
- subject { %r{\A#{Gitlab::Regex::FULL_NAMESPACE_REGEX_STR}\z} }
+ describe '.full_namespace_regex' do
+ subject { described_class.full_namespace_regex }
it { is_expected.to match('gitlab.org') }
it { is_expected.to match('gitlab.org/gitlab-git') }
diff --git a/spec/lib/gitlab/repo_path_spec.rb b/spec/lib/gitlab/repo_path_spec.rb
index 0fb5d7646f2..f9025397107 100644
--- a/spec/lib/gitlab/repo_path_spec.rb
+++ b/spec/lib/gitlab/repo_path_spec.rb
@@ -1,11 +1,35 @@
require 'spec_helper'
describe ::Gitlab::RepoPath do
+ describe '.parse' do
+ set(:project) { create(:project) }
+
+ it 'parses a full repository path' do
+ expect(described_class.parse(project.repository.path)).to eq([project, false])
+ end
+
+ it 'parses a full wiki path' do
+ expect(described_class.parse(project.wiki.repository.path)).to eq([project, true])
+ end
+
+ it 'parses a relative repository path' do
+ expect(described_class.parse(project.full_path + '.git')).to eq([project, false])
+ end
+
+ it 'parses a relative wiki path' do
+ expect(described_class.parse(project.full_path + '.wiki.git')).to eq([project, true])
+ end
+
+ it 'parses a relative path starting with /' do
+ expect(described_class.parse('/' + project.full_path + '.git')).to eq([project, false])
+ end
+ end
+
describe '.strip_storage_path' do
before do
allow(Gitlab.config.repositories).to receive(:storages).and_return({
'storage1' => { 'path' => '/foo' },
- 'storage2' => { 'path' => '/bar' },
+ 'storage2' => { 'path' => '/bar' }
})
end
diff --git a/spec/lib/gitlab/request_profiler_spec.rb b/spec/lib/gitlab/request_profiler_spec.rb
new file mode 100644
index 00000000000..ae9c06ebb7d
--- /dev/null
+++ b/spec/lib/gitlab/request_profiler_spec.rb
@@ -0,0 +1,27 @@
+require 'spec_helper'
+
+describe Gitlab::RequestProfiler, lib: true do
+ describe '.profile_token' do
+ it 'returns a token' do
+ expect(described_class.profile_token).to be_present
+ end
+
+ it 'caches the token' do
+ expect(Rails.cache).to receive(:fetch).with('profile-token')
+
+ described_class.profile_token
+ end
+ end
+
+ describe '.remove_all_profiles' do
+ it 'removes Gitlab::RequestProfiler::PROFILES_DIR directory' do
+ dir = described_class::PROFILES_DIR
+ FileUtils.mkdir_p(dir)
+
+ expect(Dir.exist?(dir)).to be true
+
+ described_class.remove_all_profiles
+ expect(Dir.exist?(dir)).to be false
+ end
+ end
+end
diff --git a/spec/lib/gitlab/saml/user_spec.rb b/spec/lib/gitlab/saml/user_spec.rb
index 4f6ef3c10fc..b106d156b75 100644
--- a/spec/lib/gitlab/saml/user_spec.rb
+++ b/spec/lib/gitlab/saml/user_spec.rb
@@ -211,6 +211,31 @@ describe Gitlab::Saml::User, lib: true do
end
end
end
+
+ context 'when signup is disabled' do
+ before do
+ stub_application_setting signup_enabled: false
+ end
+
+ it 'creates the user' do
+ saml_user.save
+
+ expect(gl_user).to be_persisted
+ end
+ end
+
+ context 'when user confirmation email is enabled' do
+ before do
+ stub_application_setting send_user_confirmation_email: true
+ end
+
+ it 'creates and confirms the user anyway' do
+ saml_user.save
+
+ expect(gl_user).to be_persisted
+ expect(gl_user).to be_confirmed
+ end
+ end
end
describe 'blocking' do
diff --git a/spec/lib/gitlab/search_results_spec.rb b/spec/lib/gitlab/search_results_spec.rb
index 847fb977400..31c3cd4d53c 100644
--- a/spec/lib/gitlab/search_results_spec.rb
+++ b/spec/lib/gitlab/search_results_spec.rb
@@ -72,9 +72,9 @@ describe Gitlab::SearchResults do
let(:admin) { create(:admin) }
let!(:issue) { create(:issue, project: project_1, title: 'Issue 1') }
let!(:security_issue_1) { create(:issue, :confidential, project: project_1, title: 'Security issue 1', author: author) }
- let!(:security_issue_2) { create(:issue, :confidential, title: 'Security issue 2', project: project_1, assignee: assignee) }
+ let!(:security_issue_2) { create(:issue, :confidential, title: 'Security issue 2', project: project_1, assignees: [assignee]) }
let!(:security_issue_3) { create(:issue, :confidential, project: project_2, title: 'Security issue 3', author: author) }
- let!(:security_issue_4) { create(:issue, :confidential, project: project_3, title: 'Security issue 4', assignee: assignee) }
+ let!(:security_issue_4) { create(:issue, :confidential, project: project_3, title: 'Security issue 4', assignees: [assignee]) }
let!(:security_issue_5) { create(:issue, :confidential, project: project_4, title: 'Security issue 5') }
it 'does not list confidential issues for non project members' do
diff --git a/spec/lib/gitlab/backend/shell_spec.rb b/spec/lib/gitlab/shell_spec.rb
index 6675d26734e..a97a0f8452b 100644
--- a/spec/lib/gitlab/backend/shell_spec.rb
+++ b/spec/lib/gitlab/shell_spec.rb
@@ -91,4 +91,45 @@ describe Gitlab::Shell, lib: true do
end
end
end
+
+ describe 'projects commands' do
+ let(:projects_path) { 'tmp/tests/shell-projects-test/bin/gitlab-projects' }
+
+ before do
+ allow(Gitlab.config.gitlab_shell).to receive(:path).and_return('tmp/tests/shell-projects-test')
+ allow(Gitlab.config.gitlab_shell).to receive(:git_timeout).and_return(800)
+ end
+
+ describe '#fetch_remote' do
+ it 'returns true when the command succeeds' do
+ expect(Gitlab::Popen).to receive(:popen)
+ .with([projects_path, 'fetch-remote', 'current/storage', 'project/path.git', 'new/storage', '800']).and_return([nil, 0])
+
+ expect(gitlab_shell.fetch_remote('current/storage', 'project/path', 'new/storage')).to be true
+ end
+
+ it 'raises an exception when the command fails' do
+ expect(Gitlab::Popen).to receive(:popen)
+ .with([projects_path, 'fetch-remote', 'current/storage', 'project/path.git', 'new/storage', '800']).and_return(["error", 1])
+
+ expect { gitlab_shell.fetch_remote('current/storage', 'project/path', 'new/storage') }.to raise_error(Gitlab::Shell::Error, "error")
+ end
+ end
+
+ describe '#import_repository' do
+ it 'returns true when the command succeeds' do
+ expect(Gitlab::Popen).to receive(:popen)
+ .with([projects_path, 'import-project', 'current/storage', 'project/path.git', 'https://gitlab.com/gitlab-org/gitlab-ce.git', "800"]).and_return([nil, 0])
+
+ expect(gitlab_shell.import_repository('current/storage', 'project/path', 'https://gitlab.com/gitlab-org/gitlab-ce.git')).to be true
+ end
+
+ it 'raises an exception when the command fails' do
+ expect(Gitlab::Popen).to receive(:popen)
+ .with([projects_path, 'import-project', 'current/storage', 'project/path.git', 'https://gitlab.com/gitlab-org/gitlab-ce.git', "800"]).and_return(["error", 1])
+
+ expect { gitlab_shell.import_repository('current/storage', 'project/path', 'https://gitlab.com/gitlab-org/gitlab-ce.git') }.to raise_error(Gitlab::Shell::Error, "error")
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/sidekiq_throttler_spec.rb b/spec/lib/gitlab/sidekiq_throttler_spec.rb
index ff32e0e699d..6374ac80207 100644
--- a/spec/lib/gitlab/sidekiq_throttler_spec.rb
+++ b/spec/lib/gitlab/sidekiq_throttler_spec.rb
@@ -13,14 +13,14 @@ describe Gitlab::SidekiqThrottler do
describe '#execute!' do
it 'sets limits on the selected queues' do
- Gitlab::SidekiqThrottler.execute!
+ described_class.execute!
expect(Sidekiq::Queue['build'].limit).to eq 4
expect(Sidekiq::Queue['project_cache'].limit).to eq 4
end
it 'does not set limits on other queues' do
- Gitlab::SidekiqThrottler.execute!
+ described_class.execute!
expect(Sidekiq::Queue['merge'].limit).to be_nil
end
diff --git a/spec/lib/gitlab/slash_commands/command_definition_spec.rb b/spec/lib/gitlab/slash_commands/command_definition_spec.rb
index c9c2f314e57..5b9173d3d3f 100644
--- a/spec/lib/gitlab/slash_commands/command_definition_spec.rb
+++ b/spec/lib/gitlab/slash_commands/command_definition_spec.rb
@@ -167,6 +167,58 @@ describe Gitlab::SlashCommands::CommandDefinition do
end
end
end
+
+ context 'when the command defines parse_params block' do
+ before do
+ subject.parse_params_block = ->(raw) { raw.strip }
+ subject.action_block = ->(parsed) { self.received_arg = parsed }
+ end
+
+ it 'executes the command passing the parsed param' do
+ subject.execute(context, {}, 'something ')
+
+ expect(context.received_arg).to eq('something')
+ end
+ end
+ end
+ end
+ end
+
+ describe '#explain' do
+ context 'when the command is not available' do
+ before do
+ subject.condition_block = proc { false }
+ subject.explanation = 'Explanation'
+ end
+
+ it 'returns nil' do
+ result = subject.explain({}, {}, nil)
+
+ expect(result).to be_nil
+ end
+ end
+
+ context 'when the explanation is a static string' do
+ before do
+ subject.explanation = 'Explanation'
+ end
+
+ it 'returns this static string' do
+ result = subject.explain({}, {}, nil)
+
+ expect(result).to eq 'Explanation'
+ end
+ end
+
+ context 'when the explanation is dynamic' do
+ before do
+ subject.explanation = proc { |arg| "Dynamic #{arg}" }
+ end
+
+ it 'invokes the proc' do
+ result = subject.explain({}, {}, 'explanation')
+
+ expect(result).to eq 'Dynamic explanation'
end
end
end
diff --git a/spec/lib/gitlab/slash_commands/dsl_spec.rb b/spec/lib/gitlab/slash_commands/dsl_spec.rb
index 26217a0e3b2..33b49a5ddf9 100644
--- a/spec/lib/gitlab/slash_commands/dsl_spec.rb
+++ b/spec/lib/gitlab/slash_commands/dsl_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
describe Gitlab::SlashCommands::Dsl do
before :all do
DummyClass = Struct.new(:project) do
- include Gitlab::SlashCommands::Dsl
+ include Gitlab::SlashCommands::Dsl # rubocop:disable RSpec/DescribedClass
desc 'A command with no args'
command :no_args, :none do
@@ -11,67 +11,99 @@ describe Gitlab::SlashCommands::Dsl do
end
params 'The first argument'
- command :one_arg, :once, :first do |arg1|
- arg1
+ explanation 'Static explanation'
+ command :explanation_with_aliases, :once, :first do |arg|
+ arg
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]
+ command :dynamic_description do |args|
+ args.split
end
command :cc
+ explanation do |arg|
+ "Action does something with #{arg}"
+ end
condition do
project == 'foo'
end
command :cond_action do |arg|
arg
end
+
+ parse_params do |raw_arg|
+ raw_arg.strip
+ end
+ command :with_params_parsing do |parsed|
+ parsed
+ 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
+ no_args_def, explanation_with_aliases_def, dynamic_description_def,
+ cc_def, cond_action_def, with_params_parsing_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.explanation).to eq('')
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(no_args_def.parse_params_block).to be_nil
- 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(explanation_with_aliases_def.name).to eq(:explanation_with_aliases)
+ expect(explanation_with_aliases_def.aliases).to eq([:once, :first])
+ expect(explanation_with_aliases_def.description).to eq('')
+ expect(explanation_with_aliases_def.explanation).to eq('Static explanation')
+ expect(explanation_with_aliases_def.params).to eq(['The first argument'])
+ expect(explanation_with_aliases_def.condition_block).to be_nil
+ expect(explanation_with_aliases_def.action_block).to be_a_kind_of(Proc)
+ expect(explanation_with_aliases_def.parse_params_block).to be_nil
- 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(dynamic_description_def.name).to eq(:dynamic_description)
+ expect(dynamic_description_def.aliases).to eq([])
+ expect(dynamic_description_def.to_h(noteable: 'issue')[:description]).to eq('A dynamic description for ISSUE')
+ expect(dynamic_description_def.explanation).to eq('')
+ expect(dynamic_description_def.params).to eq(['The first argument', 'The second argument'])
+ expect(dynamic_description_def.condition_block).to be_nil
+ expect(dynamic_description_def.action_block).to be_a_kind_of(Proc)
+ expect(dynamic_description_def.parse_params_block).to be_nil
expect(cc_def.name).to eq(:cc)
expect(cc_def.aliases).to eq([])
expect(cc_def.description).to eq('')
+ expect(cc_def.explanation).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(cc_def.parse_params_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.explanation).to be_a_kind_of(Proc)
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)
+ expect(cond_action_def.parse_params_block).to be_nil
+
+ expect(with_params_parsing_def.name).to eq(:with_params_parsing)
+ expect(with_params_parsing_def.aliases).to eq([])
+ expect(with_params_parsing_def.description).to eq('')
+ expect(with_params_parsing_def.explanation).to eq('')
+ expect(with_params_parsing_def.params).to eq([])
+ expect(with_params_parsing_def.condition_block).to be_nil
+ expect(with_params_parsing_def.action_block).to be_a_kind_of(Proc)
+ expect(with_params_parsing_def.parse_params_block).to be_a_kind_of(Proc)
end
end
end
diff --git a/spec/lib/gitlab/string_range_marker_spec.rb b/spec/lib/gitlab/string_range_marker_spec.rb
new file mode 100644
index 00000000000..7c77772b3f6
--- /dev/null
+++ b/spec/lib/gitlab/string_range_marker_spec.rb
@@ -0,0 +1,36 @@
+require 'spec_helper'
+
+describe Gitlab::StringRangeMarker, lib: true do
+ describe '#mark' do
+ context "when the rich text is html safe" do
+ let(:raw) { "abc <def>" }
+ let(:rich) { %{<span class="abc">abc</span><span class="space"> </span><span class="def">&lt;def&gt;</span>}.html_safe }
+ let(:inline_diffs) { [2..5] }
+ subject do
+ described_class.new(raw, rich).mark(inline_diffs) do |text, left:, right:|
+ "LEFT#{text}RIGHT"
+ end
+ end
+
+ it 'marks the inline diffs' do
+ expect(subject).to eq(%{<span class="abc">abLEFTcRIGHT</span><span class="space">LEFT RIGHT</span><span class="def">LEFT&lt;dRIGHTef&gt;</span>})
+ expect(subject).to be_html_safe
+ end
+ end
+
+ context "when the rich text is not html safe" do
+ let(:raw) { "abc <def>" }
+ let(:inline_diffs) { [2..5] }
+ subject do
+ described_class.new(raw).mark(inline_diffs) do |text, left:, right:|
+ "LEFT#{text}RIGHT"
+ end
+ end
+
+ it 'marks the inline diffs' do
+ expect(subject).to eq(%{abLEFTc &lt;dRIGHTef&gt;})
+ expect(subject).to be_html_safe
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/string_regex_marker_spec.rb b/spec/lib/gitlab/string_regex_marker_spec.rb
new file mode 100644
index 00000000000..2f5cf6c6e3b
--- /dev/null
+++ b/spec/lib/gitlab/string_regex_marker_spec.rb
@@ -0,0 +1,18 @@
+require 'spec_helper'
+
+describe Gitlab::StringRegexMarker, lib: true do
+ describe '#mark' do
+ let(:raw) { %{"name": "AFNetworking"} }
+ let(:rich) { %{<span class="key">"name"</span><span class="punctuation">: </span><span class="value">"AFNetworking"</span>}.html_safe }
+ subject do
+ described_class.new(raw, rich).mark(/"[^"]+":\s*"(?<name>[^"]+)"/, group: :name) do |text, left:, right:|
+ %{<a href="#">#{text}</a>}
+ end
+ end
+
+ it 'marks the inline diffs' do
+ expect(subject).to eq(%{<span class="key">"name"</span><span class="punctuation">: </span><span class="value">"<a href="#">AFNetworking</a>"</span>})
+ expect(subject).to be_html_safe
+ end
+ end
+end
diff --git a/spec/lib/gitlab/template/gitignore_template_spec.rb b/spec/lib/gitlab/template/gitignore_template_spec.rb
index 9750a012e22..97797f42aaa 100644
--- a/spec/lib/gitlab/template/gitignore_template_spec.rb
+++ b/spec/lib/gitlab/template/gitignore_template_spec.rb
@@ -24,7 +24,7 @@ describe Gitlab::Template::GitignoreTemplate do
it 'returns the Gitignore object of a valid file' do
ruby = subject.find('Ruby')
- expect(ruby).to be_a Gitlab::Template::GitignoreTemplate
+ expect(ruby).to be_a described_class
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
index e3b8321eda3..6541326d1de 100644
--- a/spec/lib/gitlab/template/gitlab_ci_yml_template_spec.rb
+++ b/spec/lib/gitlab/template/gitlab_ci_yml_template_spec.rb
@@ -25,7 +25,7 @@ describe Gitlab::Template::GitlabCiYmlTemplate do
it 'returns the GitlabCiYml object of a valid file' do
ruby = subject.find('Ruby')
- expect(ruby).to be_a Gitlab::Template::GitlabCiYmlTemplate
+ expect(ruby).to be_a described_class
expect(ruby.name).to eq('Ruby')
end
end
diff --git a/spec/lib/gitlab/template/issue_template_spec.rb b/spec/lib/gitlab/template/issue_template_spec.rb
index 9213ced7b19..329d1d74970 100644
--- a/spec/lib/gitlab/template/issue_template_spec.rb
+++ b/spec/lib/gitlab/template/issue_template_spec.rb
@@ -37,7 +37,7 @@ describe Gitlab::Template::IssueTemplate do
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).to be_a described_class
expect(ruby.name).to eq('bug')
end
end
diff --git a/spec/lib/gitlab/template/merge_request_template_spec.rb b/spec/lib/gitlab/template/merge_request_template_spec.rb
index 77dd3079e22..2b0056d9bab 100644
--- a/spec/lib/gitlab/template/merge_request_template_spec.rb
+++ b/spec/lib/gitlab/template/merge_request_template_spec.rb
@@ -37,7 +37,7 @@ describe Gitlab::Template::MergeRequestTemplate do
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).to be_a described_class
expect(ruby.name).to eq('bug')
end
end
diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb
index 7f21288cf88..2c46920456b 100644
--- a/spec/lib/gitlab/usage_data_spec.rb
+++ b/spec/lib/gitlab/usage_data_spec.rb
@@ -6,7 +6,7 @@ describe Gitlab::UsageData do
let!(:board) { create(:board, project: project) }
describe '#data' do
- subject { Gitlab::UsageData.data }
+ subject { described_class.data }
it "gathers usage data" do
expect(subject.keys).to match_array(%i(
@@ -17,6 +17,7 @@ describe Gitlab::UsageData do
edition
version
uuid
+ hostname
))
end
@@ -32,6 +33,7 @@ describe Gitlab::UsageData do
ci_pipelines
ci_runners
ci_triggers
+ ci_pipeline_schedules
deploy_keys
deployments
environments
@@ -58,7 +60,7 @@ describe Gitlab::UsageData do
end
describe '#license_usage_data' do
- subject { Gitlab::UsageData.license_usage_data }
+ subject { described_class.license_usage_data }
it "gathers license data" do
expect(subject[:uuid]).to eq(current_application_settings.uuid)
diff --git a/spec/lib/gitlab/user_access_spec.rb b/spec/lib/gitlab/user_access_spec.rb
index 611cdbbc865..0d87cf25dbb 100644
--- a/spec/lib/gitlab/user_access_spec.rb
+++ b/spec/lib/gitlab/user_access_spec.rb
@@ -5,7 +5,7 @@ describe Gitlab::UserAccess, lib: true do
let(:project) { create(:project) }
let(:user) { create(:user) }
- describe 'can_push_to_branch?' do
+ 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]
@@ -87,10 +87,10 @@ describe Gitlab::UserAccess, lib: true do
expect(access.can_push_to_branch?(branch.name)).to be_falsey
end
- it 'returns true if branch does not exist and user has permission to merge' do
+ it 'returns false if branch does not exist' do
project.team << [user, :developer]
- expect(access.can_push_to_branch?(not_existing_branch.name)).to be_truthy
+ expect(access.can_push_to_branch?(not_existing_branch.name)).to be_falsey
end
end
@@ -143,7 +143,7 @@ describe Gitlab::UserAccess, lib: true do
end
end
- describe 'can_create_tag?' do
+ describe '#can_create_tag?' do
describe 'push to none protected tag' do
it 'returns true if user is a master' do
project.add_user(user, :master)
@@ -211,4 +211,48 @@ describe Gitlab::UserAccess, lib: true do
end
end
end
+
+ describe '#can_delete_branch?' do
+ describe 'delete unprotected branch' do
+ it 'returns true if user is a master' do
+ project.add_user(user, :master)
+
+ expect(access.can_delete_branch?('random_branch')).to be_truthy
+ end
+
+ it 'returns true if user is a developer' do
+ project.add_user(user, :developer)
+
+ expect(access.can_delete_branch?('random_branch')).to be_truthy
+ end
+
+ it 'returns false if user is a reporter' do
+ project.add_user(user, :reporter)
+
+ expect(access.can_delete_branch?('random_branch')).to be_falsey
+ end
+ end
+
+ describe 'delete protected branch' do
+ let(:branch) { create(:protected_branch, project: project, name: "test") }
+
+ it 'returns true if user is a master' do
+ project.add_user(user, :master)
+
+ expect(access.can_delete_branch?(branch.name)).to be_truthy
+ end
+
+ it 'returns false if user is a developer' do
+ project.add_user(user, :developer)
+
+ expect(access.can_delete_branch?(branch.name)).to be_falsey
+ end
+
+ it 'returns false if user is a reporter' do
+ project.add_user(user, :reporter)
+
+ expect(access.can_delete_branch?(branch.name)).to be_falsey
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/workhorse_spec.rb b/spec/lib/gitlab/workhorse_spec.rb
index b703e9808a8..67b759f7dcd 100644
--- a/spec/lib/gitlab/workhorse_spec.rb
+++ b/spec/lib/gitlab/workhorse_spec.rb
@@ -181,15 +181,28 @@ describe Gitlab::Workhorse, lib: true do
let(:user) { create(:user) }
let(:repo_path) { repository.path_to_repo }
let(:action) { 'info_refs' }
+ let(:params) do
+ { GL_ID: "user-#{user.id}", GL_REPOSITORY: "project-#{project.id}", RepoPath: repo_path }
+ end
+
+ subject { described_class.git_http_ok(repository, false, user, action) }
+
+ it { expect(subject).to include(params) }
- subject { described_class.git_http_ok(repository, user, action) }
+ context 'when is_wiki' do
+ let(:params) do
+ { GL_ID: "user-#{user.id}", GL_REPOSITORY: "wiki-#{project.id}", RepoPath: repo_path }
+ end
+
+ subject { described_class.git_http_ok(repository, true, user, action) }
- it { expect(subject).to include({ GL_ID: "user-#{user.id}", RepoPath: repo_path }) }
+ it { expect(subject).to include(params) }
+ end
context 'when Gitaly is enabled' do
let(:gitaly_params) do
{
- GitalyAddress: Gitlab::GitalyClient.get_address('default'),
+ GitalyAddress: Gitlab::GitalyClient.get_address('default')
}
end
@@ -201,7 +214,7 @@ describe Gitlab::Workhorse, lib: true do
repo_param = { Repository: {
path: repo_path,
storage_name: 'default',
- relative_path: project.full_path + '.git',
+ relative_path: project.full_path + '.git'
} }
expect(subject).to include(repo_param)
diff --git a/spec/lib/light_url_builder_spec.rb b/spec/lib/light_url_builder_spec.rb
deleted file mode 100644
index 3fe8cf43934..00000000000
--- a/spec/lib/light_url_builder_spec.rb
+++ /dev/null
@@ -1,119 +0,0 @@
-require 'spec_helper'
-
-describe Gitlab::UrlBuilder, lib: true do
- describe '.build' do
- context 'when passing a Commit' do
- it 'returns a proper URL' do
- commit = build_stubbed(:commit)
-
- url = described_class.build(commit)
-
- expect(url).to eq "#{Settings.gitlab['url']}/#{commit.project.path_with_namespace}/commit/#{commit.id}"
- end
- end
-
- context 'when passing an Issue' do
- it 'returns a proper URL' do
- issue = build_stubbed(:issue, iid: 42)
-
- url = described_class.build(issue)
-
- expect(url).to eq "#{Settings.gitlab['url']}/#{issue.project.path_with_namespace}/issues/#{issue.iid}"
- end
- end
-
- context 'when passing a MergeRequest' do
- it 'returns a proper URL' do
- merge_request = build_stubbed(:merge_request, iid: 42)
-
- url = described_class.build(merge_request)
-
- expect(url).to eq "#{Settings.gitlab['url']}/#{merge_request.project.path_with_namespace}/merge_requests/#{merge_request.iid}"
- end
- end
-
- context 'when passing a Note' do
- context 'on a Commit' do
- it 'returns a proper URL' do
- note = build_stubbed(:note_on_commit)
-
- url = described_class.build(note)
-
- expect(url).to eq "#{Settings.gitlab['url']}/#{note.project.path_with_namespace}/commit/#{note.commit_id}#note_#{note.id}"
- end
- end
-
- context 'on a Commit Diff' do
- it 'returns a proper URL' do
- note = build_stubbed(:diff_note_on_commit)
-
- url = described_class.build(note)
-
- expect(url).to eq "#{Settings.gitlab['url']}/#{note.project.path_with_namespace}/commit/#{note.commit_id}#note_#{note.id}"
- end
- end
-
- context 'on an Issue' do
- it 'returns a proper URL' do
- issue = create(:issue, iid: 42)
- note = build_stubbed(:note_on_issue, noteable: issue)
-
- url = described_class.build(note)
-
- expect(url).to eq "#{Settings.gitlab['url']}/#{issue.project.path_with_namespace}/issues/#{issue.iid}#note_#{note.id}"
- end
- end
-
- context 'on a MergeRequest' do
- it 'returns a proper URL' do
- merge_request = create(:merge_request, iid: 42)
- note = build_stubbed(:note_on_merge_request, noteable: merge_request)
-
- url = described_class.build(note)
-
- expect(url).to eq "#{Settings.gitlab['url']}/#{merge_request.project.path_with_namespace}/merge_requests/#{merge_request.iid}#note_#{note.id}"
- end
- end
-
- context 'on a MergeRequest Diff' do
- it 'returns a proper URL' do
- merge_request = create(:merge_request, iid: 42)
- note = build_stubbed(:diff_note_on_merge_request, noteable: merge_request)
-
- url = described_class.build(note)
-
- expect(url).to eq "#{Settings.gitlab['url']}/#{merge_request.project.path_with_namespace}/merge_requests/#{merge_request.iid}#note_#{note.id}"
- end
- end
-
- context 'on a ProjectSnippet' do
- it 'returns a proper URL' do
- project_snippet = create(:project_snippet)
- note = build_stubbed(:note_on_project_snippet, noteable: project_snippet)
-
- url = described_class.build(note)
-
- expect(url).to eq "#{Settings.gitlab['url']}/#{project_snippet.project.path_with_namespace}/snippets/#{note.noteable_id}#note_#{note.id}"
- end
- end
-
- context 'on another object' do
- it 'returns a proper URL' do
- project = build_stubbed(:empty_project)
-
- expect { described_class.build(project) }.
- to raise_error(NotImplementedError, 'No URL builder defined for Project')
- end
- end
- end
-
- context 'when passing a WikiPage' do
- it 'returns a proper URL' do
- wiki_page = build(:wiki_page)
- url = described_class.build(wiki_page)
-
- expect(url).to eq "#{Gitlab.config.gitlab.url}#{wiki_page.wiki.wiki_base_path}/#{wiki_page.slug}"
- end
- end
- end
-end
diff --git a/spec/mailers/emails/merge_requests_spec.rb b/spec/mailers/emails/merge_requests_spec.rb
index e22858d1d8f..2ad572bb5c7 100644
--- a/spec/mailers/emails/merge_requests_spec.rb
+++ b/spec/mailers/emails/merge_requests_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
require 'email_spec'
-describe Notify, "merge request notifications" do
+describe Emails::MergeRequests do
include EmailSpec::Matchers
describe "#resolved_all_discussions_email" do
diff --git a/spec/mailers/emails/profile_spec.rb b/spec/mailers/emails/profile_spec.rb
index 5ca936f28f0..8c1c9bf135f 100644
--- a/spec/mailers/emails/profile_spec.rb
+++ b/spec/mailers/emails/profile_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
require 'email_spec'
-describe Notify do
+describe Emails::Profile do
include EmailSpec::Matchers
include_context 'gitlab email notification'
@@ -15,106 +15,104 @@ describe Notify do
end
end
- describe 'profile notifications' do
- describe 'for new users, the email' do
- let(:example_site_path) { root_path }
- let(:new_user) { create(:user, email: new_user_address, created_by_id: 1) }
- let(:token) { 'kETLwRaayvigPq_x3SNM' }
+ describe 'for new users, the email' do
+ let(:example_site_path) { root_path }
+ let(:new_user) { create(:user, email: new_user_address, created_by_id: 1) }
+ let(:token) { 'kETLwRaayvigPq_x3SNM' }
- subject { Notify.new_user_email(new_user.id, token) }
+ subject { Notify.new_user_email(new_user.id, token) }
- it_behaves_like 'an email sent from GitLab'
- it_behaves_like 'a new user email'
- it_behaves_like 'it should not have Gmail Actions links'
- it_behaves_like 'a user cannot unsubscribe through footer link'
+ it_behaves_like 'an email sent from GitLab'
+ it_behaves_like 'a new user email'
+ it_behaves_like 'it should not have Gmail Actions links'
+ it_behaves_like 'a user cannot unsubscribe through footer link'
- it 'contains the password text' do
- is_expected.to have_body_text /Click here to set your password/
- end
+ it 'contains the password text' do
+ is_expected.to have_body_text /Click here to set your password/
+ end
- it 'includes a link for user to set password' do
- params = "reset_password_token=#{token}"
- is_expected.to have_body_text(
- %r{http://#{Gitlab.config.gitlab.host}(:\d+)?/users/password/edit\?#{params}}
- )
- end
+ it 'includes a link for user to set password' do
+ params = "reset_password_token=#{token}"
+ is_expected.to have_body_text(
+ %r{http://#{Gitlab.config.gitlab.host}(:\d+)?/users/password/edit\?#{params}}
+ )
+ end
- it 'explains the reset link expiration' do
- is_expected.to have_body_text(/This link is valid for \d+ (hours?|days?)/)
- is_expected.to have_body_text(new_user_password_url)
- is_expected.to have_body_text(/\?user_email=.*%40.*/)
- end
+ it 'explains the reset link expiration' do
+ is_expected.to have_body_text(/This link is valid for \d+ (hours?|days?)/)
+ is_expected.to have_body_text(new_user_password_url)
+ is_expected.to have_body_text(/\?user_email=.*%40.*/)
end
+ end
- describe 'for users that signed up, the email' do
- let(:example_site_path) { root_path }
- let(:new_user) { create(:user, email: new_user_address, password: "securePassword") }
+ describe 'for users that signed up, the email' do
+ let(:example_site_path) { root_path }
+ let(:new_user) { create(:user, email: new_user_address, password: "securePassword") }
- subject { Notify.new_user_email(new_user.id) }
+ subject { Notify.new_user_email(new_user.id) }
- it_behaves_like 'an email sent from GitLab'
- it_behaves_like 'a new user email'
- it_behaves_like 'it should not have Gmail Actions links'
- it_behaves_like 'a user cannot unsubscribe through footer link'
+ it_behaves_like 'an email sent from GitLab'
+ it_behaves_like 'a new user email'
+ it_behaves_like 'it should not have Gmail Actions links'
+ it_behaves_like 'a user cannot unsubscribe through footer link'
- it 'does not contain the new user\'s password' do
- is_expected.not_to have_body_text /password/
- end
+ it 'does not contain the new user\'s password' do
+ is_expected.not_to have_body_text /password/
end
+ end
- describe 'user added ssh key' do
- let(:key) { create(:personal_key) }
+ describe 'user added ssh key' do
+ let(:key) { create(:personal_key) }
- subject { Notify.new_ssh_key_email(key.id) }
+ subject { Notify.new_ssh_key_email(key.id) }
- it_behaves_like 'an email sent from GitLab'
- it_behaves_like 'it should not have Gmail Actions links'
- it_behaves_like 'a user cannot unsubscribe through footer link'
+ it_behaves_like 'an email sent from GitLab'
+ it_behaves_like 'it should not have Gmail Actions links'
+ it_behaves_like 'a user cannot unsubscribe through footer link'
- it 'is sent to the new user' do
- is_expected.to deliver_to key.user.email
- end
+ it 'is sent to the new user' do
+ is_expected.to deliver_to key.user.email
+ end
- it 'has the correct subject' do
- is_expected.to have_subject /^SSH key was added to your account$/i
- end
+ it 'has the correct subject' do
+ is_expected.to have_subject /^SSH key was added to your account$/i
+ end
- it 'contains the new ssh key title' do
- is_expected.to have_body_text /#{key.title}/
- end
+ it 'contains the new ssh key title' do
+ is_expected.to have_body_text /#{key.title}/
+ end
- it 'includes a link to ssh keys page' do
- is_expected.to have_body_text /#{profile_keys_path}/
- end
+ it 'includes a link to ssh keys page' do
+ is_expected.to have_body_text /#{profile_keys_path}/
+ end
- context 'with SSH key that does not exist' do
- it { expect { Notify.new_ssh_key_email('foo') }.not_to raise_error }
- end
+ context 'with SSH key that does not exist' do
+ it { expect { Notify.new_ssh_key_email('foo') }.not_to raise_error }
end
+ end
- describe 'user added email' do
- let(:email) { create(:email) }
+ describe 'user added email' do
+ let(:email) { create(:email) }
- subject { Notify.new_email_email(email.id) }
+ subject { Notify.new_email_email(email.id) }
- it_behaves_like 'it should not have Gmail Actions links'
- it_behaves_like 'a user cannot unsubscribe through footer link'
+ it_behaves_like 'it should not have Gmail Actions links'
+ it_behaves_like 'a user cannot unsubscribe through footer link'
- it 'is sent to the new user' do
- is_expected.to deliver_to email.user.email
- end
+ it 'is sent to the new user' do
+ is_expected.to deliver_to email.user.email
+ end
- it 'has the correct subject' do
- is_expected.to have_subject /^Email was added to your account$/i
- end
+ it 'has the correct subject' do
+ is_expected.to have_subject /^Email was added to your account$/i
+ end
- it 'contains the new email address' do
- is_expected.to have_body_text /#{email.email}/
- end
+ it 'contains the new email address' do
+ is_expected.to have_body_text /#{email.email}/
+ end
- it 'includes a link to emails page' do
- is_expected.to have_body_text /#{profile_emails_path}/
- end
+ it 'includes a link to emails page' do
+ is_expected.to have_body_text /#{profile_emails_path}/
end
end
end
diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb
index e6f0a3b5920..1e6260270fe 100644
--- a/spec/mailers/notify_spec.rb
+++ b/spec/mailers/notify_spec.rb
@@ -36,11 +36,11 @@ describe Notify do
end
context 'for issues' do
- let(:issue) { create(:issue, author: current_user, assignee: assignee, project: project) }
- let(:issue_with_description) { create(:issue, author: current_user, assignee: assignee, project: project, description: 'My awesome description') }
+ let(:issue) { create(:issue, author: current_user, assignees: [assignee], project: project) }
+ let(:issue_with_description) { create(:issue, author: current_user, assignees: [assignee], project: project, description: 'My awesome description') }
describe 'that are new' do
- subject { Notify.new_issue_email(issue.assignee_id, issue.id) }
+ subject { described_class.new_issue_email(issue.assignees.first.id, issue.id) }
it_behaves_like 'an assignee email'
it_behaves_like 'an email starting a new thread with reply-by-email enabled' do
@@ -69,7 +69,7 @@ describe Notify do
end
describe 'that are new with a description' do
- subject { Notify.new_issue_email(issue_with_description.assignee_id, issue_with_description.id) }
+ subject { described_class.new_issue_email(issue_with_description.assignees.first.id, issue_with_description.id) }
it_behaves_like 'it should show Gmail Actions View Issue link'
@@ -79,7 +79,7 @@ describe Notify do
end
describe 'that have been reassigned' do
- subject { Notify.reassigned_issue_email(recipient.id, issue.id, previous_assignee.id, current_user.id) }
+ subject { described_class.reassigned_issue_email(recipient.id, issue.id, [previous_assignee.id], current_user.id) }
it_behaves_like 'a multiple recipients email'
it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
@@ -105,7 +105,7 @@ describe Notify do
end
describe 'that have been relabeled' do
- subject { Notify.relabeled_issue_email(recipient.id, issue.id, %w[foo bar baz], current_user.id) }
+ subject { described_class.relabeled_issue_email(recipient.id, issue.id, %w[foo bar baz], current_user.id) }
it_behaves_like 'a multiple recipients email'
it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
@@ -132,7 +132,7 @@ describe Notify do
describe 'status changed' do
let(:status) { 'closed' }
- subject { Notify.issue_status_changed_email(recipient.id, issue.id, status, current_user.id) }
+ subject { described_class.issue_status_changed_email(recipient.id, issue.id, status, current_user.id) }
it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
let(:model) { issue }
@@ -158,7 +158,7 @@ describe Notify do
describe 'moved to another project' do
let(:new_issue) { create(:issue) }
- subject { Notify.issue_moved_email(recipient, issue, new_issue, current_user) }
+ subject { described_class.issue_moved_email(recipient, issue, new_issue, current_user) }
it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
let(:model) { issue }
@@ -190,7 +190,7 @@ describe Notify do
let(:merge_request_with_description) { create(:merge_request, author: current_user, assignee: assignee, source_project: project, target_project: project, description: 'My awesome description') }
describe 'that are new' do
- subject { Notify.new_merge_request_email(merge_request.assignee_id, merge_request.id) }
+ subject { described_class.new_merge_request_email(merge_request.assignee_id, merge_request.id) }
it_behaves_like 'an assignee email'
it_behaves_like 'an email starting a new thread with reply-by-email enabled' do
@@ -221,7 +221,7 @@ describe Notify do
end
describe 'that are new with a description' do
- subject { Notify.new_merge_request_email(merge_request_with_description.assignee_id, merge_request_with_description.id) }
+ subject { described_class.new_merge_request_email(merge_request_with_description.assignee_id, merge_request_with_description.id) }
it_behaves_like 'it should show Gmail Actions View Merge request link'
it_behaves_like "an unsubscribeable thread"
@@ -232,7 +232,7 @@ describe Notify do
end
describe 'that are reassigned' do
- subject { Notify.reassigned_merge_request_email(recipient.id, merge_request.id, previous_assignee.id, current_user.id) }
+ subject { described_class.reassigned_merge_request_email(recipient.id, merge_request.id, previous_assignee.id, current_user.id) }
it_behaves_like 'a multiple recipients email'
it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
@@ -258,7 +258,7 @@ describe Notify do
end
describe 'that have been relabeled' do
- subject { Notify.relabeled_merge_request_email(recipient.id, merge_request.id, %w[foo bar baz], current_user.id) }
+ subject { described_class.relabeled_merge_request_email(recipient.id, merge_request.id, %w[foo bar baz], current_user.id) }
it_behaves_like 'a multiple recipients email'
it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
@@ -283,7 +283,7 @@ describe Notify do
describe 'status changed' do
let(:status) { 'reopened' }
- subject { Notify.merge_request_status_email(recipient.id, merge_request.id, status, current_user.id) }
+ subject { described_class.merge_request_status_email(recipient.id, merge_request.id, status, current_user.id) }
it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
let(:model) { merge_request }
@@ -308,7 +308,7 @@ describe Notify do
end
describe 'that are merged' do
- subject { Notify.merged_merge_request_email(recipient.id, merge_request.id, merge_author.id) }
+ subject { described_class.merged_merge_request_email(recipient.id, merge_request.id, merge_author.id) }
it_behaves_like 'a multiple recipients email'
it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
@@ -337,7 +337,7 @@ describe Notify do
describe 'project was moved' do
let(:project) { create(:empty_project) }
let(:user) { create(:user) }
- subject { Notify.project_was_moved_email(project.id, user.id, "gitlab/gitlab") }
+ subject { described_class.project_was_moved_email(project.id, user.id, "gitlab/gitlab") }
it_behaves_like 'an email sent from GitLab'
it_behaves_like 'it should not have Gmail Actions links'
@@ -363,7 +363,7 @@ describe Notify do
project.request_access(user)
project.requesters.find_by(user_id: user.id)
end
- subject { Notify.member_access_requested_email('project', project_member.id) }
+ subject { described_class.member_access_requested_email('project', project_member.id) }
it_behaves_like 'an email sent from GitLab'
it_behaves_like 'it should not have Gmail Actions links'
@@ -390,7 +390,7 @@ describe Notify do
project.request_access(user)
project.requesters.find_by(user_id: user.id)
end
- subject { Notify.member_access_requested_email('project', project_member.id) }
+ subject { described_class.member_access_requested_email('project', project_member.id) }
it_behaves_like 'an email sent from GitLab'
it_behaves_like 'it should not have Gmail Actions links'
@@ -416,7 +416,7 @@ describe Notify do
project.request_access(user)
project.requesters.find_by(user_id: user.id)
end
- subject { Notify.member_access_denied_email('project', project.id, user.id) }
+ subject { described_class.member_access_denied_email('project', project.id, user.id) }
it_behaves_like 'an email sent from GitLab'
it_behaves_like 'it should not have Gmail Actions links'
@@ -434,7 +434,7 @@ describe Notify do
let(:project) { create(:empty_project, :public, :access_requestable, namespace: owner.namespace) }
let(:user) { create(:user) }
let(:project_member) { create(:project_member, project: project, user: user) }
- subject { Notify.member_access_granted_email('project', project_member.id) }
+ subject { described_class.member_access_granted_email('project', project_member.id) }
it_behaves_like 'an email sent from GitLab'
it_behaves_like 'it should not have Gmail Actions links'
@@ -465,7 +465,7 @@ describe Notify do
let(:master) { create(:user).tap { |u| project.team << [u, :master] } }
let(:project_member) { invite_to_project(project, inviter: master) }
- subject { Notify.member_invited_email('project', project_member.id, project_member.invite_token) }
+ subject { described_class.member_invited_email('project', project_member.id, project_member.invite_token) }
it_behaves_like 'an email sent from GitLab'
it_behaves_like 'it should not have Gmail Actions links'
@@ -490,7 +490,7 @@ describe Notify do
invitee
end
- subject { Notify.member_invite_accepted_email('project', project_member.id) }
+ subject { described_class.member_invite_accepted_email('project', project_member.id) }
it_behaves_like 'an email sent from GitLab'
it_behaves_like 'it should not have Gmail Actions links'
@@ -514,7 +514,7 @@ describe Notify do
invitee
end
- subject { Notify.member_invite_declined_email('project', project.id, project_member.invite_email, master.id) }
+ subject { described_class.member_invite_declined_email('project', project.id, project_member.invite_email, master.id) }
it_behaves_like 'an email sent from GitLab'
it_behaves_like 'it should not have Gmail Actions links'
@@ -574,7 +574,7 @@ describe Notify do
before(:each) { allow(note).to receive(:noteable).and_return(commit) }
- subject { Notify.note_commit_email(recipient.id, note.id) }
+ subject { described_class.note_commit_email(recipient.id, note.id) }
it_behaves_like 'a note email'
it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
@@ -596,7 +596,7 @@ describe Notify do
let(:note_on_merge_request_path) { namespace_project_merge_request_path(project.namespace, project, merge_request, anchor: "note_#{note.id}") }
before(:each) { allow(note).to receive(:noteable).and_return(merge_request) }
- subject { Notify.note_merge_request_email(recipient.id, note.id) }
+ subject { described_class.note_merge_request_email(recipient.id, note.id) }
it_behaves_like 'a note email'
it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
@@ -618,7 +618,7 @@ describe Notify do
let(:note_on_issue_path) { namespace_project_issue_path(project.namespace, project, issue, anchor: "note_#{note.id}") }
before(:each) { allow(note).to receive(:noteable).and_return(issue) }
- subject { Notify.note_issue_email(recipient.id, note.id) }
+ subject { described_class.note_issue_email(recipient.id, note.id) }
it_behaves_like 'a note email'
it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
@@ -680,7 +680,7 @@ describe Notify do
before(:each) { allow(note).to receive(:noteable).and_return(commit) }
- subject { Notify.note_commit_email(recipient.id, note.id) }
+ subject { described_class.note_commit_email(recipient.id, note.id) }
it_behaves_like 'a discussion note email', :discussion_note_on_commit
it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
@@ -704,7 +704,7 @@ describe Notify do
let(:note_on_merge_request_path) { namespace_project_merge_request_path(project.namespace, project, merge_request, anchor: "note_#{note.id}") }
before(:each) { allow(note).to receive(:noteable).and_return(merge_request) }
- subject { Notify.note_merge_request_email(recipient.id, note.id) }
+ subject { described_class.note_merge_request_email(recipient.id, note.id) }
it_behaves_like 'a discussion note email', :discussion_note_on_merge_request
it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
@@ -728,7 +728,7 @@ describe Notify do
let(:note_on_issue_path) { namespace_project_issue_path(project.namespace, project, issue, anchor: "note_#{note.id}") }
before(:each) { allow(note).to receive(:noteable).and_return(issue) }
- subject { Notify.note_issue_email(recipient.id, note.id) }
+ subject { described_class.note_issue_email(recipient.id, note.id) }
it_behaves_like 'a discussion note email', :discussion_note_on_issue
it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
@@ -798,7 +798,7 @@ describe Notify do
let(:commit) { project.commit }
let(:note) { create(:diff_note_on_commit) }
- subject { Notify.note_commit_email(recipient.id, note.id) }
+ subject { described_class.note_commit_email(recipient.id, note.id) }
it_behaves_like 'an email for a note on a diff discussion', :diff_note_on_commit
it_behaves_like 'it should show Gmail Actions View Commit link'
@@ -809,7 +809,7 @@ describe Notify do
let(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
let(:note) { create(:diff_note_on_merge_request) }
- subject { Notify.note_merge_request_email(recipient.id, note.id) }
+ subject { described_class.note_merge_request_email(recipient.id, note.id) }
it_behaves_like 'an email for a note on a diff discussion', :diff_note_on_merge_request
it_behaves_like 'it should show Gmail Actions View Merge request link'
@@ -826,7 +826,7 @@ describe Notify do
group.request_access(user)
group.requesters.find_by(user_id: user.id)
end
- subject { Notify.member_access_requested_email('group', group_member.id) }
+ subject { described_class.member_access_requested_email('group', group_member.id) }
it_behaves_like 'an email sent from GitLab'
it_behaves_like 'it should not have Gmail Actions links'
@@ -847,7 +847,7 @@ describe Notify do
group.request_access(user)
group.requesters.find_by(user_id: user.id)
end
- subject { Notify.member_access_denied_email('group', group.id, user.id) }
+ subject { described_class.member_access_denied_email('group', group.id, user.id) }
it_behaves_like 'an email sent from GitLab'
it_behaves_like 'it should not have Gmail Actions links'
@@ -865,7 +865,7 @@ describe Notify do
let(:user) { create(:user) }
let(:group_member) { create(:group_member, group: group, user: user) }
- subject { Notify.member_access_granted_email('group', group_member.id) }
+ subject { described_class.member_access_granted_email('group', group_member.id) }
it_behaves_like 'an email sent from GitLab'
it_behaves_like 'it should not have Gmail Actions links'
@@ -896,7 +896,7 @@ describe Notify do
let(:owner) { create(:user).tap { |u| group.add_user(u, Gitlab::Access::OWNER) } }
let(:group_member) { invite_to_group(group, inviter: owner) }
- subject { Notify.member_invited_email('group', group_member.id, group_member.invite_token) }
+ subject { described_class.member_invited_email('group', group_member.id, group_member.invite_token) }
it_behaves_like 'an email sent from GitLab'
it_behaves_like 'it should not have Gmail Actions links'
@@ -921,7 +921,7 @@ describe Notify do
invitee
end
- subject { Notify.member_invite_accepted_email('group', group_member.id) }
+ subject { described_class.member_invite_accepted_email('group', group_member.id) }
it_behaves_like 'an email sent from GitLab'
it_behaves_like 'it should not have Gmail Actions links'
@@ -945,7 +945,7 @@ describe Notify do
invitee
end
- subject { Notify.member_invite_declined_email('group', group.id, group_member.invite_email, owner.id) }
+ subject { described_class.member_invite_declined_email('group', group.id, group_member.invite_email, owner.id) }
it_behaves_like 'an email sent from GitLab'
it_behaves_like 'it should not have Gmail Actions links'
@@ -994,7 +994,7 @@ describe Notify do
let(:user) { create(:user) }
let(:tree_path) { namespace_project_tree_path(project.namespace, project, "empty-branch") }
- subject { Notify.repository_push_email(project.id, author_id: user.id, ref: 'refs/heads/empty-branch', action: :create) }
+ subject { described_class.repository_push_email(project.id, author_id: user.id, ref: 'refs/heads/empty-branch', action: :create) }
it_behaves_like 'it should not have Gmail Actions links'
it_behaves_like 'a user cannot unsubscribe through footer link'
@@ -1020,7 +1020,7 @@ describe Notify do
let(:user) { create(:user) }
let(:tree_path) { namespace_project_tree_path(project.namespace, project, "v1.0") }
- subject { Notify.repository_push_email(project.id, author_id: user.id, ref: 'refs/tags/v1.0', action: :create) }
+ subject { described_class.repository_push_email(project.id, author_id: user.id, ref: 'refs/tags/v1.0', action: :create) }
it_behaves_like 'it should not have Gmail Actions links'
it_behaves_like "a user cannot unsubscribe through footer link"
@@ -1045,7 +1045,7 @@ describe Notify do
let(:example_site_path) { root_path }
let(:user) { create(:user) }
- subject { Notify.repository_push_email(project.id, author_id: user.id, ref: 'refs/heads/master', action: :delete) }
+ subject { described_class.repository_push_email(project.id, author_id: user.id, ref: 'refs/heads/master', action: :delete) }
it_behaves_like 'it should not have Gmail Actions links'
it_behaves_like 'a user cannot unsubscribe through footer link'
@@ -1067,7 +1067,7 @@ describe Notify do
let(:example_site_path) { root_path }
let(:user) { create(:user) }
- subject { Notify.repository_push_email(project.id, author_id: user.id, ref: 'refs/tags/v1.0', action: :delete) }
+ subject { described_class.repository_push_email(project.id, author_id: user.id, ref: 'refs/tags/v1.0', action: :delete) }
it_behaves_like 'it should not have Gmail Actions links'
it_behaves_like 'a user cannot unsubscribe through footer link'
@@ -1096,7 +1096,7 @@ describe Notify do
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) }
- subject { Notify.repository_push_email(project.id, author_id: user.id, ref: 'refs/heads/master', action: :push, compare: compare, reverse_compare: false, diff_refs: diff_refs, send_from_committer_email: send_from_committer_email) }
+ subject { described_class.repository_push_email(project.id, author_id: user.id, ref: 'refs/heads/master', action: :push, compare: compare, reverse_compare: false, diff_refs: diff_refs, send_from_committer_email: send_from_committer_email) }
it_behaves_like 'it should not have Gmail Actions links'
it_behaves_like 'a user cannot unsubscribe through footer link'
@@ -1189,7 +1189,7 @@ describe Notify do
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) }
- subject { Notify.repository_push_email(project.id, author_id: user.id, ref: 'refs/heads/master', action: :push, compare: compare, diff_refs: diff_refs) }
+ subject { described_class.repository_push_email(project.id, author_id: user.id, ref: 'refs/heads/master', action: :push, compare: compare, diff_refs: diff_refs) }
it_behaves_like 'it should show Gmail Actions View Commit link'
it_behaves_like 'a user cannot unsubscribe through footer link'
@@ -1215,7 +1215,7 @@ describe Notify do
describe 'HTML emails setting' do
let(:project) { create(:empty_project) }
let(:user) { create(:user) }
- let(:multipart_mail) { Notify.project_was_moved_email(project.id, user.id, "gitlab/gitlab") }
+ let(:multipart_mail) { described_class.project_was_moved_email(project.id, user.id, "gitlab/gitlab") }
context 'when disabled' do
it 'only sends the text template' do
diff --git a/spec/migrations/schema_spec.rb b/spec/migrations/active_record/schema_spec.rb
index e132529d8d8..e132529d8d8 100644
--- a/spec/migrations/schema_spec.rb
+++ b/spec/migrations/active_record/schema_spec.rb
diff --git a/spec/migrations/add_head_pipeline_for_each_merge_request_spec.rb b/spec/migrations/add_head_pipeline_for_each_merge_request_spec.rb
new file mode 100644
index 00000000000..bd5f85b901d
--- /dev/null
+++ b/spec/migrations/add_head_pipeline_for_each_merge_request_spec.rb
@@ -0,0 +1,33 @@
+require 'spec_helper'
+require Rails.root.join('db', 'post_migrate', '20170508170547_add_head_pipeline_for_each_merge_request.rb')
+
+describe AddHeadPipelineForEachMergeRequest do
+ let(:migration) { described_class.new }
+
+ let!(:project) { create(:empty_project) }
+ let!(:forked_project_link) { create(:forked_project_link, forked_from_project: project) }
+ let!(:other_project) { forked_project_link.forked_to_project }
+
+ let!(:pipeline_1) { create(:ci_pipeline, project: project, ref: "branch_1") }
+ let!(:pipeline_2) { create(:ci_pipeline, project: other_project, ref: "branch_1") }
+ let!(:pipeline_3) { create(:ci_pipeline, project: other_project, ref: "branch_1") }
+ let!(:pipeline_4) { create(:ci_pipeline, project: project, ref: "branch_2") }
+
+ let!(:mr_1) { create(:merge_request, source_project: project, target_project: project, source_branch: "branch_1", target_branch: "target_1") }
+ let!(:mr_2) { create(:merge_request, source_project: other_project, target_project: project, source_branch: "branch_1", target_branch: "target_2") }
+ let!(:mr_3) { create(:merge_request, source_project: project, target_project: project, source_branch: "branch_2", target_branch: "master") }
+ let!(:mr_4) { create(:merge_request, source_project: project, target_project: project, source_branch: "branch_3", target_branch: "master") }
+
+ context "#up" do
+ context "when source_project and source_branch of pipeline are the same of merge request" do
+ it "sets head_pipeline_id of given merge requests" do
+ migration.up
+
+ expect(mr_1.reload.head_pipeline_id).to eq(pipeline_1.id)
+ expect(mr_2.reload.head_pipeline_id).to eq(pipeline_3.id)
+ expect(mr_3.reload.head_pipeline_id).to eq(pipeline_4.id)
+ expect(mr_4.reload.head_pipeline_id).to be_nil
+ end
+ end
+ end
+end
diff --git a/spec/migrations/cleanup_namespaceless_pending_delete_projects_spec.rb b/spec/migrations/cleanup_namespaceless_pending_delete_projects_spec.rb
new file mode 100644
index 00000000000..49e750a3f4d
--- /dev/null
+++ b/spec/migrations/cleanup_namespaceless_pending_delete_projects_spec.rb
@@ -0,0 +1,32 @@
+require 'spec_helper'
+require Rails.root.join('db', 'post_migrate', '20170502101023_cleanup_namespaceless_pending_delete_projects.rb')
+
+describe CleanupNamespacelessPendingDeleteProjects do
+ before do
+ # Stub after_save callbacks that will fail when Project has no namespace
+ allow_any_instance_of(Project).to receive(:ensure_dir_exist).and_return(nil)
+ allow_any_instance_of(Project).to receive(:update_project_statistics).and_return(nil)
+ end
+
+ describe '#up' do
+ it 'only cleans up pending delete projects' do
+ create(:empty_project)
+ create(:empty_project, pending_delete: true)
+ project = build(:empty_project, pending_delete: true, namespace_id: nil)
+ project.save(validate: false)
+
+ expect(NamespacelessProjectDestroyWorker).to receive(:bulk_perform_async).with([[project.id]])
+
+ described_class.new.up
+ end
+
+ it 'does nothing when no pending delete projects without namespace found' do
+ create(:empty_project)
+ create(:empty_project, pending_delete: true)
+
+ expect(NamespacelessProjectDestroyWorker).not_to receive(:bulk_perform_async)
+
+ described_class.new.up
+ end
+ end
+end
diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb
index 01ca1584ed2..fa229542f70 100644
--- a/spec/models/application_setting_spec.rb
+++ b/spec/models/application_setting_spec.rb
@@ -4,6 +4,7 @@ describe ApplicationSetting, models: true do
let(:setting) { ApplicationSetting.create_from_defaults }
it { expect(setting).to be_valid }
+ it { expect(setting.uuid).to be_present }
describe 'validations' do
let(:http) { 'http://example.com' }
@@ -88,7 +89,7 @@ describe ApplicationSetting, models: true do
storages = {
'custom1' => 'tmp/tests/custom_repositories_1',
'custom2' => 'tmp/tests/custom_repositories_2',
- 'custom3' => 'tmp/tests/custom_repositories_3',
+ 'custom3' => 'tmp/tests/custom_repositories_3'
}
allow(Gitlab.config.repositories).to receive(:storages).and_return(storages)
@@ -210,4 +211,66 @@ describe ApplicationSetting, models: true do
expect(setting.domain_blacklist).to contain_exactly('example.com', 'test.com', 'foo.bar')
end
end
+
+ describe 'usage ping settings' do
+ context 'when the usage ping is disabled in gitlab.yml' do
+ before do
+ allow(Settings.gitlab).to receive(:usage_ping_enabled).and_return(false)
+ end
+
+ it 'does not allow the usage ping to be configured' do
+ expect(setting.usage_ping_can_be_configured?).to be_falsey
+ end
+
+ context 'when the usage ping is disabled in the DB' do
+ before do
+ setting.usage_ping_enabled = false
+ end
+
+ it 'returns false for usage_ping_enabled' do
+ expect(setting.usage_ping_enabled).to be_falsey
+ end
+ end
+
+ context 'when the usage ping is enabled in the DB' do
+ before do
+ setting.usage_ping_enabled = true
+ end
+
+ it 'returns false for usage_ping_enabled' do
+ expect(setting.usage_ping_enabled).to be_falsey
+ end
+ end
+ end
+
+ context 'when the usage ping is enabled in gitlab.yml' do
+ before do
+ allow(Settings.gitlab).to receive(:usage_ping_enabled).and_return(true)
+ end
+
+ it 'allows the usage ping to be configured' do
+ expect(setting.usage_ping_can_be_configured?).to be_truthy
+ end
+
+ context 'when the usage ping is disabled in the DB' do
+ before do
+ setting.usage_ping_enabled = false
+ end
+
+ it 'returns false for usage_ping_enabled' do
+ expect(setting.usage_ping_enabled).to be_falsey
+ end
+ end
+
+ context 'when the usage ping is enabled in the DB' do
+ before do
+ setting.usage_ping_enabled = true
+ end
+
+ it 'returns true for usage_ping_enabled' do
+ expect(setting.usage_ping_enabled).to be_truthy
+ end
+ end
+ end
+ end
end
diff --git a/spec/models/blob_spec.rb b/spec/models/blob_spec.rb
index e5dd57fc4bb..f19e1af65a6 100644
--- a/spec/models/blob_spec.rb
+++ b/spec/models/blob_spec.rb
@@ -2,6 +2,14 @@
require 'rails_helper'
describe Blob do
+ include FakeBlobHelpers
+
+ let(:project) { build(:empty_project, lfs_enabled: true) }
+
+ before do
+ allow(Gitlab.config.lfs).to receive(:enabled).and_return(true)
+ end
+
describe '.decorate' do
it 'returns NilClass when given nil' do
expect(described_class.decorate(nil)).to be_nil
@@ -12,7 +20,7 @@ describe Blob do
context 'using a binary blob' do
it 'returns the data as-is' do
data = "\n\xFF\xB9\xC3"
- blob = described_class.new(double(binary?: true, data: data))
+ blob = fake_blob(binary: true, data: data)
expect(blob.data).to eq(data)
end
@@ -20,202 +28,330 @@ describe Blob do
context 'using a text blob' do
it 'converts the data to UTF-8' do
- blob = described_class.new(double(binary?: false, data: "\n\xFF\xB9\xC3"))
+ blob = fake_blob(binary: false, data: "\n\xFF\xB9\xC3")
expect(blob.data).to eq("\n���")
end
end
end
- describe '#svg?' do
- it 'is falsey when not text' do
- git_blob = double(text?: false)
+ describe '#external_storage_error?' do
+ context 'if the blob is stored in LFS' do
+ let(:blob) { fake_blob(path: 'file.pdf', lfs: true) }
- expect(described_class.decorate(git_blob)).not_to be_svg
- end
+ context 'when the project has LFS enabled' do
+ it 'returns false' do
+ expect(blob.external_storage_error?).to be_falsey
+ end
+ end
- it 'is falsey when no language is detected' do
- git_blob = double(text?: true, language: nil)
+ context 'when the project does not have LFS enabled' do
+ before do
+ project.lfs_enabled = false
+ end
- expect(described_class.decorate(git_blob)).not_to be_svg
+ it 'returns true' do
+ expect(blob.external_storage_error?).to be_truthy
+ end
+ end
end
- it' is falsey when language is not SVG' do
- git_blob = double(text?: true, language: double(name: 'XML'))
+ context 'if the blob is not stored in LFS' do
+ let(:blob) { fake_blob(path: 'file.md') }
- expect(described_class.decorate(git_blob)).not_to be_svg
+ it 'returns false' do
+ expect(blob.external_storage_error?).to be_falsey
+ end
end
+ end
- it 'is truthy when language is SVG' do
- git_blob = double(text?: true, language: double(name: 'SVG'))
+ describe '#stored_externally?' do
+ context 'if the blob is stored in LFS' do
+ let(:blob) { fake_blob(path: 'file.pdf', lfs: true) }
- expect(described_class.decorate(git_blob)).to be_svg
- end
- end
+ context 'when the project has LFS enabled' do
+ it 'returns true' do
+ expect(blob.stored_externally?).to be_truthy
+ end
+ end
- describe '#pdf?' do
- it 'is falsey when file extension is not .pdf' do
- git_blob = Gitlab::Git::Blob.new(name: 'git_blob.txt')
+ context 'when the project does not have LFS enabled' do
+ before do
+ project.lfs_enabled = false
+ end
- expect(described_class.decorate(git_blob)).not_to be_pdf
+ it 'returns false' do
+ expect(blob.stored_externally?).to be_falsey
+ end
+ end
end
- it 'is truthy when file extension is .pdf' do
- git_blob = Gitlab::Git::Blob.new(name: 'git_blob.pdf')
+ context 'if the blob is not stored in LFS' do
+ let(:blob) { fake_blob(path: 'file.md') }
- expect(described_class.decorate(git_blob)).to be_pdf
+ it 'returns false' do
+ expect(blob.stored_externally?).to be_falsey
+ end
end
end
- describe '#ipython_notebook?' do
- it 'is falsey when language is not Jupyter Notebook' do
- git_blob = double(text?: true, language: double(name: 'JSON'))
+ describe '#raw_binary?' do
+ context 'if the blob is stored externally' do
+ context 'if the extension has a rich viewer' do
+ context 'if the viewer is binary' do
+ it 'returns true' do
+ blob = fake_blob(path: 'file.pdf', lfs: true)
- expect(described_class.decorate(git_blob)).not_to be_ipython_notebook
- end
+ expect(blob.raw_binary?).to be_truthy
+ end
+ end
+
+ context 'if the viewer is text-based' do
+ it 'return false' do
+ blob = fake_blob(path: 'file.md', lfs: true)
- it 'is truthy when language is Jupyter Notebook' do
- git_blob = double(text?: true, language: double(name: 'Jupyter Notebook'))
+ expect(blob.raw_binary?).to be_falsey
+ end
+ end
+ end
- expect(described_class.decorate(git_blob)).to be_ipython_notebook
+ context "if the extension doesn't have a rich viewer" do
+ context 'if the extension has a text mime type' do
+ context 'if the extension is for a programming language' do
+ it 'returns false' do
+ blob = fake_blob(path: 'file.txt', lfs: true)
+
+ expect(blob.raw_binary?).to be_falsey
+ end
+ end
+
+ context 'if the extension is not for a programming language' do
+ it 'returns false' do
+ blob = fake_blob(path: 'file.ics', lfs: true)
+
+ expect(blob.raw_binary?).to be_falsey
+ end
+ end
+ end
+
+ context 'if the extension has a binary mime type' do
+ context 'if the extension is for a programming language' do
+ it 'returns false' do
+ blob = fake_blob(path: 'file.rb', lfs: true)
+
+ expect(blob.raw_binary?).to be_falsey
+ end
+ end
+
+ context 'if the extension is not for a programming language' do
+ it 'returns true' do
+ blob = fake_blob(path: 'file.exe', lfs: true)
+
+ expect(blob.raw_binary?).to be_truthy
+ end
+ end
+ end
+
+ context 'if the extension has an unknown mime type' do
+ context 'if the extension is for a programming language' do
+ it 'returns false' do
+ blob = fake_blob(path: 'file.ini', lfs: true)
+
+ expect(blob.raw_binary?).to be_falsey
+ end
+ end
+
+ context 'if the extension is not for a programming language' do
+ it 'returns true' do
+ blob = fake_blob(path: 'file.wtf', lfs: true)
+
+ expect(blob.raw_binary?).to be_truthy
+ end
+ end
+ end
+ end
end
- end
- describe '#sketch?' do
- it 'is falsey with image extension' do
- git_blob = Gitlab::Git::Blob.new(name: "design.png")
+ context 'if the blob is not stored externally' do
+ context 'if the blob is binary' do
+ it 'returns true' do
+ blob = fake_blob(path: 'file.pdf', binary: true)
- expect(described_class.decorate(git_blob)).not_to be_sketch
- end
+ expect(blob.raw_binary?).to be_truthy
+ end
+ end
- it 'is truthy with sketch extension' do
- git_blob = Gitlab::Git::Blob.new(name: "design.sketch")
+ context 'if the blob is text-based' do
+ it 'return false' do
+ blob = fake_blob(path: 'file.md')
- expect(described_class.decorate(git_blob)).to be_sketch
+ expect(blob.raw_binary?).to be_falsey
+ end
+ end
end
end
- describe '#video?' do
- it 'is falsey with image extension' do
- git_blob = Gitlab::Git::Blob.new(name: 'image.png')
+ describe '#extension' do
+ it 'returns the extension' do
+ blob = fake_blob(path: 'file.md')
- expect(described_class.decorate(git_blob)).not_to be_video
+ expect(blob.extension).to eq('md')
end
+ end
- UploaderHelper::VIDEO_EXT.each do |ext|
- it "is truthy when extension is .#{ext}" do
- git_blob = Gitlab::Git::Blob.new(name: "video.#{ext}")
+ describe '#simple_viewer' do
+ context 'when the blob is empty' do
+ it 'returns an empty viewer' do
+ blob = fake_blob(data: '', size: 0)
- expect(described_class.decorate(git_blob)).to be_video
+ expect(blob.simple_viewer).to be_a(BlobViewer::Empty)
end
end
- end
- describe '#stl?' do
- it 'is falsey with image extension' do
- git_blob = Gitlab::Git::Blob.new(name: 'file.png')
+ context 'when the file represented by the blob is binary' do
+ it 'returns a download viewer' do
+ blob = fake_blob(binary: true)
- expect(described_class.decorate(git_blob)).not_to be_stl
+ expect(blob.simple_viewer).to be_a(BlobViewer::Download)
+ end
end
- it 'is truthy with STL extension' do
- git_blob = Gitlab::Git::Blob.new(name: 'file.stl')
+ context 'when the file represented by the blob is text-based' do
+ it 'returns a text viewer' do
+ blob = fake_blob
- expect(described_class.decorate(git_blob)).to be_stl
+ expect(blob.simple_viewer).to be_a(BlobViewer::Text)
+ end
end
end
- describe '#to_partial_path' do
- let(:project) { double(lfs_enabled?: true) }
+ describe '#rich_viewer' do
+ context 'when the blob has an external storage error' do
+ before do
+ project.lfs_enabled = false
+ end
- def stubbed_blob(overrides = {})
- overrides.reverse_merge!(
- name: nil,
- image?: false,
- language: nil,
- lfs_pointer?: false,
- svg?: false,
- text?: false,
- binary?: false,
- stl?: false
- )
+ it 'returns nil' do
+ blob = fake_blob(path: 'file.pdf', lfs: true)
- described_class.decorate(Gitlab::Git::Blob.new({})).tap do |blob|
- allow(blob).to receive_messages(overrides)
+ expect(blob.rich_viewer).to be_nil
end
end
- it 'handles LFS pointers with LFS enabled' do
- blob = stubbed_blob(lfs_pointer?: true, text?: true)
- expect(blob.to_partial_path(project)).to eq 'download'
- end
+ context 'when the blob is empty' do
+ it 'returns nil' do
+ blob = fake_blob(data: '')
- it 'handles LFS pointers with LFS disabled' do
- blob = stubbed_blob(lfs_pointer?: true, text?: true)
- project = double(lfs_enabled?: false)
- expect(blob.to_partial_path(project)).to eq 'text'
+ expect(blob.rich_viewer).to be_nil
+ end
end
- it 'handles SVGs' do
- blob = stubbed_blob(text?: true, svg?: true)
- expect(blob.to_partial_path(project)).to eq 'svg'
+ context 'when the blob is stored externally' do
+ it 'returns a matching viewer' do
+ blob = fake_blob(path: 'file.pdf', lfs: true)
+
+ expect(blob.rich_viewer).to be_a(BlobViewer::PDF)
+ end
end
- it 'handles images' do
- blob = stubbed_blob(image?: true)
- expect(blob.to_partial_path(project)).to eq 'image'
+ context 'when the blob is binary' do
+ it 'returns a matching binary viewer' do
+ blob = fake_blob(path: 'file.pdf', binary: true)
+
+ expect(blob.rich_viewer).to be_a(BlobViewer::PDF)
+ end
end
- it 'handles text' do
- blob = stubbed_blob(text?: true, name: 'test.txt')
- expect(blob.to_partial_path(project)).to eq 'text'
+ context 'when the blob is text-based' do
+ it 'returns a matching text-based viewer' do
+ blob = fake_blob(path: 'file.md')
+
+ expect(blob.rich_viewer).to be_a(BlobViewer::Markup)
+ end
end
+ end
+
+ describe '#auxiliary_viewer' do
+ context 'when the blob has an external storage error' do
+ before do
+ project.lfs_enabled = false
+ end
- it 'defaults to download' do
- blob = stubbed_blob
- expect(blob.to_partial_path(project)).to eq 'download'
+ it 'returns nil' do
+ blob = fake_blob(path: 'LICENSE', lfs: true)
+
+ expect(blob.auxiliary_viewer).to be_nil
+ end
end
- it 'handles PDFs' do
- blob = stubbed_blob(name: 'blob.pdf', pdf?: true)
- expect(blob.to_partial_path(project)).to eq 'pdf'
+ context 'when the blob is empty' do
+ it 'returns nil' do
+ blob = fake_blob(data: '')
+
+ expect(blob.auxiliary_viewer).to be_nil
+ end
end
- it 'handles iPython notebooks' do
- blob = stubbed_blob(text?: true, ipython_notebook?: true)
- expect(blob.to_partial_path(project)).to eq 'notebook'
+ context 'when the blob is stored externally' do
+ it 'returns a matching viewer' do
+ blob = fake_blob(path: 'LICENSE', lfs: true)
+
+ expect(blob.auxiliary_viewer).to be_a(BlobViewer::License)
+ end
end
- it 'handles Sketch files' do
- blob = stubbed_blob(text?: true, sketch?: true, binary?: true)
- expect(blob.to_partial_path(project)).to eq 'sketch'
+ context 'when the blob is binary' do
+ it 'returns nil' do
+ blob = fake_blob(path: 'LICENSE', binary: true)
+
+ expect(blob.auxiliary_viewer).to be_nil
+ end
end
- it 'handles STLs' do
- blob = stubbed_blob(text?: true, stl?: true)
- expect(blob.to_partial_path(project)).to eq 'stl'
+ context 'when the blob is text-based' do
+ it 'returns a matching text-based viewer' do
+ blob = fake_blob(path: 'LICENSE')
+
+ expect(blob.auxiliary_viewer).to be_a(BlobViewer::License)
+ end
end
end
- describe '#size_within_svg_limits?' do
- let(:blob) { described_class.decorate(double(:blob)) }
+ describe '#rendered_as_text?' do
+ context 'when ignoring errors' do
+ context 'when the simple viewer is text-based' do
+ it 'returns true' do
+ blob = fake_blob(path: 'file.md', size: 100.megabytes)
- it 'returns true when the blob size is smaller than the SVG limit' do
- expect(blob).to receive(:size).and_return(42)
+ expect(blob.rendered_as_text?).to be_truthy
+ end
+ end
+
+ context 'when the simple viewer is binary' do
+ it 'returns false' do
+ blob = fake_blob(path: 'file.pdf', binary: true, size: 100.megabytes)
- expect(blob.size_within_svg_limits?).to eq(true)
+ expect(blob.rendered_as_text?).to be_falsey
+ end
+ end
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)
+ context 'when not ignoring errors' do
+ context 'when the viewer has render errors' do
+ it 'returns false' do
+ blob = fake_blob(path: 'file.md', size: 100.megabytes)
- expect(blob.size_within_svg_limits?).to eq(true)
- end
+ expect(blob.rendered_as_text?(ignore_errors: false)).to be_falsey
+ end
+ end
- it 'returns false when the blob size is larger than the SVG limit' do
- expect(blob).to receive(:size).and_return(1.terabyte)
+ context "when the viewer doesn't have render errors" do
+ it 'returns true' do
+ blob = fake_blob(path: 'file.md')
- expect(blob.size_within_svg_limits?).to eq(false)
+ expect(blob.rendered_as_text?(ignore_errors: false)).to be_truthy
+ end
+ end
end
end
end
diff --git a/spec/models/blob_viewer/base_spec.rb b/spec/models/blob_viewer/base_spec.rb
new file mode 100644
index 00000000000..a6641970e1b
--- /dev/null
+++ b/spec/models/blob_viewer/base_spec.rb
@@ -0,0 +1,190 @@
+require 'spec_helper'
+
+describe BlobViewer::Base, model: true do
+ include FakeBlobHelpers
+
+ let(:project) { build(:empty_project) }
+
+ let(:viewer_class) do
+ Class.new(described_class) do
+ self.extensions = %w(pdf)
+ self.binary = true
+ self.max_size = 1.megabyte
+ self.absolute_max_size = 5.megabytes
+ self.client_side = false
+ end
+ end
+
+ let(:viewer) { viewer_class.new(blob) }
+
+ describe '.can_render?' do
+ context 'when the extension is supported' do
+ context 'when the binaryness matches' do
+ let(:blob) { fake_blob(path: 'file.pdf', binary: true) }
+
+ it 'returns true' do
+ expect(viewer_class.can_render?(blob)).to be_truthy
+ end
+ end
+
+ context 'when the binaryness does not match' do
+ let(:blob) { fake_blob(path: 'file.pdf', binary: false) }
+
+ it 'returns false' do
+ expect(viewer_class.can_render?(blob)).to be_falsey
+ end
+ end
+ end
+
+ context 'when the file type is supported' do
+ before do
+ viewer_class.file_type = :license
+ viewer_class.binary = false
+ end
+
+ context 'when the binaryness matches' do
+ let(:blob) { fake_blob(path: 'LICENSE', binary: false) }
+
+ it 'returns true' do
+ expect(viewer_class.can_render?(blob)).to be_truthy
+ end
+ end
+
+ context 'when the binaryness does not match' do
+ let(:blob) { fake_blob(path: 'LICENSE', binary: true) }
+
+ it 'returns false' do
+ expect(viewer_class.can_render?(blob)).to be_falsey
+ end
+ end
+ end
+
+ context 'when the extension and file type are not supported' do
+ let(:blob) { fake_blob(path: 'file.txt') }
+
+ it 'returns false' do
+ expect(viewer_class.can_render?(blob)).to be_falsey
+ end
+ end
+ end
+
+ describe '#too_large?' do
+ context 'when the blob size is larger than the max size' do
+ let(:blob) { fake_blob(path: 'file.pdf', size: 2.megabytes) }
+
+ it 'returns true' do
+ expect(viewer.too_large?).to be_truthy
+ end
+ end
+
+ context 'when the blob size is smaller than the max size' do
+ let(:blob) { fake_blob(path: 'file.pdf', size: 10.kilobytes) }
+
+ it 'returns false' do
+ expect(viewer.too_large?).to be_falsey
+ end
+ end
+ end
+
+ describe '#absolutely_too_large?' do
+ context 'when the blob size is larger than the absolute max size' do
+ let(:blob) { fake_blob(path: 'file.pdf', size: 10.megabytes) }
+
+ it 'returns true' do
+ expect(viewer.absolutely_too_large?).to be_truthy
+ end
+ end
+
+ context 'when the blob size is smaller than the absolute max size' do
+ let(:blob) { fake_blob(path: 'file.pdf', size: 2.megabytes) }
+
+ it 'returns false' do
+ expect(viewer.absolutely_too_large?).to be_falsey
+ end
+ end
+ end
+
+ describe '#can_override_max_size?' do
+ context 'when the blob size is larger than the max size' do
+ context 'when the blob size is larger than the absolute max size' do
+ let(:blob) { fake_blob(path: 'file.pdf', size: 10.megabytes) }
+
+ it 'returns false' do
+ expect(viewer.can_override_max_size?).to be_falsey
+ end
+ end
+
+ context 'when the blob size is smaller than the absolute max size' do
+ let(:blob) { fake_blob(path: 'file.pdf', size: 2.megabytes) }
+
+ it 'returns true' do
+ expect(viewer.can_override_max_size?).to be_truthy
+ end
+ end
+ end
+
+ context 'when the blob size is smaller than the max size' do
+ let(:blob) { fake_blob(path: 'file.pdf', size: 10.kilobytes) }
+
+ it 'returns false' do
+ expect(viewer.can_override_max_size?).to be_falsey
+ end
+ end
+ end
+
+ describe '#render_error' do
+ context 'when the max size is overridden' do
+ before do
+ viewer.override_max_size = true
+ end
+
+ context 'when the blob size is larger than the absolute max size' do
+ let(:blob) { fake_blob(path: 'file.pdf', size: 10.megabytes) }
+
+ it 'returns :too_large' do
+ expect(viewer.render_error).to eq(:too_large)
+ end
+ end
+
+ context 'when the blob size is smaller than the absolute max size' do
+ let(:blob) { fake_blob(path: 'file.pdf', size: 2.megabytes) }
+
+ it 'returns nil' do
+ expect(viewer.render_error).to be_nil
+ end
+ end
+ end
+
+ context 'when the max size is not overridden' do
+ context 'when the blob size is larger than the max size' do
+ let(:blob) { fake_blob(path: 'file.pdf', size: 2.megabytes) }
+
+ it 'returns :too_large' do
+ expect(viewer.render_error).to eq(:too_large)
+ end
+ end
+
+ context 'when the blob size is smaller than the max size' do
+ let(:blob) { fake_blob(path: 'file.pdf', size: 10.kilobytes) }
+
+ it 'returns nil' do
+ expect(viewer.render_error).to be_nil
+ end
+ end
+ end
+
+ context 'when the viewer is server side but the blob is stored externally' do
+ let(:project) { build(:empty_project, lfs_enabled: true) }
+
+ let(:blob) { fake_blob(path: 'file.pdf', lfs: true) }
+
+ before do
+ allow(Gitlab.config.lfs).to receive(:enabled).and_return(true)
+ end
+
+ it 'return :server_side_but_stored_externally' do
+ expect(viewer.render_error).to eq(:server_side_but_stored_externally)
+ end
+ end
+ end
+end
diff --git a/spec/models/blob_viewer/gitlab_ci_yml_spec.rb b/spec/models/blob_viewer/gitlab_ci_yml_spec.rb
new file mode 100644
index 00000000000..0c6c24ece21
--- /dev/null
+++ b/spec/models/blob_viewer/gitlab_ci_yml_spec.rb
@@ -0,0 +1,32 @@
+require 'spec_helper'
+
+describe BlobViewer::GitlabCiYml, model: true do
+ include FakeBlobHelpers
+
+ let(:project) { build(:project) }
+ let(:data) { File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml')) }
+ let(:blob) { fake_blob(path: '.gitlab-ci.yml', data: data) }
+ subject { described_class.new(blob) }
+
+ describe '#validation_message' do
+ it 'calls prepare! on the viewer' do
+ expect(subject).to receive(:prepare!)
+
+ subject.validation_message
+ end
+
+ context 'when the configuration is valid' do
+ it 'returns nil' do
+ expect(subject.validation_message).to be_nil
+ end
+ end
+
+ context 'when the configuration is invalid' do
+ let(:data) { 'oof' }
+
+ it 'returns the error message' do
+ expect(subject.validation_message).to eq('Invalid configuration format')
+ end
+ end
+ end
+end
diff --git a/spec/models/blob_viewer/license_spec.rb b/spec/models/blob_viewer/license_spec.rb
new file mode 100644
index 00000000000..944ddd32b92
--- /dev/null
+++ b/spec/models/blob_viewer/license_spec.rb
@@ -0,0 +1,34 @@
+require 'spec_helper'
+
+describe BlobViewer::License, model: true do
+ include FakeBlobHelpers
+
+ let(:project) { create(:project, :repository) }
+ let(:blob) { fake_blob(path: 'LICENSE') }
+ subject { described_class.new(blob) }
+
+ describe '#license' do
+ it 'returns the blob project repository license' do
+ expect(subject.license).not_to be_nil
+ expect(subject.license).to eq(project.repository.license)
+ end
+ end
+
+ describe '#render_error' do
+ context 'when there is no license' do
+ before do
+ allow(project.repository).to receive(:license).and_return(nil)
+ end
+
+ it 'returns :unknown_license' do
+ expect(subject.render_error).to eq(:unknown_license)
+ end
+ end
+
+ context 'when there is a license' do
+ it 'returns nil' do
+ expect(subject.render_error).to be_nil
+ end
+ end
+ end
+end
diff --git a/spec/models/blob_viewer/route_map_spec.rb b/spec/models/blob_viewer/route_map_spec.rb
new file mode 100644
index 00000000000..4854e0262d9
--- /dev/null
+++ b/spec/models/blob_viewer/route_map_spec.rb
@@ -0,0 +1,38 @@
+require 'spec_helper'
+
+describe BlobViewer::RouteMap, model: true do
+ include FakeBlobHelpers
+
+ let(:project) { build(:project) }
+ let(:data) do
+ <<-MAP.strip_heredoc
+ # Team data
+ - source: 'data/team.yml'
+ public: 'team/'
+ MAP
+ end
+ let(:blob) { fake_blob(path: '.gitlab/route-map.yml', data: data) }
+ subject { described_class.new(blob) }
+
+ describe '#validation_message' do
+ it 'calls prepare! on the viewer' do
+ expect(subject).to receive(:prepare!)
+
+ subject.validation_message
+ end
+
+ context 'when the configuration is valid' do
+ it 'returns nil' do
+ expect(subject.validation_message).to be_nil
+ end
+ end
+
+ context 'when the configuration is invalid' do
+ let(:data) { 'oof' }
+
+ it 'returns the error message' do
+ expect(subject.validation_message).to eq('Route map is not an array')
+ end
+ end
+ end
+end
diff --git a/spec/models/blob_viewer/server_side_spec.rb b/spec/models/blob_viewer/server_side_spec.rb
new file mode 100644
index 00000000000..ddca9b79390
--- /dev/null
+++ b/spec/models/blob_viewer/server_side_spec.rb
@@ -0,0 +1,25 @@
+require 'spec_helper'
+
+describe BlobViewer::ServerSide, model: true do
+ include FakeBlobHelpers
+
+ let(:project) { build(:empty_project) }
+
+ let(:viewer_class) do
+ Class.new(BlobViewer::Base) do
+ include BlobViewer::ServerSide
+ end
+ end
+
+ subject { viewer_class.new(blob) }
+
+ describe '#prepare!' do
+ let(:blob) { fake_blob(path: 'file.txt') }
+
+ it 'loads all blob data' do
+ expect(blob).to receive(:load_all_data!)
+
+ subject.prepare!
+ end
+ end
+end
diff --git a/spec/models/ci/artifact_blob_spec.rb b/spec/models/ci/artifact_blob_spec.rb
new file mode 100644
index 00000000000..968593d7e9b
--- /dev/null
+++ b/spec/models/ci/artifact_blob_spec.rb
@@ -0,0 +1,44 @@
+require 'spec_helper'
+
+describe Ci::ArtifactBlob, models: true do
+ let(:build) { create(:ci_build, :artifacts) }
+ let(:entry) { build.artifacts_metadata_entry('other_artifacts_0.1.2/another-subdirectory/banana_sample.gif') }
+
+ subject { described_class.new(entry) }
+
+ describe '#id' do
+ it 'returns a hash of the path' do
+ expect(subject.id).to eq(Digest::SHA1.hexdigest(entry.path))
+ end
+ end
+
+ describe '#name' do
+ it 'returns the entry name' do
+ expect(subject.name).to eq(entry.name)
+ end
+ end
+
+ describe '#path' do
+ it 'returns the entry path' do
+ expect(subject.path).to eq(entry.path)
+ end
+ end
+
+ describe '#size' do
+ it 'returns the entry size' do
+ expect(subject.size).to eq(entry.metadata[:size])
+ end
+ end
+
+ describe '#mode' do
+ it 'returns the entry mode' do
+ expect(subject.mode).to eq(entry.metadata[:mode])
+ end
+ end
+
+ describe '#external_storage' do
+ it 'returns :build_artifact' do
+ expect(subject.external_storage).to eq(:build_artifact)
+ end
+ end
+end
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
index 6e8845cdcf4..e971b4bc3f9 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/ci/build_spec.rb
@@ -897,22 +897,26 @@ describe Ci::Build, :models do
end
describe '#persisted_environment' do
- before do
- @environment = create(:environment, project: project, name: "foo-#{project.default_branch}")
+ let!(:environment) do
+ create(:environment, project: project, name: "foo-#{project.default_branch}")
end
subject { build.persisted_environment }
- context 'referenced literally' do
- let(:build) { create(:ci_build, pipeline: pipeline, environment: "foo-#{project.default_branch}") }
+ context 'when referenced literally' do
+ let(:build) do
+ create(:ci_build, pipeline: pipeline, environment: "foo-#{project.default_branch}")
+ end
- it { is_expected.to eq(@environment) }
+ it { is_expected.to eq(environment) }
end
- context 'referenced with a variable' do
- let(:build) { create(:ci_build, pipeline: pipeline, environment: "foo-$CI_COMMIT_REF_NAME") }
+ context 'when referenced with a variable' do
+ let(:build) do
+ create(:ci_build, pipeline: pipeline, environment: "foo-$CI_COMMIT_REF_NAME")
+ end
- it { is_expected.to eq(@environment) }
+ it { is_expected.to eq(environment) }
end
end
@@ -923,26 +927,8 @@ describe Ci::Build, :models do
project.add_developer(user)
end
- context 'when build is manual' do
- it 'enqueues a build' do
- new_build = build.play(user)
-
- expect(new_build).to be_pending
- expect(new_build).to eq(build)
- end
- end
-
- context 'when build is passed' do
- before do
- build.update(status: 'success')
- end
-
- it 'creates a new build' do
- new_build = build.play(user)
-
- expect(new_build).to be_pending
- expect(new_build).not_to eq(build)
- end
+ it 'enqueues the build' do
+ expect(build.play(user)).to be_pending
end
end
@@ -986,7 +972,7 @@ describe Ci::Build, :models do
'fix-1-foo' => 'fix-1-foo',
'a' * 63 => 'a' * 63,
'a' * 64 => 'a' * 63,
- 'FOO' => 'foo',
+ 'FOO' => 'foo'
}.each do |ref, slug|
it "transforms #{ref} to #{slug}" do
build.ref = ref
@@ -1158,7 +1144,7 @@ describe Ci::Build, :models do
{ key: 'CI_PIPELINE_ID', value: pipeline.id.to_s, public: true },
{ key: 'CI_REGISTRY_USER', value: 'gitlab-ci-token', public: true },
{ key: 'CI_REGISTRY_PASSWORD', value: build.token, public: false },
- { key: 'CI_REPOSITORY_URL', value: build.repo_url, public: false },
+ { key: 'CI_REPOSITORY_URL', value: build.repo_url, public: false }
]
end
diff --git a/spec/models/ci/group_spec.rb b/spec/models/ci/group_spec.rb
new file mode 100644
index 00000000000..62e15093089
--- /dev/null
+++ b/spec/models/ci/group_spec.rb
@@ -0,0 +1,44 @@
+require 'spec_helper'
+
+describe Ci::Group, models: true do
+ subject do
+ described_class.new('test', name: 'rspec', jobs: jobs)
+ end
+
+ let!(:jobs) { build_list(:ci_build, 1, :success) }
+
+ it { is_expected.to include_module(StaticModel) }
+
+ it { is_expected.to respond_to(:stage) }
+ it { is_expected.to respond_to(:name) }
+ it { is_expected.to respond_to(:jobs) }
+ it { is_expected.to respond_to(:status) }
+
+ describe '#size' do
+ it 'returns the number of statuses in the group' do
+ expect(subject.size).to eq(1)
+ end
+ end
+
+ describe '#detailed_status' do
+ context 'when there is only one item in the group' do
+ it 'calls the status from the object itself' do
+ expect(jobs.first).to receive(:detailed_status)
+
+ expect(subject.detailed_status(double(:user)))
+ end
+ end
+
+ context 'when there are more than one commit status in the group' do
+ let(:jobs) do
+ [create(:ci_build, :failed),
+ create(:ci_build, :success)]
+ end
+
+ it 'fabricates a new detailed status object' do
+ expect(subject.detailed_status(double(:user)))
+ .to be_a(Gitlab::Ci::Status::Failed)
+ end
+ end
+ end
+end
diff --git a/spec/models/ci/pipeline_schedule_spec.rb b/spec/models/ci/pipeline_schedule_spec.rb
new file mode 100644
index 00000000000..822b98c5f6c
--- /dev/null
+++ b/spec/models/ci/pipeline_schedule_spec.rb
@@ -0,0 +1,112 @@
+require 'spec_helper'
+
+describe Ci::PipelineSchedule, models: true do
+ it { is_expected.to belong_to(:project) }
+ it { is_expected.to belong_to(:owner) }
+
+ it { is_expected.to have_many(:pipelines) }
+
+ it { is_expected.to respond_to(:ref) }
+ it { is_expected.to respond_to(:cron) }
+ it { is_expected.to respond_to(:cron_timezone) }
+ it { is_expected.to respond_to(:description) }
+ it { is_expected.to respond_to(:next_run_at) }
+ it { is_expected.to respond_to(:deleted_at) }
+
+ describe 'validations' do
+ it 'does not allow invalid cron patters' do
+ pipeline_schedule = build(:ci_pipeline_schedule, cron: '0 0 0 * *')
+
+ expect(pipeline_schedule).not_to be_valid
+ end
+
+ it 'does not allow invalid cron patters' do
+ pipeline_schedule = build(:ci_pipeline_schedule, cron_timezone: 'invalid')
+
+ expect(pipeline_schedule).not_to be_valid
+ end
+ end
+
+ describe '#set_next_run_at' do
+ let!(:pipeline_schedule) { create(:ci_pipeline_schedule, :nightly) }
+
+ context 'when creates new pipeline schedule' do
+ let(:expected_next_run_at) do
+ Gitlab::Ci::CronParser.new(pipeline_schedule.cron, pipeline_schedule.cron_timezone).
+ next_time_from(Time.now)
+ end
+
+ it 'updates next_run_at automatically' do
+ expect(Ci::PipelineSchedule.last.next_run_at).to eq(expected_next_run_at)
+ end
+ end
+
+ context 'when updates cron of exsisted pipeline schedule' do
+ let(:new_cron) { '0 0 1 1 *' }
+
+ let(:expected_next_run_at) do
+ Gitlab::Ci::CronParser.new(new_cron, pipeline_schedule.cron_timezone).
+ next_time_from(Time.now)
+ end
+
+ it 'updates next_run_at automatically' do
+ pipeline_schedule.update!(cron: new_cron)
+
+ expect(Ci::PipelineSchedule.last.next_run_at).to eq(expected_next_run_at)
+ end
+ end
+ end
+
+ describe '#schedule_next_run!' do
+ let!(:pipeline_schedule) { create(:ci_pipeline_schedule, :nightly) }
+
+ context 'when reschedules after 10 days from now' do
+ let(:future_time) { 10.days.from_now }
+
+ let(:expected_next_run_at) do
+ Gitlab::Ci::CronParser.new(pipeline_schedule.cron, pipeline_schedule.cron_timezone).
+ next_time_from(future_time)
+ end
+
+ it 'points to proper next_run_at' do
+ Timecop.freeze(future_time) do
+ pipeline_schedule.schedule_next_run!
+
+ expect(pipeline_schedule.next_run_at).to eq(expected_next_run_at)
+ end
+ end
+ end
+ end
+
+ describe '#real_next_run' do
+ subject do
+ described_class.last.real_next_run(worker_cron: worker_cron,
+ worker_time_zone: worker_time_zone)
+ end
+
+ context 'when GitLab time_zone is UTC' do
+ before do
+ allow(Time).to receive(:zone)
+ .and_return(ActiveSupport::TimeZone[worker_time_zone])
+ end
+
+ let(:worker_time_zone) { 'UTC' }
+
+ context 'when cron_timezone is Eastern Time (US & Canada)' do
+ before do
+ create(:ci_pipeline_schedule, :nightly,
+ cron_timezone: 'Eastern Time (US & Canada)')
+ end
+
+ let(:worker_cron) { '0 1 2 3 *' }
+
+ it 'returns the next time worker executes' do
+ expect(subject.min).to eq(0)
+ expect(subject.hour).to eq(1)
+ expect(subject.day).to eq(2)
+ expect(subject.month).to eq(3)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index d7d6a75d38d..157d17fbb68 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -13,6 +13,7 @@ describe Ci::Pipeline, models: true do
it { is_expected.to belong_to(:project) }
it { is_expected.to belong_to(:user) }
it { is_expected.to belong_to(:auto_canceled_by) }
+ it { is_expected.to belong_to(:pipeline_schedule) }
it { is_expected.to have_many(:statuses) }
it { is_expected.to have_many(:trigger_requests) }
@@ -59,8 +60,8 @@ describe Ci::Pipeline, models: true do
subject { pipeline.retried }
before do
- @build1 = FactoryGirl.create :ci_build, pipeline: pipeline, name: 'deploy'
- @build2 = FactoryGirl.create :ci_build, pipeline: pipeline, name: 'deploy'
+ @build1 = create(:ci_build, pipeline: pipeline, name: 'deploy', retried: true)
+ @build2 = create(:ci_build, pipeline: pipeline, name: 'deploy')
end
it 'returns old builds' do
@@ -69,31 +70,31 @@ describe Ci::Pipeline, models: true do
end
describe "coverage" do
- let(:project) { FactoryGirl.create :empty_project, build_coverage_regex: "/.*/" }
- let(:pipeline) { FactoryGirl.create :ci_empty_pipeline, project: project }
+ let(:project) { create(:empty_project, build_coverage_regex: "/.*/") }
+ let(:pipeline) { create(:ci_empty_pipeline, project: project) }
it "calculates average when there are two builds with coverage" do
- FactoryGirl.create :ci_build, name: "rspec", coverage: 30, pipeline: pipeline
- FactoryGirl.create :ci_build, name: "rubocop", coverage: 40, pipeline: pipeline
+ create(:ci_build, name: "rspec", coverage: 30, pipeline: pipeline)
+ create(:ci_build, name: "rubocop", coverage: 40, pipeline: pipeline)
expect(pipeline.coverage).to eq("35.00")
end
it "calculates average when there are two builds with coverage and one with nil" do
- FactoryGirl.create :ci_build, name: "rspec", coverage: 30, pipeline: pipeline
- FactoryGirl.create :ci_build, name: "rubocop", coverage: 40, pipeline: pipeline
- FactoryGirl.create :ci_build, pipeline: pipeline
+ create(:ci_build, name: "rspec", coverage: 30, pipeline: pipeline)
+ create(:ci_build, name: "rubocop", coverage: 40, pipeline: pipeline)
+ create(:ci_build, pipeline: pipeline)
expect(pipeline.coverage).to eq("35.00")
end
it "calculates average when there are two builds with coverage and one is retried" do
- FactoryGirl.create :ci_build, name: "rspec", coverage: 30, pipeline: pipeline
- FactoryGirl.create :ci_build, name: "rubocop", coverage: 30, pipeline: pipeline
- FactoryGirl.create :ci_build, name: "rubocop", coverage: 40, pipeline: pipeline
+ create(:ci_build, name: "rspec", coverage: 30, pipeline: pipeline)
+ create(:ci_build, name: "rubocop", coverage: 30, pipeline: pipeline, retried: true)
+ create(:ci_build, name: "rubocop", coverage: 40, pipeline: pipeline)
expect(pipeline.coverage).to eq("35.00")
end
it "calculates average when there is one build without coverage" do
- FactoryGirl.create :ci_build, pipeline: pipeline
+ FactoryGirl.create(:ci_build, pipeline: pipeline)
expect(pipeline.coverage).to be_nil
end
end
@@ -221,13 +222,15 @@ describe Ci::Pipeline, models: true do
%w(deploy running)])
end
- context 'when commit status is retried' do
+ context 'when commit status is retried' do
before do
create(:commit_status, pipeline: pipeline,
stage: 'build',
name: 'mac',
stage_idx: 0,
status: 'success')
+
+ pipeline.process!
end
it 'ignores the previous state' do
@@ -296,32 +299,56 @@ describe Ci::Pipeline, models: true do
describe 'state machine' do
let(:current) { Time.now.change(usec: 0) }
- let(:build) { create_build('build1', 0) }
- let(:build_b) { create_build('build2', 0) }
- let(:build_c) { create_build('build3', 0) }
+ let(:build) { create_build('build1', queued_at: 0) }
+ let(:build_b) { create_build('build2', queued_at: 0) }
+ let(:build_c) { create_build('build3', queued_at: 0) }
describe '#duration' do
- before do
- travel_to(current + 30) do
- build.run!
- build.success!
- build_b.run!
- build_c.run!
- end
+ context 'when multiple builds are finished' do
+ before do
+ travel_to(current + 30) do
+ build.run!
+ build.success!
+ build_b.run!
+ build_c.run!
+ end
- travel_to(current + 40) do
- build_b.drop!
+ travel_to(current + 40) do
+ build_b.drop!
+ end
+
+ travel_to(current + 70) do
+ build_c.success!
+ end
end
- travel_to(current + 70) do
- build_c.success!
+ it 'matches sum of builds duration' do
+ pipeline.reload
+
+ expect(pipeline.duration).to eq(40)
end
end
- it 'matches sum of builds duration' do
- pipeline.reload
+ context 'when pipeline becomes blocked' do
+ let!(:build) { create_build('build:1') }
+ let!(:action) { create_build('manual:action', :manual) }
- expect(pipeline.duration).to eq(40)
+ before do
+ travel_to(current + 1.minute) do
+ build.run!
+ end
+
+ travel_to(current + 5.minutes) do
+ build.success!
+ end
+ end
+
+ it 'recalculates pipeline duration' do
+ pipeline.reload
+
+ expect(pipeline).to be_manual
+ expect(pipeline.duration).to eq 4.minutes
+ end
end
end
@@ -376,19 +403,20 @@ describe Ci::Pipeline, models: true do
end
describe 'pipeline caching' do
- it 'executes ExpirePipelinesCacheService' do
- expect_any_instance_of(Ci::ExpirePipelineCacheService).to receive(:execute).with(pipeline)
+ it 'performs ExpirePipelinesCacheWorker' do
+ expect(ExpirePipelineCacheWorker).to receive(:perform_async).with(pipeline.id)
pipeline.cancel
end
end
- def create_build(name, queued_at = current, started_from = 0)
- create(:ci_build,
+ def create_build(name, *traits, queued_at: current, started_from: 0, **opts)
+ create(:ci_build, *traits,
name: name,
pipeline: pipeline,
queued_at: queued_at,
- started_at: queued_at + started_from)
+ started_at: queued_at + started_from,
+ **opts)
end
end
@@ -463,6 +491,10 @@ describe Ci::Pipeline, models: true do
context 'there are multiple of the same name' do
let!(:manual2) { create(:ci_build, :manual, pipeline: pipeline, name: 'deploy') }
+ before do
+ manual.update(retried: true)
+ end
+
it 'returns latest one' do
is_expected.to contain_exactly(manual2)
end
@@ -1014,11 +1046,12 @@ describe Ci::Pipeline, models: true do
end
describe "#merge_requests" do
- let(:project) { create(:project, :repository) }
- let(:pipeline) { FactoryGirl.create(:ci_empty_pipeline, status: 'created', project: project, ref: 'master', sha: project.repository.commit('master').id) }
+ let(:project) { create(:empty_project) }
+ let(:pipeline) { create(:ci_empty_pipeline, status: 'created', project: project, ref: 'master', sha: 'a288a022a53a5a944fae87bcec6efc87b7061808') }
it "returns merge requests whose `diff_head_sha` matches the pipeline's SHA" do
- merge_request = create(:merge_request, source_project: project, source_branch: pipeline.ref)
+ allow_any_instance_of(MergeRequest).to receive(:diff_head_sha) { 'a288a022a53a5a944fae87bcec6efc87b7061808' }
+ merge_request = create(:merge_request, source_project: project, head_pipeline: pipeline, source_branch: pipeline.ref)
expect(pipeline.merge_requests).to eq([merge_request])
end
@@ -1037,6 +1070,23 @@ describe Ci::Pipeline, models: true do
end
end
+ describe "#all_merge_requests" do
+ let(:project) { create(:empty_project) }
+ let(:pipeline) { create(:ci_empty_pipeline, status: 'created', project: project, ref: 'master') }
+
+ it "returns all merge requests having the same source branch" do
+ merge_request = create(:merge_request, source_project: project, source_branch: pipeline.ref)
+
+ expect(pipeline.all_merge_requests).to eq([merge_request])
+ end
+
+ it "doesn't return merge requests having a different source branch" do
+ create(:merge_request, source_project: project, source_branch: 'feature', target_branch: 'master')
+
+ expect(pipeline.all_merge_requests).to be_empty
+ end
+ end
+
describe '#stuck?' do
before do
create(:ci_build, :pending, pipeline: pipeline)
diff --git a/spec/models/ci/stage_spec.rb b/spec/models/ci/stage_spec.rb
index c38faf32f7d..8f6ab908987 100644
--- a/spec/models/ci/stage_spec.rb
+++ b/spec/models/ci/stage_spec.rb
@@ -28,6 +28,35 @@ describe Ci::Stage, models: true do
end
end
+ describe '#groups' do
+ before do
+ create_job(:ci_build, name: 'rspec 0 2')
+ create_job(:ci_build, name: 'rspec 0 1')
+ create_job(:ci_build, name: 'spinach 0 1')
+ create_job(:commit_status, name: 'aaaaa')
+ end
+
+ it 'returns an array of three groups' do
+ expect(stage.groups).to be_a Array
+ expect(stage.groups).to all(be_a Ci::Group)
+ expect(stage.groups.size).to eq 3
+ end
+
+ it 'returns groups with correctly ordered statuses' do
+ expect(stage.groups.first.jobs.map(&:name))
+ .to eq ['aaaaa']
+ expect(stage.groups.second.jobs.map(&:name))
+ .to eq ['rspec 0 1', 'rspec 0 2']
+ expect(stage.groups.third.jobs.map(&:name))
+ .to eq ['spinach 0 1']
+ end
+
+ it 'returns groups with correct names' do
+ expect(stage.groups.map(&:name))
+ .to eq %w[aaaaa rspec spinach]
+ end
+ end
+
describe '#statuses_count' do
before do
create_job(:ci_build)
@@ -73,6 +102,10 @@ describe Ci::Stage, models: true do
context 'and builds are retried' do
let!(:new_build) { create_job(:ci_build, status: :success) }
+ before do
+ stage_build.update(retried: true)
+ end
+
it "returns status of latest build" do
is_expected.to eq('success')
end
@@ -223,7 +256,7 @@ describe Ci::Stage, models: true do
end
end
- def create_job(type, status: 'success', stage: stage_name)
- create(type, pipeline: pipeline, stage: stage, status: status)
+ def create_job(type, status: 'success', stage: stage_name, **opts)
+ create(type, pipeline: pipeline, stage: stage, status: status, **opts)
end
end
diff --git a/spec/models/ci/trigger_schedule_spec.rb b/spec/models/ci/trigger_schedule_spec.rb
deleted file mode 100644
index 75d21541cee..00000000000
--- a/spec/models/ci/trigger_schedule_spec.rb
+++ /dev/null
@@ -1,76 +0,0 @@
-require 'spec_helper'
-
-describe Ci::TriggerSchedule, models: true do
- it { is_expected.to belong_to(:project) }
- it { is_expected.to belong_to(:trigger) }
- it { is_expected.to respond_to(:ref) }
-
- describe '#set_next_run_at' do
- context 'when creates new TriggerSchedule' do
- before do
- trigger_schedule = create(:ci_trigger_schedule, :nightly)
- @expected_next_run_at = Gitlab::Ci::CronParser.new(trigger_schedule.cron, trigger_schedule.cron_timezone)
- .next_time_from(Time.now)
- end
-
- it 'updates next_run_at automatically' do
- expect(Ci::TriggerSchedule.last.next_run_at).to eq(@expected_next_run_at)
- end
- end
-
- context 'when updates cron of exsisted TriggerSchedule' do
- before do
- trigger_schedule = create(:ci_trigger_schedule, :nightly)
- new_cron = '0 0 1 1 *'
- trigger_schedule.update!(cron: new_cron) # Subject
- @expected_next_run_at = Gitlab::Ci::CronParser.new(new_cron, trigger_schedule.cron_timezone)
- .next_time_from(Time.now)
- end
-
- it 'updates next_run_at automatically' do
- expect(Ci::TriggerSchedule.last.next_run_at).to eq(@expected_next_run_at)
- end
- end
- end
-
- describe '#schedule_next_run!' do
- context 'when reschedules after 10 days from now' do
- before do
- trigger_schedule = create(:ci_trigger_schedule, :nightly)
- time_future = Time.now + 10.days
- allow(Time).to receive(:now).and_return(time_future)
- trigger_schedule.schedule_next_run! # Subject
- @expected_next_run_at = Gitlab::Ci::CronParser.new(trigger_schedule.cron, trigger_schedule.cron_timezone)
- .next_time_from(time_future)
- end
-
- it 'points to proper next_run_at' do
- expect(Ci::TriggerSchedule.last.next_run_at).to eq(@expected_next_run_at)
- end
- end
-
- context 'when cron is invalid' do
- before do
- trigger_schedule = create(:ci_trigger_schedule, :nightly)
- trigger_schedule.cron = 'Invalid-cron'
- trigger_schedule.schedule_next_run! # Subject
- end
-
- it 'sets nil to next_run_at' do
- expect(Ci::TriggerSchedule.last.next_run_at).to be_nil
- end
- end
-
- context 'when cron_timezone is invalid' do
- before do
- trigger_schedule = create(:ci_trigger_schedule, :nightly)
- trigger_schedule.cron_timezone = 'Invalid-cron_timezone'
- trigger_schedule.schedule_next_run! # Subject
- end
-
- it 'sets nil to next_run_at' do
- expect(Ci::TriggerSchedule.last.next_run_at).to be_nil
- end
- end
- end
-end
diff --git a/spec/models/ci/trigger_spec.rb b/spec/models/ci/trigger_spec.rb
index d26121018ce..92c15c13c18 100644
--- a/spec/models/ci/trigger_spec.rb
+++ b/spec/models/ci/trigger_spec.rb
@@ -7,7 +7,6 @@ describe Ci::Trigger, models: true do
it { is_expected.to belong_to(:project) }
it { is_expected.to belong_to(:owner) }
it { is_expected.to have_many(:trigger_requests) }
- it { is_expected.to have_one(:trigger_schedule) }
end
describe 'before_validation' do
diff --git a/spec/models/ci/variable_spec.rb b/spec/models/ci/variable_spec.rb
index 048d25869bc..fe8c52d5353 100644
--- a/spec/models/ci/variable_spec.rb
+++ b/spec/models/ci/variable_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe Ci::Variable, models: true do
- subject { Ci::Variable.new }
+ subject { build(:ci_variable) }
let(:secret_value) { 'secret' }
diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb
index ce31c8ed94c..72f83d63224 100644
--- a/spec/models/commit_spec.rb
+++ b/spec/models/commit_spec.rb
@@ -212,7 +212,7 @@ eos
end
end
- describe '#latest_pipeline' do
+ describe '#last_pipeline' do
let!(:first_pipeline) do
create(:ci_empty_pipeline,
project: project,
@@ -226,8 +226,8 @@ eos
status: 'success')
end
- it 'returns latest pipeline' do
- expect(commit.latest_pipeline).to eq second_pipeline
+ it 'returns last pipeline' do
+ expect(commit.last_pipeline).to eq second_pipeline
end
end
@@ -388,33 +388,4 @@ eos
expect(described_class.valid_hash?('a' * 41)).to be false
end
end
-
- # describe '#raw_diffs' do
- # TODO: Uncomment when feature is reenabled
- # context 'Gitaly commit_raw_diffs feature enabled' do
- # before do
- # allow(Gitlab::GitalyClient).to receive(:feature_enabled?).with(:commit_raw_diffs).and_return(true)
- # end
- #
- # context 'when a truthy deltas_only is not passed to args' do
- # it 'fetches diffs from Gitaly server' do
- # expect(Gitlab::GitalyClient::Commit).to receive(:diff_from_parent).
- # with(commit)
- #
- # commit.raw_diffs
- # end
- # end
- #
- # context 'when a truthy deltas_only is passed to args' do
- # it 'fetches diffs using Rugged' do
- # opts = { deltas_only: true }
- #
- # expect(Gitlab::GitalyClient::Commit).not_to receive(:diff_from_parent)
- # expect(commit.raw).to receive(:diffs).with(opts)
- #
- # commit.raw_diffs(opts)
- # end
- # end
- # end
- # end
end
diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb
index 0ee85489574..6947affcc1e 100644
--- a/spec/models/commit_status_spec.rb
+++ b/spec/models/commit_status_spec.rb
@@ -157,9 +157,9 @@ describe CommitStatus, :models do
subject { described_class.latest.order(:id) }
let(:statuses) do
- [create_status(name: 'aa', ref: 'bb', status: 'running'),
- create_status(name: 'cc', ref: 'cc', status: 'pending'),
- create_status(name: 'aa', ref: 'cc', status: 'success'),
+ [create_status(name: 'aa', ref: 'bb', status: 'running', retried: true),
+ create_status(name: 'cc', ref: 'cc', status: 'pending', retried: true),
+ create_status(name: 'aa', ref: 'cc', status: 'success', retried: true),
create_status(name: 'cc', ref: 'bb', status: 'success'),
create_status(name: 'aa', ref: 'bb', status: 'success')]
end
@@ -169,6 +169,22 @@ describe CommitStatus, :models do
end
end
+ describe '.retried' do
+ subject { described_class.retried.order(:id) }
+
+ let(:statuses) do
+ [create_status(name: 'aa', ref: 'bb', status: 'running', retried: true),
+ create_status(name: 'cc', ref: 'cc', status: 'pending', retried: true),
+ create_status(name: 'aa', ref: 'cc', status: 'success', retried: true),
+ create_status(name: 'cc', ref: 'bb', status: 'success'),
+ create_status(name: 'aa', ref: 'bb', status: 'success')]
+ end
+
+ it 'returns unique statuses' do
+ is_expected.to contain_exactly(*statuses.values_at(0, 1, 2))
+ end
+ end
+
describe '.running_or_pending' do
subject { described_class.running_or_pending.order(:id) }
@@ -181,7 +197,7 @@ describe CommitStatus, :models do
end
it 'returns statuses that are running or pending' do
- is_expected.to eq(statuses.values_at(0, 1))
+ is_expected.to contain_exactly(*statuses.values_at(0, 1))
end
end
diff --git a/spec/models/concerns/awardable_spec.rb b/spec/models/concerns/awardable_spec.rb
index de791abdf3d..63ad3a3630b 100644
--- a/spec/models/concerns/awardable_spec.rb
+++ b/spec/models/concerns/awardable_spec.rb
@@ -1,10 +1,12 @@
require 'spec_helper'
-describe Issue, "Awardable" do
+describe Awardable do
let!(:issue) { create(:issue) }
let!(:award_emoji) { create(:award_emoji, :downvote, awardable: issue) }
describe "Associations" do
+ subject { build(:issue) }
+
it { is_expected.to have_many(:award_emoji).dependent(:destroy) }
end
diff --git a/spec/models/concerns/cache_markdown_field_spec.rb b/spec/models/concerns/cache_markdown_field_spec.rb
index de0069bdcac..40bbb10eaac 100644
--- a/spec/models/concerns/cache_markdown_field_spec.rb
+++ b/spec/models/concerns/cache_markdown_field_spec.rb
@@ -18,7 +18,7 @@ describe CacheMarkdownField do
end
extend ActiveModel::Callbacks
- define_model_callbacks :save
+ define_model_callbacks :create, :update
include CacheMarkdownField
cache_markdown_field :foo
@@ -56,7 +56,7 @@ describe CacheMarkdownField do
end
def save
- run_callbacks :save do
+ run_callbacks :update do
changes_applied
end
end
@@ -170,6 +170,12 @@ describe CacheMarkdownField do
is_expected.to be_truthy
end
+
+ it 'returns false if the markdown field is set but the html is not' do
+ thing.foo_html = nil
+
+ is_expected.to be_falsy
+ end
end
describe '#refresh_markdown_cache!' do
diff --git a/spec/models/concerns/discussion_on_diff_spec.rb b/spec/models/concerns/discussion_on_diff_spec.rb
index 0002a00770f..8571e85627c 100644
--- a/spec/models/concerns/discussion_on_diff_spec.rb
+++ b/spec/models/concerns/discussion_on_diff_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe DiffDiscussion, DiscussionOnDiff, model: true do
+describe DiscussionOnDiff, model: true do
subject { create(:diff_note_on_merge_request).to_discussion }
describe "#truncated_diff_lines" do
@@ -8,9 +8,9 @@ describe DiffDiscussion, DiscussionOnDiff, model: true do
context "when diff is greater than allowed number of truncated diff lines " do
it "returns fewer lines" do
- expect(subject.diff_lines.count).to be > described_class::NUMBER_OF_TRUNCATED_DIFF_LINES
+ expect(subject.diff_lines.count).to be > DiffDiscussion::NUMBER_OF_TRUNCATED_DIFF_LINES
- expect(truncated_lines.count).to be <= described_class::NUMBER_OF_TRUNCATED_DIFF_LINES
+ expect(truncated_lines.count).to be <= DiffDiscussion::NUMBER_OF_TRUNCATED_DIFF_LINES
end
end
diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb
index 4522206fab1..27890e33b49 100644
--- a/spec/models/concerns/issuable_spec.rb
+++ b/spec/models/concerns/issuable_spec.rb
@@ -1,13 +1,15 @@
require 'spec_helper'
-describe Issue, "Issuable" do
+describe Issuable do
+ let(:issuable_class) { Issue }
let(:issue) { create(:issue) }
let(:user) { create(:user) }
describe "Associations" do
+ subject { build(:issue) }
+
it { is_expected.to belong_to(:project) }
it { is_expected.to belong_to(:author) }
- it { is_expected.to belong_to(:assignee) }
it { is_expected.to have_many(:notes).dependent(:destroy) }
it { is_expected.to have_many(:todos).dependent(:destroy) }
@@ -23,10 +25,14 @@ describe Issue, "Issuable" do
end
describe 'Included modules' do
+ let(:described_class) { issuable_class }
+
it { is_expected.to include_module(Awardable) }
end
describe "Validation" do
+ subject { build(:issue) }
+
before do
allow(subject).to receive(:set_iid).and_return(false)
end
@@ -39,9 +45,11 @@ describe Issue, "Issuable" do
end
describe "Scope" do
- it { expect(described_class).to respond_to(:opened) }
- it { expect(described_class).to respond_to(:closed) }
- it { expect(described_class).to respond_to(:assigned) }
+ subject { build(:issue) }
+
+ it { expect(issuable_class).to respond_to(:opened) }
+ it { expect(issuable_class).to respond_to(:closed) }
+ it { expect(issuable_class).to respond_to(:assigned) }
end
describe 'author_name' do
@@ -57,74 +65,20 @@ describe Issue, "Issuable" do
end
end
- describe 'assignee_name' do
- it 'is delegated to assignee' do
- issue.update!(assignee: create(:user))
-
- expect(issue.assignee_name).to eq issue.assignee.name
- end
-
- it 'returns nil when assignee is nil' do
- issue.assignee_id = nil
- issue.save(validate: false)
-
- expect(issue.assignee_name).to eq nil
- end
- end
-
- describe "before_save" do
- describe "#update_cache_counts" do
- context "when previous assignee exists" do
- before do
- assignee = create(:user)
- issue.project.team << [assignee, :developer]
- issue.update(assignee: assignee)
- end
-
- it "updates cache counts for new assignee" do
- user = create(:user)
-
- expect(user).to receive(:update_cache_counts)
-
- issue.update(assignee: user)
- end
-
- it "updates cache counts for previous assignee" do
- old_assignee = issue.assignee
- allow(User).to receive(:find_by_id).with(old_assignee.id).and_return(old_assignee)
-
- expect(old_assignee).to receive(:update_cache_counts)
-
- issue.update(assignee: nil)
- end
- end
-
- context "when previous assignee does not exist" do
- before{ issue.update(assignee: nil) }
-
- it "updates cache count for the new assignee" do
- expect_any_instance_of(User).to receive(:update_cache_counts)
-
- issue.update(assignee: user)
- end
- end
- end
- end
-
describe ".search" do
let!(:searchable_issue) { create(:issue, title: "Searchable issue") }
it 'returns notes with a matching title' do
- expect(described_class.search(searchable_issue.title)).
+ expect(issuable_class.search(searchable_issue.title)).
to eq([searchable_issue])
end
it 'returns notes with a partially matching title' do
- expect(described_class.search('able')).to eq([searchable_issue])
+ expect(issuable_class.search('able')).to eq([searchable_issue])
end
it 'returns notes with a matching title regardless of the casing' do
- expect(described_class.search(searchable_issue.title.upcase)).
+ expect(issuable_class.search(searchable_issue.title.upcase)).
to eq([searchable_issue])
end
end
@@ -135,31 +89,31 @@ describe Issue, "Issuable" do
end
it 'returns notes with a matching title' do
- expect(described_class.full_search(searchable_issue.title)).
+ expect(issuable_class.full_search(searchable_issue.title)).
to eq([searchable_issue])
end
it 'returns notes with a partially matching title' do
- expect(described_class.full_search('able')).to eq([searchable_issue])
+ expect(issuable_class.full_search('able')).to eq([searchable_issue])
end
it 'returns notes with a matching title regardless of the casing' do
- expect(described_class.full_search(searchable_issue.title.upcase)).
+ expect(issuable_class.full_search(searchable_issue.title.upcase)).
to eq([searchable_issue])
end
it 'returns notes with a matching description' do
- expect(described_class.full_search(searchable_issue.description)).
+ expect(issuable_class.full_search(searchable_issue.description)).
to eq([searchable_issue])
end
it 'returns notes with a partially matching description' do
- expect(described_class.full_search(searchable_issue.description)).
+ expect(issuable_class.full_search(searchable_issue.description)).
to eq([searchable_issue])
end
it 'returns notes with a matching description regardless of the casing' do
- expect(described_class.full_search(searchable_issue.description.upcase)).
+ expect(issuable_class.full_search(searchable_issue.description.upcase)).
to eq([searchable_issue])
end
end
@@ -298,7 +252,20 @@ describe Issue, "Issuable" do
end
context "issue is assigned" do
- before { issue.update_attribute(:assignee, user) }
+ before { issue.assignees << user }
+
+ it "returns correct hook data" do
+ expect(data[:assignees].first).to eq(user.hook_attrs)
+ end
+ end
+
+ context "merge_request is assigned" do
+ let(:merge_request) { create(:merge_request) }
+ let(:data) { merge_request.to_hook_data(user) }
+
+ before do
+ merge_request.update_attribute(:assignee, user)
+ end
it "returns correct hook data" do
expect(data[:object_attributes]['assignee_id']).to eq(user.id)
@@ -320,24 +287,6 @@ describe Issue, "Issuable" do
include_examples 'deprecated repository hook data'
end
- describe '#card_attributes' do
- it 'includes the author name' do
- allow(issue).to receive(:author).and_return(double(name: 'Robert'))
- allow(issue).to receive(:assignee).and_return(nil)
-
- expect(issue.card_attributes).
- to eq({ 'Author' => 'Robert', 'Assignee' => nil })
- end
-
- it 'includes the assignee name' do
- allow(issue).to receive(:author).and_return(double(name: 'Robert'))
- allow(issue).to receive(:assignee).and_return(double(name: 'Douwe'))
-
- expect(issue.card_attributes).
- to eq({ 'Author' => 'Robert', 'Assignee' => 'Douwe' })
- end
- end
-
describe '#labels_array' do
let(:project) { create(:empty_project) }
let(:bug) { create(:label, project: project, title: 'bug') }
@@ -466,27 +415,6 @@ describe Issue, "Issuable" do
end
end
- describe '#assignee_or_author?' do
- let(:user) { build(:user, id: 1) }
- let(:issue) { build(:issue) }
-
- it 'returns true for a user that is assigned to an issue' do
- issue.assignee = user
-
- expect(issue.assignee_or_author?(user)).to eq(true)
- end
-
- it 'returns true for a user that is the author of an issue' do
- issue.author = user
-
- expect(issue.assignee_or_author?(user)).to eq(true)
- end
-
- it 'returns false for a user that is not the assignee or author' do
- expect(issue.assignee_or_author?(user)).to eq(false)
- end
- end
-
describe '#spend_time' do
let(:user) { create(:user) }
let(:issue) { create(:issue) }
diff --git a/spec/models/concerns/mentionable_spec.rb b/spec/models/concerns/mentionable_spec.rb
index 2092576e981..e382c7120de 100644
--- a/spec/models/concerns/mentionable_spec.rb
+++ b/spec/models/concerns/mentionable_spec.rb
@@ -163,3 +163,52 @@ describe Issue, "Mentionable" do
end
end
end
+
+describe Commit, 'Mentionable' do
+ let(:project) { create(:project, :public, :repository) }
+ let(:commit) { project.commit }
+
+ describe '#matches_cross_reference_regex?' do
+ it "is false when message doesn't reference anything" do
+ allow(commit.raw).to receive(:message).and_return "WIP: Do something"
+
+ expect(commit.matches_cross_reference_regex?).to be false
+ end
+
+ it 'is true if issue #number mentioned in title' do
+ allow(commit.raw).to receive(:message).and_return "#1"
+
+ expect(commit.matches_cross_reference_regex?).to be true
+ end
+
+ it 'is true if references an MR' do
+ allow(commit.raw).to receive(:message).and_return "See merge request !12"
+
+ expect(commit.matches_cross_reference_regex?).to be true
+ end
+
+ it 'is true if references a commit' do
+ allow(commit.raw).to receive(:message).and_return "a1b2c3d4"
+
+ expect(commit.matches_cross_reference_regex?).to be true
+ end
+
+ it 'is true if issue referenced by url' do
+ issue = create(:issue, project: project)
+
+ allow(commit.raw).to receive(:message).and_return Gitlab::UrlBuilder.build(issue)
+
+ expect(commit.matches_cross_reference_regex?).to be true
+ end
+
+ context 'with external issue tracker' do
+ let(:project) { create(:jira_project) }
+
+ it 'is true if external issues referenced' do
+ allow(commit.raw).to receive(:message).and_return 'JIRA-123'
+
+ expect(commit.matches_cross_reference_regex?).to be true
+ end
+ end
+ end
+end
diff --git a/spec/models/concerns/milestoneish_spec.rb b/spec/models/concerns/milestoneish_spec.rb
index 68e4c0a522b..675b730c557 100644
--- a/spec/models/concerns/milestoneish_spec.rb
+++ b/spec/models/concerns/milestoneish_spec.rb
@@ -11,13 +11,13 @@ describe Milestone, 'Milestoneish' do
let(:milestone) { create(:milestone, project: project) }
let!(:issue) { create(:issue, project: project, milestone: milestone) }
let!(:security_issue_1) { create(:issue, :confidential, project: project, author: author, milestone: milestone) }
- let!(:security_issue_2) { create(:issue, :confidential, project: project, assignee: assignee, milestone: milestone) }
+ let!(:security_issue_2) { create(:issue, :confidential, project: project, assignees: [assignee], milestone: milestone) }
let!(:closed_issue_1) { create(:issue, :closed, project: project, milestone: milestone) }
let!(:closed_issue_2) { create(:issue, :closed, project: project, milestone: milestone) }
let!(:closed_security_issue_1) { create(:issue, :confidential, :closed, project: project, author: author, milestone: milestone) }
- let!(:closed_security_issue_2) { create(:issue, :confidential, :closed, project: project, assignee: assignee, milestone: milestone) }
+ let!(:closed_security_issue_2) { create(:issue, :confidential, :closed, project: project, assignees: [assignee], milestone: milestone) }
let!(:closed_security_issue_3) { create(:issue, :confidential, :closed, project: project, author: author, milestone: milestone) }
- let!(:closed_security_issue_4) { create(:issue, :confidential, :closed, project: project, assignee: assignee, milestone: milestone) }
+ let!(:closed_security_issue_4) { create(:issue, :confidential, :closed, project: project, assignees: [assignee], milestone: milestone) }
let!(:merge_request) { create(:merge_request, source_project: project, target_project: project, milestone: milestone) }
before do
diff --git a/spec/models/concerns/noteable_spec.rb b/spec/models/concerns/noteable_spec.rb
index 92cc8859a8c..bdae742ff1d 100644
--- a/spec/models/concerns/noteable_spec.rb
+++ b/spec/models/concerns/noteable_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe MergeRequest, Noteable, model: true do
+describe Noteable, model: true do
let!(:active_diff_note1) { create(:diff_note_on_merge_request) }
let(:project) { active_diff_note1.project }
subject { active_diff_note1.noteable }
diff --git a/spec/models/concerns/relative_positioning_spec.rb b/spec/models/concerns/relative_positioning_spec.rb
index 255b584a85e..494e6f1b6f6 100644
--- a/spec/models/concerns/relative_positioning_spec.rb
+++ b/spec/models/concerns/relative_positioning_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Issue, 'RelativePositioning' do
+describe RelativePositioning do
let(:project) { create(:empty_project) }
let(:issue) { create(:issue, project: project) }
let(:issue1) { create(:issue, project: project) }
diff --git a/spec/models/concerns/routable_spec.rb b/spec/models/concerns/routable_spec.rb
index f191605dbdb..49a4132f763 100644
--- a/spec/models/concerns/routable_spec.rb
+++ b/spec/models/concerns/routable_spec.rb
@@ -9,6 +9,7 @@ describe Group, 'Routable' do
describe 'Associations' do
it { is_expected.to have_one(:route).dependent(:destroy) }
+ it { is_expected.to have_many(:redirect_routes).dependent(:destroy) }
end
describe 'Callbacks' do
@@ -35,10 +36,53 @@ describe Group, 'Routable' do
describe '.find_by_full_path' do
let!(:nested_group) { create(:group, parent: group) }
- it { expect(described_class.find_by_full_path(group.to_param)).to eq(group) }
- it { expect(described_class.find_by_full_path(group.to_param.upcase)).to eq(group) }
- it { expect(described_class.find_by_full_path(nested_group.to_param)).to eq(nested_group) }
- it { expect(described_class.find_by_full_path('unknown')).to eq(nil) }
+ context 'without any redirect routes' do
+ it { expect(described_class.find_by_full_path(group.to_param)).to eq(group) }
+ it { expect(described_class.find_by_full_path(group.to_param.upcase)).to eq(group) }
+ it { expect(described_class.find_by_full_path(nested_group.to_param)).to eq(nested_group) }
+ it { expect(described_class.find_by_full_path('unknown')).to eq(nil) }
+ end
+
+ context 'with redirect routes' do
+ let!(:group_redirect_route) { group.redirect_routes.create!(path: 'bar') }
+ let!(:nested_group_redirect_route) { nested_group.redirect_routes.create!(path: nested_group.path.sub('foo', 'bar')) }
+
+ context 'without follow_redirects option' do
+ context 'with the given path not matching any route' do
+ it { expect(described_class.find_by_full_path('unknown')).to eq(nil) }
+ end
+
+ context 'with the given path matching the canonical route' do
+ it { expect(described_class.find_by_full_path(group.to_param)).to eq(group) }
+ it { expect(described_class.find_by_full_path(group.to_param.upcase)).to eq(group) }
+ it { expect(described_class.find_by_full_path(nested_group.to_param)).to eq(nested_group) }
+ end
+
+ context 'with the given path matching a redirect route' do
+ it { expect(described_class.find_by_full_path(group_redirect_route.path)).to eq(nil) }
+ it { expect(described_class.find_by_full_path(group_redirect_route.path.upcase)).to eq(nil) }
+ it { expect(described_class.find_by_full_path(nested_group_redirect_route.path)).to eq(nil) }
+ end
+ end
+
+ context 'with follow_redirects option set to true' do
+ context 'with the given path not matching any route' do
+ it { expect(described_class.find_by_full_path('unknown', follow_redirects: true)).to eq(nil) }
+ end
+
+ context 'with the given path matching the canonical route' do
+ it { expect(described_class.find_by_full_path(group.to_param, follow_redirects: true)).to eq(group) }
+ it { expect(described_class.find_by_full_path(group.to_param.upcase, follow_redirects: true)).to eq(group) }
+ it { expect(described_class.find_by_full_path(nested_group.to_param, follow_redirects: true)).to eq(nested_group) }
+ end
+
+ context 'with the given path matching a redirect route' do
+ it { expect(described_class.find_by_full_path(group_redirect_route.path, follow_redirects: true)).to eq(group) }
+ it { expect(described_class.find_by_full_path(group_redirect_route.path.upcase, follow_redirects: true)).to eq(group) }
+ it { expect(described_class.find_by_full_path(nested_group_redirect_route.path, follow_redirects: true)).to eq(nested_group) }
+ end
+ end
+ end
end
describe '.where_full_path_in' do
@@ -194,6 +238,24 @@ describe Group, 'Routable' do
it { expect(group.full_path).to eq(group.path) }
it { expect(nested_group.full_path).to eq("#{group.full_path}/#{nested_group.path}") }
+
+ context 'with RequestStore active' do
+ before do
+ RequestStore.begin!
+ end
+
+ after do
+ RequestStore.end!
+ RequestStore.clear!
+ end
+
+ it 'does not load the route table more than once' do
+ expect(group).to receive(:uncached_full_path).once.and_call_original
+
+ 3.times { group.full_path }
+ expect(group.full_path).to eq(group.path)
+ end
+ end
end
describe '#full_name' do
diff --git a/spec/models/concerns/spammable_spec.rb b/spec/models/concerns/spammable_spec.rb
index fd3b8307571..e698207166c 100644
--- a/spec/models/concerns/spammable_spec.rb
+++ b/spec/models/concerns/spammable_spec.rb
@@ -1,9 +1,11 @@
require 'spec_helper'
-describe Issue, 'Spammable' do
+describe Spammable do
let(:issue) { create(:issue, description: 'Test Desc.') }
describe 'Associations' do
+ subject { build(:issue) }
+
it { is_expected.to have_one(:user_agent_detail).dependent(:destroy) }
end
diff --git a/spec/models/concerns/strip_attribute_spec.rb b/spec/models/concerns/strip_attribute_spec.rb
index c3af7a0960f..8c945686b66 100644
--- a/spec/models/concerns/strip_attribute_spec.rb
+++ b/spec/models/concerns/strip_attribute_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Milestone, "StripAttribute" do
+describe StripAttribute do
let(:milestone) { create(:milestone) }
describe ".strip_attributes" do
diff --git a/spec/models/cycle_analytics/test_spec.rb b/spec/models/cycle_analytics/test_spec.rb
index c2ba012a0e6..d0b919efcf9 100644
--- a/spec/models/cycle_analytics/test_spec.rb
+++ b/spec/models/cycle_analytics/test_spec.rb
@@ -14,6 +14,7 @@ describe 'CycleAnalytics#test', feature: true do
issue = context.create(:issue, project: context.project)
merge_request = context.create_merge_request_closing_issue(issue)
pipeline = context.create(:ci_pipeline, ref: merge_request.source_branch, sha: merge_request.diff_head_sha, project: context.project)
+ merge_request.update(head_pipeline: pipeline)
{ pipeline: pipeline, issue: issue }
end,
start_time_conditions: [["pipeline is started", -> (context, data) { data[:pipeline].run! }]],
diff --git a/spec/models/deployment_spec.rb b/spec/models/deployment_spec.rb
index 080ff2f3f43..4bda7d4314a 100644
--- a/spec/models/deployment_spec.rb
+++ b/spec/models/deployment_spec.rb
@@ -49,6 +49,34 @@ describe Deployment, models: true do
end
end
+ describe '#metrics' do
+ let(:deployment) { create(:deployment) }
+
+ subject { deployment.metrics }
+
+ context 'metrics are disabled' do
+ it { is_expected.to eq({}) }
+ end
+
+ context 'metrics are enabled' do
+ let(:simple_metrics) do
+ {
+ success: true,
+ metrics: {},
+ last_update: 42,
+ deployment_time: 1494408956
+ }
+ end
+
+ before do
+ allow(deployment.project).to receive_message_chain(:monitoring_service, :deployment_metrics)
+ .with(any_args).and_return(simple_metrics)
+ end
+
+ it { is_expected.to eq(simple_metrics) }
+ end
+ end
+
describe '#stop_action' do
let(:build) { create(:ci_build) }
diff --git a/spec/models/diff_discussion_spec.rb b/spec/models/diff_discussion_spec.rb
index 48e7c0a822c..81f338745b1 100644
--- a/spec/models/diff_discussion_spec.rb
+++ b/spec/models/diff_discussion_spec.rb
@@ -1,19 +1,86 @@
require 'spec_helper'
describe DiffDiscussion, model: true do
- subject { described_class.new([first_note, second_note, third_note]) }
+ include RepoHelpers
- let(:first_note) { create(:diff_note_on_merge_request) }
- let(:merge_request) { first_note.noteable }
- let(:project) { first_note.project }
- let(:second_note) { create(:diff_note_on_merge_request, noteable: merge_request, project: project, in_reply_to: first_note) }
- let(:third_note) { create(:diff_note_on_merge_request, noteable: merge_request, project: project, in_reply_to: first_note) }
+ subject { described_class.new([diff_note]) }
+
+ let(:project) { create(:project) }
+ let(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
+ let(:diff_note) { create(:diff_note_on_merge_request, noteable: merge_request, project: project) }
describe '#reply_attributes' do
it 'includes position and original_position' do
attributes = subject.reply_attributes
- expect(attributes[:position]).to eq(first_note.position.to_json)
- expect(attributes[:original_position]).to eq(first_note.original_position.to_json)
+ expect(attributes[:position]).to eq(diff_note.position.to_json)
+ expect(attributes[:original_position]).to eq(diff_note.original_position.to_json)
+ end
+ end
+
+ describe '#merge_request_version_params' do
+ let(:merge_request) { create(:merge_request, source_project: project, target_project: project, importing: true) }
+ let!(:merge_request_diff1) { merge_request.merge_request_diffs.create(head_commit_sha: '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9') }
+ let!(:merge_request_diff2) { merge_request.merge_request_diffs.create(head_commit_sha: nil) }
+ let!(:merge_request_diff3) { merge_request.merge_request_diffs.create(head_commit_sha: '5937ac0a7beb003549fc5fd26fc247adbce4a52e') }
+
+ context 'when the discussion is active' do
+ it 'returns an empty hash, which will end up showing the latest version' do
+ expect(subject.merge_request_version_params).to eq({})
+ end
+ end
+
+ context 'when the discussion is on an older merge request version' do
+ let(:position) do
+ Gitlab::Diff::Position.new(
+ old_path: ".gitmodules",
+ new_path: ".gitmodules",
+ old_line: nil,
+ new_line: 4,
+ diff_refs: merge_request_diff1.diff_refs
+ )
+ end
+
+ let(:diff_note) { create(:diff_note_on_merge_request, noteable: merge_request, project: project, position: position) }
+
+ before do
+ diff_note.position = diff_note.original_position
+ diff_note.save!
+ end
+
+ it 'returns the diff ID for the version to show' do
+ expect(diff_id: merge_request_diff1.id)
+ end
+ end
+
+ context 'when the discussion is on a comparison between merge request versions' do
+ let(:position) do
+ Gitlab::Diff::Position.new(
+ old_path: ".gitmodules",
+ new_path: ".gitmodules",
+ old_line: 4,
+ new_line: 4,
+ diff_refs: merge_request_diff3.compare_with(merge_request_diff1.head_commit_sha).diff_refs
+ )
+ end
+
+ let(:diff_note) { create(:diff_note_on_merge_request, noteable: merge_request, project: project, position: position) }
+
+ it 'returns the diff ID and start sha of the versions to compare' do
+ expect(subject.merge_request_version_params).to eq(diff_id: merge_request_diff3.id, start_sha: merge_request_diff1.head_commit_sha)
+ end
+ end
+
+ context 'when the discussion does not have a merge request version' do
+ let(:diff_note) { create(:diff_note_on_merge_request, noteable: merge_request, project: project, diff_refs: project.commit(sample_commit.id).diff_refs) }
+
+ before do
+ diff_note.position = diff_note.original_position
+ diff_note.save!
+ end
+
+ it 'returns nil' do
+ expect(subject.merge_request_version_params).to be_nil
+ end
end
end
end
diff --git a/spec/models/diff_note_spec.rb b/spec/models/diff_note_spec.rb
index f32b6b99b3d..ab4c51a87b0 100644
--- a/spec/models/diff_note_spec.rb
+++ b/spec/models/diff_note_spec.rb
@@ -155,23 +155,6 @@ describe DiffNote, models: true do
end
end
- describe '#latest_merge_request_diff' do
- context 'when active' do
- it 'returns the current merge request diff' do
- expect(subject.latest_merge_request_diff).to eq(merge_request.merge_request_diff)
- end
- end
-
- context 'when outdated' do
- let!(:old_merge_request_diff) { merge_request.merge_request_diff }
- let!(:new_merge_request_diff) { merge_request.merge_request_diffs.create(diff_refs: commit.diff_refs) }
-
- it 'returns the latest merge request diff that this diff note applied to' do
- expect(subject.latest_merge_request_diff).to eq(old_merge_request_diff)
- end
- end
- end
-
describe "creation" do
describe "updating of position" do
context "when noteable is a commit" do
@@ -256,4 +239,39 @@ describe DiffNote, models: true do
end
end
end
+
+ describe '#created_at_diff?' do
+ let(:diff_refs) { project.commit(sample_commit.id).diff_refs }
+ let(:position) do
+ Gitlab::Diff::Position.new(
+ old_path: "files/ruby/popen.rb",
+ new_path: "files/ruby/popen.rb",
+ old_line: nil,
+ new_line: 14,
+ diff_refs: diff_refs
+ )
+ end
+
+ context "when noteable is a commit" do
+ subject { build(:diff_note_on_commit, project: project, position: position) }
+
+ it "returns true" do
+ expect(subject.created_at_diff?(diff_refs)).to be true
+ end
+ end
+
+ context "when noteable is a merge request" do
+ context "when the diff refs match the original one of the diff note" do
+ it "returns true" do
+ expect(subject.created_at_diff?(diff_refs)).to be true
+ end
+ end
+
+ context "when the diff refs don't match the original one of the diff note" do
+ it "returns false" do
+ expect(subject.created_at_diff?(merge_request.diff_refs)).to be false
+ end
+ end
+ end
+ end
end
diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb
index 070716e859a..12519de8636 100644
--- a/spec/models/environment_spec.rb
+++ b/spec/models/environment_spec.rb
@@ -206,25 +206,52 @@ describe Environment, models: true do
end
context 'when matching action is defined' do
- let(:build) { create(:ci_build) }
- let!(:deployment) { create(:deployment, environment: environment, deployable: build, on_stop: 'close_app') }
+ let(:pipeline) { create(:ci_pipeline, project: project) }
+ let(:build) { create(:ci_build, pipeline: pipeline) }
+
+ let!(:deployment) do
+ create(:deployment, environment: environment,
+ deployable: build,
+ on_stop: 'close_app')
+ end
- context 'when action did not yet finish' do
- let!(:close_action) { create(:ci_build, :manual, pipeline: build.pipeline, name: 'close_app') }
+ context 'when user is not allowed to stop environment' do
+ let!(:close_action) do
+ create(:ci_build, :manual, pipeline: pipeline, name: 'close_app')
+ end
- it 'returns the same action' do
- expect(subject).to eq(close_action)
- expect(subject.user).to eq(user)
+ it 'raises an exception' do
+ expect { subject }.to raise_error(Gitlab::Access::AccessDeniedError)
end
end
- context 'if action did finish' do
- let!(:close_action) { create(:ci_build, :manual, :success, pipeline: build.pipeline, name: 'close_app') }
+ context 'when user is allowed to stop environment' do
+ before do
+ project.add_master(user)
+ end
+
+ context 'when action did not yet finish' do
+ let!(:close_action) do
+ create(:ci_build, :manual, pipeline: pipeline, name: 'close_app')
+ end
+
+ it 'returns the same action' do
+ expect(subject).to eq(close_action)
+ expect(subject.user).to eq(user)
+ end
+ end
- it 'returns a new action of the same type' do
- is_expected.to be_persisted
- expect(subject.name).to eq(close_action.name)
- expect(subject.user).to eq(user)
+ context 'if action did finish' do
+ let!(:close_action) do
+ create(:ci_build, :manual, :success,
+ pipeline: pipeline, name: 'close_app')
+ end
+
+ it 'returns a new action of the same type' do
+ expect(subject).to be_persisted
+ expect(subject.name).to eq(close_action.name)
+ expect(subject.user).to eq(user)
+ end
end
end
end
@@ -366,7 +393,7 @@ describe Environment, models: true do
it 'returns the metrics from the deployment service' do
expect(project.monitoring_service)
- .to receive(:metrics).with(environment)
+ .to receive(:environment_metrics).with(environment)
.and_return(:fake_metrics)
is_expected.to eq(:fake_metrics)
@@ -411,7 +438,7 @@ describe Environment, models: true do
"foo**bar" => "foo-bar" + SUFFIX,
"*-foo" => "env-foo" + SUFFIX,
"staging-12345678-" => "staging-12345678" + SUFFIX,
- "staging-12345678-01234567" => "staging-12345678" + SUFFIX,
+ "staging-12345678-01234567" => "staging-12345678" + SUFFIX
}.each do |name, matcher|
it "returns a slug matching #{matcher}, given #{name}" do
slug = described_class.new(name: name).generate_slug
diff --git a/spec/models/event_spec.rb b/spec/models/event_spec.rb
index 8c90a538f57..b8cb967c4cc 100644
--- a/spec/models/event_spec.rb
+++ b/spec/models/event_spec.rb
@@ -15,13 +15,39 @@ describe Event, models: true do
end
describe 'Callbacks' do
- describe 'after_create :reset_project_activity' do
- let(:project) { create(:empty_project) }
+ let(:project) { create(:empty_project) }
+ describe 'after_create :reset_project_activity' do
it 'calls the reset_project_activity method' do
expect_any_instance_of(described_class).to receive(:reset_project_activity)
- create_event(project, project.owner)
+ create_push_event(project, project.owner)
+ end
+ end
+
+ describe 'after_create :set_last_repository_updated_at' do
+ context 'with a push event' do
+ it 'updates the project last_repository_updated_at' do
+ project.update(last_repository_updated_at: 1.year.ago)
+
+ create_push_event(project, project.owner)
+
+ project.reload
+
+ expect(project.last_repository_updated_at).to be_within(1.minute).of(Time.now)
+ end
+ end
+
+ context 'without a push event' do
+ it 'does not update the project last_repository_updated_at' do
+ project.update(last_repository_updated_at: 1.year.ago)
+
+ create(:closed_issue_event, project: project, author: project.owner)
+
+ project.reload
+
+ expect(project.last_repository_updated_at).to be_within(1.minute).of(1.year.ago)
+ end
end
end
end
@@ -29,7 +55,7 @@ describe Event, models: true do
describe "Push event" do
let(:project) { create(:empty_project, :private) }
let(:user) { project.owner }
- let(:event) { create_event(project, user) }
+ let(:event) { create_push_event(project, user) }
it do
expect(event.push?).to be_truthy
@@ -92,8 +118,8 @@ describe Event, models: true do
let(:author) { create(:author) }
let(:assignee) { create(:user) }
let(:admin) { create(:admin) }
- let(:issue) { create(:issue, project: project, author: author, assignee: assignee) }
- let(:confidential_issue) { create(:issue, :confidential, project: project, author: author, assignee: assignee) }
+ let(:issue) { create(:issue, project: project, author: author, assignees: [assignee]) }
+ let(:confidential_issue) { create(:issue, :confidential, project: project, author: author, assignees: [assignee]) }
let(:note_on_commit) { create(:note_on_commit, project: project) }
let(:note_on_issue) { create(:note_on_issue, noteable: issue, project: project) }
let(:note_on_confidential_issue) { create(:note_on_issue, noteable: confidential_issue, project: project) }
@@ -243,7 +269,7 @@ describe Event, models: true do
expect(project).not_to receive(:update_column).
with(:last_activity_at, a_kind_of(Time))
- create_event(project, project.owner)
+ create_push_event(project, project.owner)
end
end
@@ -251,11 +277,11 @@ describe Event, models: true do
it 'updates the project' do
project.update(last_activity_at: 1.year.ago)
- create_event(project, project.owner)
+ create_push_event(project, project.owner)
project.reload
- project.last_activity_at <= 1.minute.ago
+ expect(project.last_activity_at).to be_within(1.minute).of(Time.now)
end
end
end
@@ -278,7 +304,7 @@ describe Event, models: true do
end
end
- def create_event(project, user, attrs = {})
+ def create_push_event(project, user, attrs = {})
data = {
before: Gitlab::Git::BLANK_SHA,
after: "0220c11b9a3e6c69dc8fd35321254ca9a7b98f7e",
diff --git a/spec/models/global_milestone_spec.rb b/spec/models/global_milestone_spec.rb
index 55b87d1c48a..a14efda3eda 100644
--- a/spec/models/global_milestone_spec.rb
+++ b/spec/models/global_milestone_spec.rb
@@ -137,7 +137,7 @@ describe GlobalMilestone, models: true do
[
milestone1_project1,
milestone1_project2,
- milestone1_project3,
+ milestone1_project3
]
milestones_relation = Milestone.where(id: milestones.map(&:id))
diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb
index 8ffde6f7fbb..6ca1eb0374d 100644
--- a/spec/models/group_spec.rb
+++ b/spec/models/group_spec.rb
@@ -57,6 +57,32 @@ describe Group, models: true do
it { is_expected.not_to validate_presence_of :owner }
it { is_expected.to validate_presence_of :two_factor_grace_period }
it { is_expected.to validate_numericality_of(:two_factor_grace_period).is_greater_than_or_equal_to(0) }
+
+ describe 'path validation' do
+ it 'rejects paths reserved on the root namespace when the group has no parent' do
+ group = build(:group, path: 'api')
+
+ expect(group).not_to be_valid
+ end
+
+ it 'allows root paths when the group has a parent' do
+ group = build(:group, path: 'api', parent: create(:group))
+
+ expect(group).to be_valid
+ end
+
+ it 'rejects any wildcard paths when not a top level group' do
+ group = build(:group, path: 'tree', parent: create(:group))
+
+ expect(group).not_to be_valid
+ end
+
+ it 'rejects reserved group paths' do
+ group = build(:group, path: 'activity', parent: create(:group))
+
+ expect(group).not_to be_valid
+ end
+ end
end
describe '.visible_to_user' do
@@ -149,6 +175,26 @@ describe Group, models: true do
end
end
+ describe '#avatar_url' do
+ let!(:group) { create(:group, :access_requestable, :with_avatar) }
+ let(:user) { create(:user) }
+ let(:gitlab_host) { "http://#{Gitlab.config.gitlab.host}" }
+ let(:avatar_path) { "/uploads/group/avatar/#{group.id}/dk.png" }
+
+ context 'when avatar file is uploaded' do
+ before { group.add_master(user) }
+
+ it 'shows correct avatar url' do
+ expect(group.avatar_url).to eq(avatar_path)
+ expect(group.avatar_url(only_path: false)).to eq([gitlab_host, avatar_path].join)
+
+ allow(ActionController::Base).to receive(:asset_host).and_return(gitlab_host)
+
+ expect(group.avatar_url).to eq([gitlab_host, avatar_path].join)
+ end
+ end
+ end
+
describe '.search' do
it 'returns groups with a matching name' do
expect(described_class.search(group.name)).to eq([group])
diff --git a/spec/models/hooks/system_hook_spec.rb b/spec/models/hooks/system_hook_spec.rb
index 8acec805584..4340170888d 100644
--- a/spec/models/hooks/system_hook_spec.rb
+++ b/spec/models/hooks/system_hook_spec.rb
@@ -1,6 +1,19 @@
require "spec_helper"
describe SystemHook, models: true do
+ context 'default attributes' do
+ let(:system_hook) { build(:system_hook) }
+
+ it 'sets defined default parameters' do
+ attrs = {
+ push_events: false,
+ repository_update_events: true,
+ enable_ssl_verification: true
+ }
+ expect(system_hook).to have_attributes(attrs)
+ end
+ end
+
describe "execute" do
let(:system_hook) { create(:system_hook) }
let(:user) { create(:user) }
@@ -105,4 +118,12 @@ describe SystemHook, models: true do
).once
end
end
+
+ describe '.repository_update_hooks' do
+ it 'returns hooks for repository update events only' do
+ hook = create(:system_hook, repository_update_events: true)
+ create(:system_hook, repository_update_events: false)
+ expect(SystemHook.repository_update_hooks).to eq([hook])
+ end
+ end
end
diff --git a/spec/models/issue_collection_spec.rb b/spec/models/issue_collection_spec.rb
index d8aed25c041..93c2c538e10 100644
--- a/spec/models/issue_collection_spec.rb
+++ b/spec/models/issue_collection_spec.rb
@@ -28,7 +28,7 @@ describe IssueCollection do
end
it 'returns the issues the user is assigned to' do
- issue1.assignee = user
+ issue1.assignees << user
expect(collection.updatable_by_user(user)).to eq([issue1])
end
diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb
index 11befd4edfe..bb4e70db2e9 100644
--- a/spec/models/issue_spec.rb
+++ b/spec/models/issue_spec.rb
@@ -3,6 +3,7 @@ require 'spec_helper'
describe Issue, models: true do
describe "Associations" do
it { is_expected.to belong_to(:milestone) }
+ it { is_expected.to have_many(:assignees) }
end
describe 'modules' do
@@ -37,6 +38,24 @@ describe Issue, models: true do
end
end
+ describe '#card_attributes' do
+ it 'includes the author name' do
+ allow(subject).to receive(:author).and_return(double(name: 'Robert'))
+ allow(subject).to receive(:assignees).and_return([])
+
+ expect(subject.card_attributes).
+ to eq({ 'Author' => 'Robert', 'Assignee' => '' })
+ end
+
+ it 'includes the assignee name' do
+ allow(subject).to receive(:author).and_return(double(name: 'Robert'))
+ allow(subject).to receive(:assignees).and_return([double(name: 'Douwe')])
+
+ expect(subject.card_attributes).
+ to eq({ 'Author' => 'Robert', 'Assignee' => 'Douwe' })
+ end
+ end
+
describe '#closed_at' do
after do
Timecop.return
@@ -124,13 +143,24 @@ describe Issue, models: true do
end
end
- describe '#is_being_reassigned?' do
- it 'returns true if the issue assignee has changed' do
- subject.assignee = create(:user)
- expect(subject.is_being_reassigned?).to be_truthy
+ describe '#assignee_or_author?' do
+ let(:user) { create(:user) }
+ let(:issue) { create(:issue) }
+
+ it 'returns true for a user that is assigned to an issue' do
+ issue.assignees << user
+
+ expect(issue.assignee_or_author?(user)).to be_truthy
end
- it 'returns false if the issue assignee has not changed' do
- expect(subject.is_being_reassigned?).to be_falsey
+
+ it 'returns true for a user that is the author of an issue' do
+ issue.update(author: user)
+
+ expect(issue.assignee_or_author?(user)).to be_truthy
+ end
+
+ it 'returns false for a user that is not the assignee or author' do
+ expect(issue.assignee_or_author?(user)).to be_falsey
end
end
@@ -291,6 +321,27 @@ describe Issue, models: true do
end
end
+ describe '#has_related_branch?' do
+ let(:issue) { create(:issue, title: "Blue Bell Knoll") }
+ subject { issue.has_related_branch? }
+
+ context 'branch found' do
+ before do
+ allow(issue.project.repository).to receive(:branch_names).and_return(["iceblink-luck", issue.to_branch_name])
+ end
+
+ it { is_expected.to eq true }
+ end
+
+ context 'branch not found' do
+ before do
+ allow(issue.project.repository).to receive(:branch_names).and_return(["lazy-calm"])
+ end
+
+ it { is_expected.to eq false }
+ end
+ end
+
it_behaves_like 'an editable mentionable' do
subject { create(:issue, project: create(:project, :repository)) }
@@ -362,14 +413,14 @@ describe Issue, models: true do
user1 = create(:user)
user2 = create(:user)
project = create(:empty_project)
- issue = create(:issue, assignee: user1, project: project)
+ issue = create(:issue, assignees: [user1], project: project)
project.add_developer(user1)
project.add_developer(user2)
expect(user1.assigned_open_issues_count).to eq(1)
expect(user2.assigned_open_issues_count).to eq(0)
- issue.assignee = user2
+ issue.assignees = [user2]
issue.save
expect(user1.assigned_open_issues_count).to eq(0)
@@ -655,6 +706,11 @@ describe Issue, models: true do
expect(attrs_hash).to include(:human_total_time_spent)
expect(attrs_hash).to include('time_estimate')
end
+
+ it 'includes assignee_ids and deprecated assignee_id' do
+ expect(attrs_hash).to include(:assignee_id)
+ expect(attrs_hash).to include(:assignee_ids)
+ end
end
describe '#check_for_spam' do
diff --git a/spec/models/legacy_diff_discussion_spec.rb b/spec/models/legacy_diff_discussion_spec.rb
index 153e757a0ef..6eb4a2aaf39 100644
--- a/spec/models/legacy_diff_discussion_spec.rb
+++ b/spec/models/legacy_diff_discussion_spec.rb
@@ -8,4 +8,26 @@ describe LegacyDiffDiscussion, models: true do
expect(subject.reply_attributes[:line_code]).to eq(subject.line_code)
end
end
+
+ describe '#merge_request_version_params' do
+ context 'when the discussion is active' do
+ before do
+ allow(subject).to receive(:active?).and_return(true)
+ end
+
+ it 'returns an empty hash, which will end up showing the latest version' do
+ expect(subject.merge_request_version_params).to eq({})
+ end
+ end
+
+ context 'when the discussion is outdated' do
+ before do
+ allow(subject).to receive(:active?).and_return(false)
+ end
+
+ it 'returns nil' do
+ expect(subject.merge_request_version_params).to be_nil
+ end
+ end
+ end
end
diff --git a/spec/models/member_spec.rb b/spec/models/member_spec.rb
index b0f3657d3b5..ccc3deac199 100644
--- a/spec/models/member_spec.rb
+++ b/spec/models/member_spec.rb
@@ -390,13 +390,15 @@ describe Member, models: true do
%w[project group].each do |source_type|
context "when source is a #{source_type}" do
let!(:source) { create(source_type, :public, :access_requestable) }
- let!(:user) { create(:user) }
let!(:admin) { create(:admin) }
+ let(:user1) { create(:user) }
+ let(:user2) { create(:user) }
it 'returns a <Source>Member objects' do
- members = described_class.add_users(source, [user], :master)
+ members = described_class.add_users(source, [user1, user2], :master)
expect(members).to be_a Array
+ expect(members.size).to eq(2)
expect(members.first).to be_a "#{source_type.classify}Member".constantize
expect(members.first).to be_persisted
end
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index 415d3e7b200..ce870fcc1d3 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -9,6 +9,7 @@ describe MergeRequest, models: true do
it { is_expected.to belong_to(:target_project).class_name('Project') }
it { is_expected.to belong_to(:source_project).class_name('Project') }
it { is_expected.to belong_to(:merge_user).class_name("User") }
+ it { is_expected.to belong_to(:assignee) }
it { is_expected.to have_many(:merge_request_diffs).dependent(:destroy) }
end
@@ -86,6 +87,44 @@ describe MergeRequest, models: true do
end
end
+ describe '#card_attributes' do
+ it 'includes the author name' do
+ allow(subject).to receive(:author).and_return(double(name: 'Robert'))
+ allow(subject).to receive(:assignee).and_return(nil)
+
+ expect(subject.card_attributes).
+ to eq({ 'Author' => 'Robert', 'Assignee' => nil })
+ end
+
+ it 'includes the assignee name' do
+ allow(subject).to receive(:author).and_return(double(name: 'Robert'))
+ allow(subject).to receive(:assignee).and_return(double(name: 'Douwe'))
+
+ expect(subject.card_attributes).
+ to eq({ 'Author' => 'Robert', 'Assignee' => 'Douwe' })
+ end
+ end
+
+ describe '#assignee_or_author?' do
+ let(:user) { create(:user) }
+
+ it 'returns true for a user that is assigned to a merge request' do
+ subject.assignee = user
+
+ expect(subject.assignee_or_author?(user)).to eq(true)
+ end
+
+ it 'returns true for a user that is the author of a merge request' do
+ subject.author = user
+
+ expect(subject.assignee_or_author?(user)).to eq(true)
+ end
+
+ it 'returns false for a user that is not the assignee or author' do
+ expect(subject.assignee_or_author?(user)).to eq(false)
+ end
+ end
+
describe '#cache_merge_request_closes_issues!' do
before do
subject.project.team << [subject.author, :developer]
@@ -199,10 +238,10 @@ describe MergeRequest, models: true do
end
context 'when there are no MR diffs' do
- it 'delegates to the compare object' do
+ it 'delegates to the compare object, setting no_collapse: true' do
merge_request.compare = double(:compare)
- expect(merge_request.compare).to receive(:diffs).with(options)
+ expect(merge_request.compare).to receive(:diffs).with(options.merge(no_collapse: true))
merge_request.diffs(options)
end
@@ -215,15 +254,22 @@ describe MergeRequest, models: true do
end
context 'when there are MR diffs' do
- before do
+ it 'returns the correct count' do
merge_request.save
+
+ expect(merge_request.diff_size).to eq('105')
end
- it 'returns the correct count' do
- expect(merge_request.diff_size).to eq(105)
+ it 'returns the correct overflow count' do
+ allow(Commit).to receive(:max_diff_options).and_return(max_files: 2)
+ merge_request.save
+
+ expect(merge_request.diff_size).to eq('2+')
end
it 'does not perform highlighting' do
+ merge_request.save
+
expect(Gitlab::Diff::Highlight).not_to receive(:new)
merge_request.diff_size
@@ -231,7 +277,7 @@ describe MergeRequest, models: true do
end
context 'when there are no MR diffs' do
- before do
+ def set_compare(merge_request)
merge_request.compare = CompareService.new(
merge_request.source_project,
merge_request.source_branch
@@ -242,10 +288,21 @@ describe MergeRequest, models: true do
end
it 'returns the correct count' do
- expect(merge_request.diff_size).to eq(105)
+ set_compare(merge_request)
+
+ expect(merge_request.diff_size).to eq('105')
+ end
+
+ it 'returns the correct overflow count' do
+ allow(Commit).to receive(:max_diff_options).and_return(max_files: 2)
+ set_compare(merge_request)
+
+ expect(merge_request.diff_size).to eq('2+')
end
it 'does not perform highlighting' do
+ set_compare(merge_request)
+
expect(Gitlab::Diff::Highlight).not_to receive(:new)
merge_request.diff_size
@@ -277,16 +334,6 @@ describe MergeRequest, models: true do
end
end
- describe '#is_being_reassigned?' do
- it 'returns true if the merge_request assignee has changed' do
- subject.assignee = create(:user)
- expect(subject.is_being_reassigned?).to be_truthy
- end
- it 'returns false if the merge request assignee has not changed' do
- expect(subject.is_being_reassigned?).to be_falsey
- end
- end
-
describe '#for_fork?' do
it 'returns true if the merge request is for a fork' do
subject.source_project = build_stubbed(:empty_project, namespace: create(:group))
@@ -671,13 +718,8 @@ describe MergeRequest, models: true do
describe '#head_pipeline' do
describe 'when the source project exists' do
it 'returns the latest pipeline' do
- pipeline = double(:ci_pipeline, ref: 'master')
-
- allow(subject).to receive(:diff_head_sha).and_return('123abc')
-
- expect(subject.source_project).to receive(:pipeline_for).
- with('master', '123abc').
- and_return(pipeline)
+ pipeline = create(:ci_empty_pipeline, project: subject.source_project, ref: 'master', status: 'running', sha: "123abc")
+ subject.update(head_pipeline: pipeline)
expect(subject.head_pipeline).to eq(pipeline)
end
@@ -1226,71 +1268,6 @@ describe MergeRequest, models: true do
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 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
-
- it 'returns a truthy value when the conflicts have to be resolved in an editor' do
- merge_request = create_merge_request('conflict-contains-conflict-markers')
-
- expect(merge_request.conflicts_can_be_resolved_in_ui?).to be_truthy
- end
- end
-
describe "#source_project_missing?" do
let(:project) { create(:empty_project) }
let(:fork_project) { create(:empty_project, forked_from_project: project) }
@@ -1415,11 +1392,15 @@ describe MergeRequest, models: true do
describe '#mergeable_with_slash_command?' do
def create_pipeline(status)
- create(:ci_pipeline_with_one_job,
+ pipeline = create(:ci_pipeline_with_one_job,
project: project,
ref: merge_request.source_branch,
sha: merge_request.diff_head_sha,
status: status)
+
+ merge_request.update(head_pipeline: pipeline)
+
+ pipeline
end
let(:project) { create(:project, :public, :repository, only_allow_merge_if_pipeline_succeeds: true) }
@@ -1536,4 +1517,23 @@ describe MergeRequest, models: true do
expect(subject.has_no_commits?).to be_truthy
end
end
+
+ describe '#merge_request_diff_for' do
+ subject { create(:merge_request, importing: true) }
+ let!(:merge_request_diff1) { subject.merge_request_diffs.create(head_commit_sha: '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9') }
+ let!(:merge_request_diff2) { subject.merge_request_diffs.create(head_commit_sha: nil) }
+ let!(:merge_request_diff3) { subject.merge_request_diffs.create(head_commit_sha: '5937ac0a7beb003549fc5fd26fc247adbce4a52e') }
+
+ context 'with diff refs' do
+ it 'returns the diffs' do
+ expect(subject.merge_request_diff_for(merge_request_diff1.diff_refs)).to eq(merge_request_diff1)
+ end
+ end
+
+ context 'with a commit SHA' do
+ it 'returns the diffs' do
+ expect(subject.merge_request_diff_for(merge_request_diff3.head_commit_sha)).to eq(merge_request_diff3)
+ end
+ end
+ end
end
diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb
index e406d0a16bd..8624616316c 100644
--- a/spec/models/namespace_spec.rb
+++ b/spec/models/namespace_spec.rb
@@ -34,6 +34,13 @@ describe Namespace, models: true do
let(:group) { build(:group, :nested, path: 'tree') }
it { expect(group).not_to be_valid }
+
+ it 'rejects nested paths' do
+ parent = create(:group, :nested, path: 'environments')
+ namespace = build(:project, path: 'folders', namespace: parent)
+
+ expect(namespace).not_to be_valid
+ end
end
context 'top-level group' do
@@ -47,6 +54,7 @@ describe Namespace, models: true do
describe "Respond to" do
it { is_expected.to respond_to(:human_name) }
it { is_expected.to respond_to(:to_param) }
+ it { is_expected.to respond_to(:has_parent?) }
end
describe '#to_param' do
diff --git a/spec/models/network/graph_spec.rb b/spec/models/network/graph_spec.rb
index 492c4e01bd8..0fe8a591a45 100644
--- a/spec/models/network/graph_spec.rb
+++ b/spec/models/network/graph_spec.rb
@@ -9,4 +9,40 @@ describe Network::Graph, models: true do
expect(graph.notes).to eq( { note_on_commit.commit_id => 1 } )
end
+
+ describe '#commits' do
+ let(:graph) { described_class.new(project, 'refs/heads/master', project.repository.commit, nil) }
+
+ it 'returns a list of commits' do
+ commits = graph.commits
+
+ expect(commits).not_to be_empty
+ expect(commits).to all( be_kind_of(Network::Commit) )
+ end
+
+ it 'it the commits by commit date (descending)' do
+ # Remove duplicate timestamps because they make it harder to
+ # assert that the commits are sorted as expected.
+ commits = graph.commits.uniq(&:date)
+ sorted_commits = commits.sort_by(&:date).reverse
+
+ expect(commits).not_to be_empty
+ expect(commits.map(&:id)).to eq(sorted_commits.map(&:id))
+ end
+
+ it 'sorts children before parents for commits with the same timestamp' do
+ commits_by_time = graph.commits.group_by(&:date)
+
+ commits_by_time.each do |time, commits|
+ commit_ids = commits.map(&:id)
+
+ commits.each_with_index do |commit, index|
+ parent_indexes = commit.parent_ids.map { |parent_id| commit_ids.find_index(parent_id) }.compact
+
+ # All parents of the current commit should appear after it
+ expect(parent_indexes).to all( be > index )
+ end
+ end
+ end
+ end
end
diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb
index 557ea97b008..7a01cef9b4b 100644
--- a/spec/models/note_spec.rb
+++ b/spec/models/note_spec.rb
@@ -272,9 +272,9 @@ describe Note, models: true 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
+ old_line: nil,
+ new_line: 13,
+ diff_refs: project.commit(sample_commit.id).diff_refs
)
end
@@ -288,26 +288,78 @@ describe Note, models: true do
)
end
- subject { merge_request.notes.grouped_diff_discussions }
+ context 'active diff discussions' do
+ subject { merge_request.notes.grouped_diff_discussions }
- it "includes active discussions" do
- discussions = subject.values.flatten
+ it "includes active discussions" do
+ discussions = subject.values.flatten
- 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.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
+ 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.flatten.map(&:id)).not_to include(outdated_diff_note1.discussion_id)
+ it "doesn't include outdated discussions" do
+ expect(subject.values.flatten.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].first.id).to eq(active_diff_note1.discussion_id)
+ expect(subject[active_diff_note3.line_code].first.id).to eq(active_diff_note3.discussion_id)
+ end
end
- it "groups the discussions by line code" do
- expect(subject[active_diff_note1.line_code].first.id).to eq(active_diff_note1.discussion_id)
- expect(subject[active_diff_note3.line_code].first.id).to eq(active_diff_note3.discussion_id)
+ context 'diff discussions for older diff refs' do
+ subject { merge_request.notes.grouped_diff_discussions(diff_refs) }
+
+ context 'for diff refs a discussion was created at' do
+ let(:diff_refs) { active_position2.diff_refs }
+
+ it "includes discussions that were created then" do
+ discussions = subject.values.flatten
+
+ expect(discussions.count).to eq(1)
+
+ discussion = discussions.first
+
+ expect(discussion.id).to eq(active_diff_note3.discussion_id)
+ expect(discussion.active?).to be true
+ expect(discussion.active?(diff_refs)).to be false
+ expect(discussion.created_at_diff?(diff_refs)).to be true
+
+ expect(discussion.notes).to eq([active_diff_note3])
+ end
+
+ it "groups the discussions by original line code" do
+ expect(subject[active_diff_note3.original_line_code].first.id).to eq(active_diff_note3.discussion_id)
+ end
+ end
+
+ context 'for diff refs a discussion was last active at' do
+ let(:diff_refs) { outdated_position.diff_refs }
+
+ it "includes discussions that were last active" do
+ discussions = subject.values.flatten
+
+ expect(discussions.count).to eq(1)
+
+ discussion = discussions.first
+
+ expect(discussion.id).to eq(outdated_diff_note1.discussion_id)
+ expect(discussion.active?).to be false
+ expect(discussion.active?(diff_refs)).to be true
+ expect(discussion.created_at_diff?(diff_refs)).to be true
+
+ expect(discussion.notes).to eq([outdated_diff_note1, outdated_diff_note2])
+ end
+
+ it "groups the discussions by line code" do
+ expect(subject[outdated_diff_note1.line_code].first.id).to eq(outdated_diff_note1.discussion_id)
+ end
+ end
end
end
diff --git a/spec/models/project_authorization_spec.rb b/spec/models/project_authorization_spec.rb
index 33ef67f97a7..cd0a4a94809 100644
--- a/spec/models/project_authorization_spec.rb
+++ b/spec/models/project_authorization_spec.rb
@@ -16,7 +16,7 @@ describe ProjectAuthorization do
it 'inserts rows in batches' do
described_class.insert_authorizations([
[user.id, project1.id, Gitlab::Access::MASTER],
- [user.id, project2.id, Gitlab::Access::MASTER],
+ [user.id, project2.id, Gitlab::Access::MASTER]
], 1)
expect(user.project_authorizations.count).to eq(2)
diff --git a/spec/models/project_services/asana_service_spec.rb b/spec/models/project_services/asana_service_spec.rb
index 48aef3a93f2..95c35162d96 100644
--- a/spec/models/project_services/asana_service_spec.rb
+++ b/spec/models/project_services/asana_service_spec.rb
@@ -28,7 +28,7 @@ describe AsanaService, models: true do
commits: messages.map do |m|
{
message: m,
- url: 'https://gitlab.com/',
+ url: 'https://gitlab.com/'
}
end
}
diff --git a/spec/models/project_services/chat_message/issue_message_spec.rb b/spec/models/project_services/chat_message/issue_message_spec.rb
index 34e2d94b1ed..c159ab00ab1 100644
--- a/spec/models/project_services/chat_message/issue_message_spec.rb
+++ b/spec/models/project_services/chat_message/issue_message_spec.rb
@@ -48,7 +48,7 @@ describe ChatMessage::IssueMessage, models: true do
title: "#100 Issue title",
title_link: "http://url.com",
text: "issue description",
- color: color,
+ color: color
}
])
end
diff --git a/spec/models/project_services/chat_message/merge_message_spec.rb b/spec/models/project_services/chat_message/merge_message_spec.rb
index fa0a1f4a5b7..61f17031172 100644
--- a/spec/models/project_services/chat_message/merge_message_spec.rb
+++ b/spec/models/project_services/chat_message/merge_message_spec.rb
@@ -22,7 +22,7 @@ describe ChatMessage::MergeMessage, models: true do
state: 'opened',
description: 'merge request description',
source_branch: 'source_branch',
- target_branch: 'target_branch',
+ target_branch: 'target_branch'
}
}
end
diff --git a/spec/models/project_services/chat_message/note_message_spec.rb b/spec/models/project_services/chat_message/note_message_spec.rb
index 7cd9c61ee2b..7996536218a 100644
--- a/spec/models/project_services/chat_message/note_message_spec.rb
+++ b/spec/models/project_services/chat_message/note_message_spec.rb
@@ -15,7 +15,7 @@ describe ChatMessage::NoteMessage, models: true do
project_url: 'http://somewhere.com',
repository: {
name: 'project_name',
- url: 'http://somewhere.com',
+ url: 'http://somewhere.com'
},
object_attributes: {
id: 10,
diff --git a/spec/models/project_services/chat_message/pipeline_message_spec.rb b/spec/models/project_services/chat_message/pipeline_message_spec.rb
index ec5c6c5e0ed..7d2599dc703 100644
--- a/spec/models/project_services/chat_message/pipeline_message_spec.rb
+++ b/spec/models/project_services/chat_message/pipeline_message_spec.rb
@@ -4,6 +4,7 @@ describe ChatMessage::PipelineMessage do
subject { described_class.new(args) }
let(:user) { { name: 'hacker' } }
+ let(:duration) { 7210 }
let(:args) do
{
object_attributes: {
@@ -26,7 +27,6 @@ describe ChatMessage::PipelineMessage do
context 'pipeline succeeded' do
let(:status) { 'success' }
let(:color) { 'good' }
- let(:duration) { 10 }
let(:message) { build_message('passed') }
it 'returns a message with information about succeeded build' do
@@ -39,7 +39,6 @@ describe ChatMessage::PipelineMessage do
context 'pipeline failed' do
let(:status) { 'failed' }
let(:color) { 'danger' }
- let(:duration) { 10 }
let(:message) { build_message }
it 'returns a message with information about failed build' do
@@ -63,8 +62,8 @@ describe ChatMessage::PipelineMessage do
def build_message(status_text = status, name = user[:name])
"<http://example.gitlab.com|project_name>:" \
" Pipeline <http://example.gitlab.com/pipelines/123|#123>" \
- " of <http://example.gitlab.com/commits/develop|develop> branch" \
- " by #{name} #{status_text} in #{duration} #{'second'.pluralize(duration)}"
+ " of branch `<http://example.gitlab.com/commits/develop|develop>`" \
+ " by #{name} #{status_text} in 02:00:10"
end
end
@@ -76,16 +75,15 @@ describe ChatMessage::PipelineMessage do
context 'pipeline succeeded' do
let(:status) { 'success' }
let(:color) { 'good' }
- let(:duration) { 10 }
let(:message) { build_markdown_message('passed') }
it 'returns a message with information about succeeded build' do
expect(subject.pretext).to be_empty
expect(subject.attachments).to eq(message)
expect(subject.activity).to eq({
- title: 'Pipeline [#123](http://example.gitlab.com/pipelines/123) of [develop](http://example.gitlab.com/commits/develop) branch by hacker passed',
+ title: 'Pipeline [#123](http://example.gitlab.com/pipelines/123) of branch `[develop](http://example.gitlab.com/commits/develop)` by hacker passed',
subtitle: 'in [project_name](http://example.gitlab.com)',
- text: 'in 10 seconds',
+ text: 'in 02:00:10',
image: ''
})
end
@@ -94,16 +92,15 @@ describe ChatMessage::PipelineMessage do
context 'pipeline failed' do
let(:status) { 'failed' }
let(:color) { 'danger' }
- let(:duration) { 10 }
let(:message) { build_markdown_message }
it 'returns a message with information about failed build' do
expect(subject.pretext).to be_empty
expect(subject.attachments).to eq(message)
expect(subject.activity).to eq({
- title: 'Pipeline [#123](http://example.gitlab.com/pipelines/123) of [develop](http://example.gitlab.com/commits/develop) branch by hacker failed',
+ title: 'Pipeline [#123](http://example.gitlab.com/pipelines/123) of branch `[develop](http://example.gitlab.com/commits/develop)` by hacker failed',
subtitle: 'in [project_name](http://example.gitlab.com)',
- text: 'in 10 seconds',
+ text: 'in 02:00:10',
image: ''
})
end
@@ -116,9 +113,9 @@ describe ChatMessage::PipelineMessage do
expect(subject.pretext).to be_empty
expect(subject.attachments).to eq(message)
expect(subject.activity).to eq({
- title: 'Pipeline [#123](http://example.gitlab.com/pipelines/123) of [develop](http://example.gitlab.com/commits/develop) branch by API failed',
+ title: 'Pipeline [#123](http://example.gitlab.com/pipelines/123) of branch `[develop](http://example.gitlab.com/commits/develop)` by API failed',
subtitle: 'in [project_name](http://example.gitlab.com)',
- text: 'in 10 seconds',
+ text: 'in 02:00:10',
image: ''
})
end
@@ -128,8 +125,8 @@ describe ChatMessage::PipelineMessage do
def build_markdown_message(status_text = status, name = user[:name])
"[project_name](http://example.gitlab.com):" \
" Pipeline [#123](http://example.gitlab.com/pipelines/123)" \
- " of [develop](http://example.gitlab.com/commits/develop)" \
- " branch by #{name} #{status_text} in #{duration} #{'second'.pluralize(duration)}"
+ " of branch `[develop](http://example.gitlab.com/commits/develop)`" \
+ " by #{name} #{status_text} in 02:00:10"
end
end
end
diff --git a/spec/models/project_services/chat_message/push_message_spec.rb b/spec/models/project_services/chat_message/push_message_spec.rb
index 63eb078c44e..e38117b75f6 100644
--- a/spec/models/project_services/chat_message/push_message_spec.rb
+++ b/spec/models/project_services/chat_message/push_message_spec.rb
@@ -21,19 +21,19 @@ describe ChatMessage::PushMessage, models: true do
before do
args[:commits] = [
{ message: 'message1', url: 'http://url1.com', id: 'abcdefghijkl', author: { name: 'author1' } },
- { message: 'message2', url: 'http://url2.com', id: '123456789012', author: { name: 'author2' } },
+ { message: 'message2', url: 'http://url2.com', id: '123456789012', author: { name: 'author2' } }
]
end
context 'without markdown' do
it 'returns a message regarding pushes' do
expect(subject.pretext).to eq(
- 'test.user pushed to branch <http://url.com/commits/master|master> of '\
+ 'test.user pushed to branch `<http://url.com/commits/master|master>` of '\
'<http://url.com|project_name> (<http://url.com/compare/before...after|Compare changes>)')
expect(subject.attachments).to eq([{
text: "<http://url1.com|abcdefgh>: message1 - author1\n\n"\
"<http://url2.com|12345678>: message2 - author2",
- color: color,
+ color: color
}])
end
end
@@ -45,7 +45,7 @@ describe ChatMessage::PushMessage, models: true do
it 'returns a message regarding pushes' do
expect(subject.pretext).to eq(
- 'test.user pushed to branch [master](http://url.com/commits/master) of [project_name](http://url.com) ([Compare changes](http://url.com/compare/before...after))')
+ 'test.user pushed to branch `[master](http://url.com/commits/master)` of [project_name](http://url.com) ([Compare changes](http://url.com/compare/before...after))')
expect(subject.attachments).to eq(
"[abcdefgh](http://url1.com): message1 - author1\n\n[12345678](http://url2.com): message2 - author2")
expect(subject.activity).to eq({
@@ -74,7 +74,7 @@ describe ChatMessage::PushMessage, models: true do
context 'without markdown' do
it 'returns a message regarding pushes' do
expect(subject.pretext).to eq('test.user pushed new tag ' \
- '<http://url.com/commits/new_tag|new_tag> to ' \
+ '`<http://url.com/commits/new_tag|new_tag>` to ' \
'<http://url.com|project_name>')
expect(subject.attachments).to be_empty
end
@@ -87,7 +87,7 @@ describe ChatMessage::PushMessage, models: true do
it 'returns a message regarding pushes' do
expect(subject.pretext).to eq(
- 'test.user pushed new tag [new_tag](http://url.com/commits/new_tag) to [project_name](http://url.com)')
+ 'test.user pushed new tag `[new_tag](http://url.com/commits/new_tag)` to [project_name](http://url.com)')
expect(subject.attachments).to be_empty
expect(subject.activity).to eq({
title: 'test.user created tag',
@@ -107,7 +107,7 @@ describe ChatMessage::PushMessage, models: true do
context 'without markdown' do
it 'returns a message regarding a new branch' do
expect(subject.pretext).to eq(
- 'test.user pushed new branch <http://url.com/commits/master|master> to '\
+ 'test.user pushed new branch `<http://url.com/commits/master|master>` to '\
'<http://url.com|project_name>')
expect(subject.attachments).to be_empty
end
@@ -120,7 +120,7 @@ describe ChatMessage::PushMessage, models: true do
it 'returns a message regarding a new branch' do
expect(subject.pretext).to eq(
- 'test.user pushed new branch [master](http://url.com/commits/master) to [project_name](http://url.com)')
+ 'test.user pushed new branch `[master](http://url.com/commits/master)` to [project_name](http://url.com)')
expect(subject.attachments).to be_empty
expect(subject.activity).to eq({
title: 'test.user created branch',
@@ -140,7 +140,7 @@ describe ChatMessage::PushMessage, models: true do
context 'without markdown' do
it 'returns a message regarding a removed branch' do
expect(subject.pretext).to eq(
- 'test.user removed branch master from <http://url.com|project_name>')
+ 'test.user removed branch `master` from <http://url.com|project_name>')
expect(subject.attachments).to be_empty
end
end
@@ -152,7 +152,7 @@ describe ChatMessage::PushMessage, models: true do
it 'returns a message regarding a removed branch' do
expect(subject.pretext).to eq(
- 'test.user removed branch master from [project_name](http://url.com)')
+ 'test.user removed branch `master` from [project_name](http://url.com)')
expect(subject.attachments).to be_empty
expect(subject.activity).to eq({
title: 'test.user removed branch',
diff --git a/spec/models/project_services/chat_message/wiki_page_message_spec.rb b/spec/models/project_services/chat_message/wiki_page_message_spec.rb
index 0df7db2abc2..4ca1b8aa7b7 100644
--- a/spec/models/project_services/chat_message/wiki_page_message_spec.rb
+++ b/spec/models/project_services/chat_message/wiki_page_message_spec.rb
@@ -53,7 +53,7 @@ describe ChatMessage::WikiPageMessage, models: true do
expect(subject.attachments).to eq([
{
text: "Wiki page description",
- color: color,
+ color: color
}
])
end
@@ -66,7 +66,7 @@ describe ChatMessage::WikiPageMessage, models: true do
expect(subject.attachments).to eq([
{
text: "Wiki page description",
- color: color,
+ color: color
}
])
end
diff --git a/spec/models/project_services/chat_notification_service_spec.rb b/spec/models/project_services/chat_notification_service_spec.rb
index 592c90cda36..8fbe42248ae 100644
--- a/spec/models/project_services/chat_notification_service_spec.rb
+++ b/spec/models/project_services/chat_notification_service_spec.rb
@@ -11,10 +11,10 @@ describe ChatNotificationService, models: true do
describe '#can_test?' do
context 'with empty repository' do
- it 'returns false' do
+ it 'returns true' do
subject.project = create(:empty_project, :empty_repo)
- expect(subject.can_test?).to be false
+ expect(subject.can_test?).to be true
end
end
diff --git a/spec/models/project_services/issue_tracker_service_spec.rb b/spec/models/project_services/issue_tracker_service_spec.rb
index fbe6f344a98..869b25b933b 100644
--- a/spec/models/project_services/issue_tracker_service_spec.rb
+++ b/spec/models/project_services/issue_tracker_service_spec.rb
@@ -8,7 +8,7 @@ describe IssueTrackerService, models: true do
let(:service) { RedmineService.new(project: project, active: true) }
before do
- create(:service, project: project, active: true, category: 'issue_tracker')
+ create(:custom_issue_tracker_service, project: project)
end
context 'when service is changed manually by user' do
diff --git a/spec/models/project_services/kubernetes_service_spec.rb b/spec/models/project_services/kubernetes_service_spec.rb
index e69eb0098dd..c1c2f2a7219 100644
--- a/spec/models/project_services/kubernetes_service_spec.rb
+++ b/spec/models/project_services/kubernetes_service_spec.rb
@@ -54,7 +54,7 @@ describe KubernetesService, models: true, caching: true do
'a' * 63 => true,
'a' * 64 => false,
'a.b' => false,
- 'a*b' => false,
+ 'a*b' => false
}.each do |namespace, validity|
it "validates #{namespace} as #{validity ? 'valid' : 'invalid'}" do
subject.namespace = namespace
@@ -168,7 +168,7 @@ describe KubernetesService, models: true, caching: true do
{ key: 'KUBE_TOKEN', value: 'token', public: false },
{ key: 'KUBE_NAMESPACE', value: 'my-project', public: true },
{ key: 'KUBE_CA_PEM', value: 'CA PEM DATA', public: true },
- { key: 'KUBE_CA_PEM_FILE', value: 'CA PEM DATA', public: true, file: true },
+ { key: 'KUBE_CA_PEM_FILE', value: 'CA PEM DATA', public: true, file: true }
)
end
end
@@ -179,7 +179,7 @@ describe KubernetesService, models: true, caching: true do
{ key: 'KUBE_URL', value: 'https://kube.domain.com', public: true },
{ key: 'KUBE_TOKEN', value: 'token', public: false },
{ key: 'KUBE_CA_PEM', value: 'CA PEM DATA', public: true },
- { key: 'KUBE_CA_PEM_FILE', value: 'CA PEM DATA', public: true, file: true },
+ { key: 'KUBE_CA_PEM_FILE', value: 'CA PEM DATA', public: true, file: true }
)
end
diff --git a/spec/models/project_services/pipeline_email_service_spec.rb b/spec/models/project_services/pipelines_email_service_spec.rb
index 03932895b0e..03932895b0e 100644
--- a/spec/models/project_services/pipeline_email_service_spec.rb
+++ b/spec/models/project_services/pipelines_email_service_spec.rb
diff --git a/spec/models/project_services/pivotaltracker_service_spec.rb b/spec/models/project_services/pivotaltracker_service_spec.rb
index 45b2f1068bf..a76e909d04d 100644
--- a/spec/models/project_services/pivotaltracker_service_spec.rb
+++ b/spec/models/project_services/pivotaltracker_service_spec.rb
@@ -40,7 +40,7 @@ describe PivotaltrackerService, models: true do
name: 'Some User'
},
url: 'https://example.com/commit',
- message: 'commit message',
+ message: 'commit message'
}
]
}
diff --git a/spec/models/project_services/prometheus_service_spec.rb b/spec/models/project_services/prometheus_service_spec.rb
index d15079b686b..1f9d3c07b51 100644
--- a/spec/models/project_services/prometheus_service_spec.rb
+++ b/spec/models/project_services/prometheus_service_spec.rb
@@ -6,6 +6,7 @@ describe PrometheusService, models: true, caching: true do
let(:project) { create(:prometheus_project) }
let(:service) { project.prometheus_service }
+ let(:environment_query) { Gitlab::Prometheus::Queries::EnvironmentQuery }
describe "Associations" do
it { is_expected.to belong_to :project }
@@ -45,17 +46,18 @@ describe PrometheusService, models: true, caching: true do
end
end
- describe '#metrics' do
+ describe '#environment_metrics' do
let(:environment) { build_stubbed(:environment, slug: 'env-slug') }
- subject { service.metrics(environment) }
around do |example|
Timecop.freeze { example.run }
end
context 'with valid data' do
+ subject { service.environment_metrics(environment) }
+
before do
- stub_reactive_cache(service, prometheus_data, 'env-slug')
+ stub_reactive_cache(service, prometheus_data, environment_query, environment.id)
end
it 'returns reactive data' do
@@ -64,15 +66,36 @@ describe PrometheusService, models: true, caching: true do
end
end
+ describe '#deployment_metrics' do
+ let(:deployment) { build_stubbed(:deployment)}
+ let(:deployment_query) { Gitlab::Prometheus::Queries::DeploymentQuery }
+
+ around do |example|
+ Timecop.freeze { example.run }
+ end
+
+ context 'with valid data' do
+ subject { service.deployment_metrics(deployment) }
+
+ before do
+ stub_reactive_cache(service, prometheus_data, deployment_query, deployment.id)
+ end
+
+ it 'returns reactive data' do
+ is_expected.to eq(prometheus_data.merge(deployment_time: deployment.created_at.to_i))
+ end
+ end
+ end
+
describe '#calculate_reactive_cache' do
- let(:environment) { build_stubbed(:environment, slug: 'env-slug') }
+ let(:environment) { create(:environment, slug: 'env-slug') }
around do |example|
Timecop.freeze { example.run }
end
subject do
- service.calculate_reactive_cache(environment.slug)
+ service.calculate_reactive_cache(environment_query.to_s, environment.id)
end
context 'when service is inactive' do
@@ -94,7 +117,7 @@ describe PrometheusService, models: true, caching: true do
[404, 500].each do |status|
context "when Prometheus responds with #{status}" do
before do
- stub_all_prometheus_requests(environment.slug, status: status, body: 'QUERY FAILED!')
+ stub_all_prometheus_requests(environment.slug, status: status, body: "QUERY FAILED!")
end
it { is_expected.to eq(success: false, result: %(#{status} - "QUERY FAILED!")) }
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 92d420337f9..f2b4e9070b4 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -73,6 +73,7 @@ describe Project, models: true do
it { is_expected.to have_many(:notification_settings).dependent(:destroy) }
it { is_expected.to have_many(:forks).through(:forked_project_links) }
it { is_expected.to have_many(:uploads).dependent(:destroy) }
+ it { is_expected.to have_many(:pipeline_schedules).dependent(:destroy) }
context 'after initialized' do
it "has a project_feature" do
@@ -253,6 +254,34 @@ describe Project, models: true do
expect(new_project.errors.full_messages.first).to eq('The project is still being deleted. Please try again later.')
end
end
+
+ describe 'path validation' do
+ it 'allows paths reserved on the root namespace' do
+ project = build(:project, path: 'api')
+
+ expect(project).to be_valid
+ end
+
+ it 'rejects paths reserved on another level' do
+ project = build(:project, path: 'tree')
+
+ expect(project).not_to be_valid
+ end
+
+ it 'rejects nested paths' do
+ parent = create(:group, :nested, path: 'environments')
+ project = build(:project, path: 'folders', namespace: parent)
+
+ expect(project).not_to be_valid
+ end
+
+ it 'allows a reserved group name' do
+ parent = create(:group)
+ project = build(:project, path: 'avatar', namespace: parent)
+
+ expect(project).to be_valid
+ end
+ end
end
describe 'default_scope' do
@@ -781,17 +810,19 @@ describe Project, models: true do
let(:project) { create(:empty_project) }
- context 'When avatar file is uploaded' do
- before do
- project.update_columns(avatar: 'uploads/avatar.png')
- allow(project.avatar).to receive(:present?) { true }
- end
+ context 'when avatar file is uploaded' do
+ let(:project) { create(:empty_project, :with_avatar) }
+ let(:avatar_path) { "/uploads/project/avatar/#{project.id}/dk.png" }
+ let(:gitlab_host) { "http://#{Gitlab.config.gitlab.host}" }
- let(:avatar_path) do
- "/uploads/project/avatar/#{project.id}/uploads/avatar.png"
- end
+ it 'shows correct url' do
+ expect(project.avatar_url).to eq(avatar_path)
+ expect(project.avatar_url(only_path: false)).to eq([gitlab_host, avatar_path].join)
- it { should eq "http://#{Gitlab.config.gitlab.host}#{avatar_path}" }
+ allow(ActionController::Base).to receive(:asset_host).and_return(gitlab_host)
+
+ expect(project.avatar_url).to eq([gitlab_host, avatar_path].join)
+ end
end
context 'When avatar file in git' do
@@ -799,9 +830,7 @@ describe Project, models: true do
allow(project).to receive(:avatar_in_git) { true }
end
- let(:avatar_path) do
- "/#{project.full_path}/avatar"
- end
+ let(:avatar_path) { "/#{project.full_path}/avatar" }
it { should eq "http://#{Gitlab.config.gitlab.host}#{avatar_path}" }
end
@@ -944,7 +973,7 @@ describe Project, models: true do
before do
storages = {
'default' => { 'path' => 'tmp/tests/repositories' },
- 'picked' => { 'path' => 'tmp/tests/repositories' },
+ 'picked' => { 'path' => 'tmp/tests/repositories' }
}
allow(Gitlab.config.repositories).to receive(:storages).and_return(storages)
end
@@ -1881,4 +1910,31 @@ describe Project, models: true do
expect(project.pipeline_status).to be_loaded
end
end
+
+ describe '#append_or_update_attribute' do
+ let(:project) { create(:project) }
+
+ it 'shows full error updating an invalid MR' do
+ error_message = 'Failed to replace merge_requests because one or more of the new records could not be saved.'\
+ ' Validate fork Source project is not a fork of the target project'
+
+ expect { project.append_or_update_attribute(:merge_requests, [create(:merge_request)]) }.
+ to raise_error(ActiveRecord::RecordNotSaved, error_message)
+ end
+
+ it 'updates the project succesfully' do
+ merge_request = create(:merge_request, target_project: project, source_project: project)
+
+ expect { project.append_or_update_attribute(:merge_requests, [merge_request]) }.
+ not_to raise_error
+ end
+ end
+
+ describe '#last_repository_updated_at' do
+ it 'sets to created_at upon creation' do
+ project = create(:empty_project, created_at: 2.hours.ago)
+
+ expect(project.last_repository_updated_at.to_i).to eq(project.created_at.to_i)
+ end
+ end
end
diff --git a/spec/models/project_statistics_spec.rb b/spec/models/project_statistics_spec.rb
index ff29f6f66ba..c5ffbda9821 100644
--- a/spec/models/project_statistics_spec.rb
+++ b/spec/models/project_statistics_spec.rb
@@ -35,7 +35,7 @@ describe ProjectStatistics, models: true do
commit_count: 8.exabytes - 1,
repository_size: 2.exabytes,
lfs_objects_size: 2.exabytes,
- build_artifacts_size: 4.exabytes - 1,
+ build_artifacts_size: 4.exabytes - 1
)
statistics.reload
@@ -149,7 +149,7 @@ describe ProjectStatistics, models: true do
it "sums all storage counters" do
statistics.update!(
repository_size: 2,
- lfs_objects_size: 3,
+ lfs_objects_size: 3
)
statistics.reload
diff --git a/spec/models/project_wiki_spec.rb b/spec/models/project_wiki_spec.rb
index b5b9cd024b0..969e9f7a130 100644
--- a/spec/models/project_wiki_spec.rb
+++ b/spec/models/project_wiki_spec.rb
@@ -213,9 +213,12 @@ describe ProjectWiki, models: true do
end
it 'updates project activity' do
- expect(subject).to receive(:update_project_activity)
-
subject.create_page('Test Page', 'This is content')
+
+ project.reload
+
+ expect(project.last_activity_at).to be_within(1.minute).of(Time.now)
+ expect(project.last_repository_updated_at).to be_within(1.minute).of(Time.now)
end
end
@@ -240,9 +243,12 @@ describe ProjectWiki, models: true do
end
it 'updates project activity' do
- expect(subject).to receive(:update_project_activity)
-
subject.update_page(@gollum_page, 'Yet more content', :markdown, 'Updated page again')
+
+ project.reload
+
+ expect(project.last_activity_at).to be_within(1.minute).of(Time.now)
+ expect(project.last_repository_updated_at).to be_within(1.minute).of(Time.now)
end
end
@@ -258,9 +264,12 @@ describe ProjectWiki, models: true do
end
it 'updates project activity' do
- expect(subject).to receive(:update_project_activity)
-
subject.delete_page(@page)
+
+ project.reload
+
+ expect(project.last_activity_at).to be_within(1.minute).of(Time.now)
+ expect(project.last_repository_updated_at).to be_within(1.minute).of(Time.now)
end
end
diff --git a/spec/models/protected_branch/merge_access_level_spec.rb b/spec/models/protected_branch/merge_access_level_spec.rb
new file mode 100644
index 00000000000..1e7242e9fa8
--- /dev/null
+++ b/spec/models/protected_branch/merge_access_level_spec.rb
@@ -0,0 +1,5 @@
+require 'spec_helper'
+
+describe ProtectedBranch::MergeAccessLevel, :models do
+ it { is_expected.to validate_inclusion_of(:access_level).in_array([Gitlab::Access::MASTER, Gitlab::Access::DEVELOPER, Gitlab::Access::NO_ACCESS]) }
+end
diff --git a/spec/models/protected_branch/push_access_level_spec.rb b/spec/models/protected_branch/push_access_level_spec.rb
new file mode 100644
index 00000000000..de68351198c
--- /dev/null
+++ b/spec/models/protected_branch/push_access_level_spec.rb
@@ -0,0 +1,5 @@
+require 'spec_helper'
+
+describe ProtectedBranch::PushAccessLevel, :models do
+ it { is_expected.to validate_inclusion_of(:access_level).in_array([Gitlab::Access::MASTER, Gitlab::Access::DEVELOPER, Gitlab::Access::NO_ACCESS]) }
+end
diff --git a/spec/models/redirect_route_spec.rb b/spec/models/redirect_route_spec.rb
new file mode 100644
index 00000000000..71827421dd7
--- /dev/null
+++ b/spec/models/redirect_route_spec.rb
@@ -0,0 +1,27 @@
+require 'rails_helper'
+
+describe RedirectRoute, models: true do
+ let(:group) { create(:group) }
+ let!(:redirect_route) { group.redirect_routes.create(path: 'gitlabb') }
+
+ describe 'relationships' do
+ it { is_expected.to belong_to(:source) }
+ end
+
+ describe 'validations' do
+ it { is_expected.to validate_presence_of(:source) }
+ it { is_expected.to validate_presence_of(:path) }
+ it { is_expected.to validate_uniqueness_of(:path) }
+ end
+
+ describe '.matching_path_and_descendants' do
+ let!(:redirect2) { group.redirect_routes.create(path: 'gitlabb/test') }
+ let!(:redirect3) { group.redirect_routes.create(path: 'gitlabb/test/foo') }
+ let!(:redirect4) { group.redirect_routes.create(path: 'gitlabb/test/foo/bar') }
+ let!(:redirect5) { group.redirect_routes.create(path: 'gitlabb/test/baz') }
+
+ it 'returns correct routes' do
+ expect(RedirectRoute.matching_path_and_descendants('gitlabb/test')).to match_array([redirect2, redirect3, redirect4, redirect5])
+ end
+ end
+end
diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb
index 74d5ebc6db0..61b748429d7 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -2,7 +2,7 @@ require 'spec_helper'
describe Repository, models: true do
include RepoHelpers
- TestBlob = Struct.new(:name)
+ TestBlob = Struct.new(:path)
let(:project) { create(:project, :repository) }
let(:repository) { project.repository }
@@ -565,31 +565,31 @@ describe Repository, models: true do
it 'accepts changelog' do
expect(repository.tree).to receive(:blobs).and_return([TestBlob.new('changelog')])
- expect(repository.changelog.name).to eq('changelog')
+ expect(repository.changelog.path).to eq('changelog')
end
it 'accepts news instead of changelog' do
expect(repository.tree).to receive(:blobs).and_return([TestBlob.new('news')])
- expect(repository.changelog.name).to eq('news')
+ expect(repository.changelog.path).to eq('news')
end
it 'accepts history instead of changelog' do
expect(repository.tree).to receive(:blobs).and_return([TestBlob.new('history')])
- expect(repository.changelog.name).to eq('history')
+ expect(repository.changelog.path).to eq('history')
end
it 'accepts changes instead of changelog' do
expect(repository.tree).to receive(:blobs).and_return([TestBlob.new('changes')])
- expect(repository.changelog.name).to eq('changes')
+ expect(repository.changelog.path).to eq('changes')
end
it 'is case-insensitive' do
expect(repository.tree).to receive(:blobs).and_return([TestBlob.new('CHANGELOG')])
- expect(repository.changelog.name).to eq('CHANGELOG')
+ expect(repository.changelog.path).to eq('CHANGELOG')
end
end
@@ -624,7 +624,7 @@ describe Repository, models: true do
repository.create_file(user, 'LICENSE', 'Copyright!',
message: 'Add LICENSE', branch_name: 'master')
- expect(repository.license_blob.name).to eq('LICENSE')
+ expect(repository.license_blob.path).to eq('LICENSE')
end
%w[LICENSE LICENCE LiCensE LICENSE.md LICENSE.foo COPYING COPYING.md].each do |filename|
@@ -654,7 +654,7 @@ describe Repository, models: true do
expect(repository.license_key).to be_nil
end
- it 'detects license file with no recognizable open-source license content' do
+ it 'returns nil when the content is not recognizable' do
repository.create_file(user, 'LICENSE', 'Copyright!',
message: 'Add LICENSE', branch_name: 'master')
@@ -670,12 +670,45 @@ describe Repository, models: true do
end
end
+ describe '#license' do
+ before do
+ repository.delete_file(user, 'LICENSE',
+ message: 'Remove LICENSE', branch_name: 'master')
+ end
+
+ it 'returns nil when no license is detected' do
+ expect(repository.license).to be_nil
+ end
+
+ it 'returns nil when the repository does not exist' do
+ expect(repository).to receive(:exists?).and_return(false)
+
+ expect(repository.license).to be_nil
+ end
+
+ it 'returns nil when the content is not recognizable' do
+ repository.create_file(user, 'LICENSE', 'Copyright!',
+ message: 'Add LICENSE', branch_name: 'master')
+
+ expect(repository.license).to be_nil
+ end
+
+ it 'returns the license' do
+ license = Licensee::License.new('mit')
+ repository.create_file(user, 'LICENSE',
+ license.content,
+ message: 'Add LICENSE', branch_name: 'master')
+
+ expect(repository.license).to eq(license)
+ end
+ end
+
describe "#gitlab_ci_yml", caching: true do
it 'returns valid file' do
files = [TestBlob.new('file'), TestBlob.new('.gitlab-ci.yml'), TestBlob.new('copying')]
expect(repository.tree).to receive(:blobs).and_return(files)
- expect(repository.gitlab_ci_yml.name).to eq('.gitlab-ci.yml')
+ expect(repository.gitlab_ci_yml.path).to eq('.gitlab-ci.yml')
end
it 'returns nil if not exists' do
@@ -1098,21 +1131,33 @@ describe Repository, models: true do
end
describe '#merge' do
- it 'merges the code and return the commit id' do
+ let(:merge_request) { create(:merge_request, source_branch: 'feature', target_branch: 'master', source_project: project) }
+
+ let(:commit_options) do
+ author = repository.user_to_committer(user)
+ { message: 'Test \r\n\r\n message', committer: author, author: author }
+ end
+
+ it 'merges the code and returns 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.diff_head_sha,
- merge_request,
- commit_options)
+ merge_commit_id = merge(repository, user, merge_request, commit_options)
expect(merge_request.in_progress_merge_commit_sha).to eq(merge_commit_id)
end
+
+ it 'removes carriage returns from commit message' do
+ merge_commit_id = merge(repository, user, merge_request, commit_options)
+
+ expect(repository.commit(merge_commit_id).message).to eq(commit_options[:message].delete("\r"))
+ end
+
+ def merge(repository, user, merge_request, options = {})
+ repository.merge(user, merge_request.diff_head_sha, merge_request, options)
+ end
end
describe '#revert' do
@@ -1379,12 +1424,22 @@ describe Repository, models: true do
describe '#branch_count' do
it 'returns the number of branches' do
expect(repository.branch_count).to be_an(Integer)
+
+ # NOTE: Until rugged goes away, make sure rugged and gitaly are in sync
+ rugged_count = repository.raw_repository.rugged.branches.count
+
+ expect(repository.branch_count).to eq(rugged_count)
end
end
describe '#tag_count' do
it 'returns the number of tags' do
expect(repository.tag_count).to be_an(Integer)
+
+ # NOTE: Until rugged goes away, make sure rugged and gitaly are in sync
+ rugged_count = repository.raw_repository.rugged.tags.count
+
+ expect(repository.tag_count).to eq(rugged_count)
end
end
@@ -1604,15 +1659,25 @@ describe Repository, models: true do
describe '#readme', caching: true do
context 'with a non-existing repository' do
it 'returns nil' do
- expect(repository).to receive(:tree).with(:head).and_return(nil)
+ allow(repository).to receive(:tree).with(:head).and_return(nil)
expect(repository.readme).to be_nil
end
end
context 'with an existing repository' do
- it 'returns the README' do
- expect(repository.readme).to be_an_instance_of(Gitlab::Git::Blob)
+ context 'when no README exists' do
+ it 'returns nil' do
+ allow_any_instance_of(Tree).to receive(:readme).and_return(nil)
+
+ expect(repository.readme).to be_nil
+ end
+ end
+
+ context 'when a README exists' do
+ it 'returns the README' do
+ expect(repository.readme).to be_an_instance_of(ReadmeBlob)
+ end
end
end
end
@@ -1803,11 +1868,12 @@ describe Repository, models: true do
describe '#refresh_method_caches' do
it 'refreshes the caches of the given types' do
expect(repository).to receive(:expire_method_caches).
- with(%i(readme license_blob license_key))
+ with(%i(rendered_readme license_blob license_key license))
- expect(repository).to receive(:readme)
+ expect(repository).to receive(:rendered_readme)
expect(repository).to receive(:license_blob)
expect(repository).to receive(:license_key)
+ expect(repository).to receive(:license)
repository.refresh_method_caches(%i(readme license))
end
@@ -1849,17 +1915,15 @@ describe Repository, models: true do
end
end
- # TODO: Uncomment when feature is reenabled
- # describe '#is_ancestor?' do
- # context 'Gitaly is_ancestor feature enabled' do
- # it 'asks Gitaly server if it\'s an ancestor' do
- # commit = repository.commit
- # allow(Gitlab::GitalyClient).to receive(:feature_enabled?).with(:is_ancestor).and_return(true)
- # expect(Gitlab::GitalyClient::Commit).to receive(:is_ancestor).
- # with(repository.raw_repository, commit.id, commit.id).and_return(true)
- #
- # expect(repository.is_ancestor?(commit.id, commit.id)).to be true
- # end
- # end
- # end
+ describe '#is_ancestor?' do
+ context 'Gitaly is_ancestor feature enabled' do
+ it "asks Gitaly server if it's an ancestor" do
+ commit = repository.commit
+ expect(repository.raw_repository).to receive(:is_ancestor?).and_call_original
+ allow(Gitlab::GitalyClient).to receive(:feature_enabled?).with(:is_ancestor).and_return(true)
+
+ expect(repository.is_ancestor?(commit.id, commit.id)).to be true
+ end
+ end
+ end
end
diff --git a/spec/models/route_spec.rb b/spec/models/route_spec.rb
index 171a51fcc5b..c1fe1b06c52 100644
--- a/spec/models/route_spec.rb
+++ b/spec/models/route_spec.rb
@@ -1,19 +1,43 @@
require 'spec_helper'
describe Route, models: true do
- let!(:group) { create(:group, path: 'git_lab', name: 'git_lab') }
- let!(:route) { group.route }
+ let(:group) { create(:group, path: 'git_lab', name: 'git_lab') }
+ let(:route) { group.route }
describe 'relationships' do
it { is_expected.to belong_to(:source) }
end
describe 'validations' do
+ before { route }
it { is_expected.to validate_presence_of(:source) }
it { is_expected.to validate_presence_of(:path) }
it { is_expected.to validate_uniqueness_of(:path) }
end
+ describe 'callbacks' do
+ context 'after update' do
+ it 'calls #create_redirect_for_old_path' do
+ expect(route).to receive(:create_redirect_for_old_path)
+ route.update_attributes(path: 'foo')
+ end
+
+ it 'calls #delete_conflicting_redirects' do
+ expect(route).to receive(:delete_conflicting_redirects)
+ route.update_attributes(path: 'foo')
+ end
+ end
+
+ context 'after create' do
+ it 'calls #delete_conflicting_redirects' do
+ route.destroy
+ new_route = Route.new(source: group, path: group.path)
+ expect(new_route).to receive(:delete_conflicting_redirects)
+ new_route.save!
+ end
+ end
+ end
+
describe '.inside_path' do
let!(:nested_group) { create(:group, path: 'test', name: 'test', parent: group) }
let!(:deep_nested_group) { create(:group, path: 'foo', name: 'foo', parent: nested_group) }
@@ -37,7 +61,7 @@ describe Route, models: true do
context 'when route name is set' do
before { route.update_attributes(path: 'bar') }
- it "updates children routes with new path" do
+ it 'updates children routes with new path' do
expect(described_class.exists?(path: 'bar')).to be_truthy
expect(described_class.exists?(path: 'bar/test')).to be_truthy
expect(described_class.exists?(path: 'bar/test/foo')).to be_truthy
@@ -56,10 +80,24 @@ describe Route, models: true do
expect(route.update_attributes(path: 'bar')).to be_truthy
end
end
+
+ context 'when conflicting redirects exist' do
+ let!(:conflicting_redirect1) { route.create_redirect('bar/test') }
+ let!(:conflicting_redirect2) { route.create_redirect('bar/test/foo') }
+ let!(:conflicting_redirect3) { route.create_redirect('gitlab-org') }
+
+ it 'deletes the conflicting redirects' do
+ route.update_attributes(path: 'bar')
+
+ expect(RedirectRoute.exists?(path: 'bar/test')).to be_falsey
+ expect(RedirectRoute.exists?(path: 'bar/test/foo')).to be_falsey
+ expect(RedirectRoute.exists?(path: 'gitlab-org')).to be_truthy
+ end
+ end
end
context 'name update' do
- it "updates children routes with new path" do
+ it 'updates children routes with new path' do
route.update_attributes(name: 'bar')
expect(described_class.exists?(name: 'bar')).to be_truthy
@@ -77,4 +115,72 @@ describe Route, models: true do
end
end
end
+
+ describe '#create_redirect_for_old_path' do
+ context 'if the path changed' do
+ it 'creates a RedirectRoute for the old path' do
+ redirect_scope = route.source.redirect_routes.where(path: 'git_lab')
+ expect(redirect_scope.exists?).to be_falsey
+ route.path = 'new-path'
+ route.save!
+ expect(redirect_scope.exists?).to be_truthy
+ end
+ end
+ end
+
+ describe '#create_redirect' do
+ it 'creates a RedirectRoute with the same source' do
+ redirect_route = route.create_redirect('foo')
+ expect(redirect_route).to be_a(RedirectRoute)
+ expect(redirect_route).to be_persisted
+ expect(redirect_route.source).to eq(route.source)
+ expect(redirect_route.path).to eq('foo')
+ end
+ end
+
+ describe '#delete_conflicting_redirects' do
+ context 'when a redirect route with the same path exists' do
+ let!(:redirect1) { route.create_redirect(route.path) }
+
+ it 'deletes the redirect' do
+ route.delete_conflicting_redirects
+ expect(route.conflicting_redirects).to be_empty
+ end
+
+ context 'when redirect routes with paths descending from the route path exists' do
+ let!(:redirect2) { route.create_redirect("#{route.path}/foo") }
+ let!(:redirect3) { route.create_redirect("#{route.path}/foo/bar") }
+ let!(:redirect4) { route.create_redirect("#{route.path}/baz/quz") }
+ let!(:other_redirect) { route.create_redirect("other") }
+
+ it 'deletes all redirects with paths that descend from the route path' do
+ route.delete_conflicting_redirects
+ expect(route.conflicting_redirects).to be_empty
+ end
+ end
+ end
+ end
+
+ describe '#conflicting_redirects' do
+ context 'when a redirect route with the same path exists' do
+ let!(:redirect1) { route.create_redirect(route.path) }
+
+ it 'returns the redirect route' do
+ expect(route.conflicting_redirects).to be_an(ActiveRecord::Relation)
+ expect(route.conflicting_redirects).to match_array([redirect1])
+ end
+
+ context 'when redirect routes with paths descending from the route path exists' do
+ let!(:redirect2) { route.create_redirect("#{route.path}/foo") }
+ let!(:redirect3) { route.create_redirect("#{route.path}/foo/bar") }
+ let!(:redirect4) { route.create_redirect("#{route.path}/baz/quz") }
+ let!(:other_redirect) { route.create_redirect("other") }
+
+ it 'returns the redirect routes' do
+ expect(route.conflicting_redirects).to be_an(ActiveRecord::Relation)
+ expect(route.conflicting_redirects).to match_array([redirect1, redirect2, redirect3, redirect4])
+ end
+ end
+ end
+ end
end
diff --git a/spec/models/sent_notification_spec.rb b/spec/models/sent_notification_spec.rb
index 6b7eef388be..5710edbc9e0 100644
--- a/spec/models/sent_notification_spec.rb
+++ b/spec/models/sent_notification_spec.rb
@@ -69,6 +69,7 @@ describe SentNotification, model: true do
it 'creates a comment on the issue' do
new_note = subject.create_reply('Test')
expect(new_note.in_reply_to?(note)).to be_truthy
+ expect(new_note.discussion_id).not_to eq(note.discussion_id)
end
end
@@ -79,6 +80,7 @@ describe SentNotification, model: true do
it 'creates a reply on the discussion' do
new_note = subject.create_reply('Test')
expect(new_note.in_reply_to?(note)).to be_truthy
+ expect(new_note.discussion_id).to eq(note.discussion_id)
end
end
@@ -99,6 +101,7 @@ describe SentNotification, model: true do
it 'creates a comment on the merge request' do
new_note = subject.create_reply('Test')
expect(new_note.in_reply_to?(note)).to be_truthy
+ expect(new_note.discussion_id).not_to eq(note.discussion_id)
end
end
@@ -109,6 +112,7 @@ describe SentNotification, model: true do
it 'creates a reply on the discussion' do
new_note = subject.create_reply('Test')
expect(new_note.in_reply_to?(note)).to be_truthy
+ expect(new_note.discussion_id).to eq(note.discussion_id)
end
end
@@ -119,6 +123,7 @@ describe SentNotification, model: true do
it 'creates a reply on the discussion' do
new_note = subject.create_reply('Test')
expect(new_note.in_reply_to?(note)).to be_truthy
+ expect(new_note.discussion_id).to eq(note.discussion_id)
end
end
@@ -140,6 +145,7 @@ describe SentNotification, model: true do
it 'creates a comment on the commit' do
new_note = subject.create_reply('Test')
expect(new_note.in_reply_to?(note)).to be_truthy
+ expect(new_note.discussion_id).not_to eq(note.discussion_id)
end
end
@@ -150,6 +156,7 @@ describe SentNotification, model: true do
it 'creates a reply on the discussion' do
new_note = subject.create_reply('Test')
expect(new_note.in_reply_to?(note)).to be_truthy
+ expect(new_note.discussion_id).to eq(note.discussion_id)
end
end
@@ -160,6 +167,7 @@ describe SentNotification, model: true do
it 'creates a reply on the discussion' do
new_note = subject.create_reply('Test')
expect(new_note.in_reply_to?(note)).to be_truthy
+ expect(new_note.discussion_id).to eq(note.discussion_id)
end
end
end
diff --git a/spec/models/service_spec.rb b/spec/models/service_spec.rb
index 0e2f07e945f..134882648b9 100644
--- a/spec/models/service_spec.rb
+++ b/spec/models/service_spec.rb
@@ -6,44 +6,53 @@ describe Service, models: true do
it { is_expected.to have_one :service_hook }
end
+ describe 'Validations' do
+ it { is_expected.to validate_presence_of(:type) }
+ end
+
describe "Test Button" do
- before do
- @service = Service.new
- end
+ describe '#can_test?' do
+ let(:service) { create(:service, project: project) }
- describe "Testable" do
- let(:project) { create(:project, :repository) }
+ context 'when repository is not empty' do
+ let(:project) { create(:project, :repository) }
- before do
- allow(@service).to receive(:project).and_return(project)
- @testable = @service.can_test?
+ it 'returns true' do
+ expect(service.can_test?).to be true
+ end
end
- describe '#can_test?' do
- it { expect(@testable).to eq(true) }
+ context 'when repository is empty' do
+ let(:project) { create(:empty_project) }
+
+ it 'returns true' do
+ expect(service.can_test?).to be true
+ end
end
+ end
+
+ describe '#test' do
+ let(:data) { 'test' }
+ let(:service) { create(:service, project: project) }
- describe '#test' do
- let(:data) { 'test' }
+ context 'when repository is not empty' do
+ let(:project) { create(:project, :repository) }
it 'test runs execute' do
- expect(@service).to receive(:execute).with(data)
+ expect(service).to receive(:execute).with(data)
- @service.test(data)
+ service.test(data)
end
end
- end
- describe "With commits" do
- let(:project) { create(:project, :repository) }
+ context 'when repository is empty' do
+ let(:project) { create(:empty_project) }
- before do
- allow(@service).to receive(:project).and_return(project)
- @testable = @service.can_test?
- end
+ it 'test runs execute' do
+ expect(service).to receive(:execute).with(data)
- describe '#can_test?' do
- it { expect(@testable).to eq(true) }
+ service.test(data)
+ end
end
end
end
diff --git a/spec/models/snippet_blob_spec.rb b/spec/models/snippet_blob_spec.rb
new file mode 100644
index 00000000000..120b390586b
--- /dev/null
+++ b/spec/models/snippet_blob_spec.rb
@@ -0,0 +1,47 @@
+require 'spec_helper'
+
+describe SnippetBlob, models: true do
+ let(:snippet) { create(:snippet) }
+
+ subject { described_class.new(snippet) }
+
+ describe '#id' do
+ it 'returns the snippet ID' do
+ expect(subject.id).to eq(snippet.id)
+ end
+ end
+
+ describe '#name' do
+ it 'returns the snippet file name' do
+ expect(subject.name).to eq(snippet.file_name)
+ end
+ end
+
+ describe '#size' do
+ it 'returns the data size' do
+ expect(subject.size).to eq(subject.data.bytesize)
+ end
+ end
+
+ describe '#data' do
+ it 'returns the snippet content' do
+ expect(subject.data).to eq(snippet.content)
+ end
+ end
+
+ describe '#rendered_markup' do
+ context 'when the content is GFM' do
+ let(:snippet) { create(:snippet, file_name: 'file.md') }
+
+ it 'returns the rendered GFM' do
+ expect(subject.rendered_markup).to eq(snippet.content_html)
+ end
+ end
+
+ context 'when the content is not GFM' do
+ it 'returns nil' do
+ expect(subject.rendered_markup).to be_nil
+ end
+ end
+ end
+end
diff --git a/spec/models/snippet_spec.rb b/spec/models/snippet_spec.rb
index 8095d01b69e..1e5c96fe593 100644
--- a/spec/models/snippet_spec.rb
+++ b/spec/models/snippet_spec.rb
@@ -5,7 +5,6 @@ describe Snippet, models: true do
subject { described_class }
it { is_expected.to include_module(Gitlab::VisibilityLevel) }
- it { is_expected.to include_module(Linguist::BlobHelper) }
it { is_expected.to include_module(Participable) }
it { is_expected.to include_module(Referable) }
it { is_expected.to include_module(Sortable) }
@@ -132,46 +131,6 @@ describe Snippet, models: true do
end
end
- describe '.accessible_to' do
- let(:author) { create(:author) }
- let(:project) { create(:empty_project) }
-
- let!(:public_snippet) { create(:snippet, :public) }
- let!(:internal_snippet) { create(:snippet, :internal) }
- let!(:private_snippet) { create(:snippet, :private, author: author) }
-
- let!(:project_public_snippet) { create(:snippet, :public, project: project) }
- let!(:project_internal_snippet) { create(:snippet, :internal, project: project) }
- let!(:project_private_snippet) { create(:snippet, :private, project: project) }
-
- it 'returns only public snippets when user is blank' do
- expect(described_class.accessible_to(nil)).to match_array [public_snippet, project_public_snippet]
- end
-
- it 'returns only public, and internal snippets for regular users' do
- user = create(:user)
-
- expect(described_class.accessible_to(user)).to match_array [public_snippet, internal_snippet, project_public_snippet, project_internal_snippet]
- end
-
- it 'returns public, internal snippets and project private snippets for project members' do
- member = create(:user)
- project.team << [member, :developer]
-
- expect(described_class.accessible_to(member)).to match_array [public_snippet, internal_snippet, project_public_snippet, project_internal_snippet, project_private_snippet]
- end
-
- it 'returns private snippets where the user is the author' do
- expect(described_class.accessible_to(author)).to match_array [public_snippet, internal_snippet, private_snippet, project_public_snippet, project_internal_snippet]
- end
-
- it 'returns all snippets when for admins' do
- admin = create(:admin)
-
- expect(described_class.accessible_to(admin)).to match_array [public_snippet, internal_snippet, private_snippet, project_public_snippet, project_internal_snippet, project_private_snippet]
- end
- end
-
describe '#participants' do
let(:project) { create(:empty_project, :public) }
let(:snippet) { create(:snippet, content: 'foo', project: project) }
@@ -241,4 +200,16 @@ describe Snippet, models: true do
end
end
end
+
+ describe '#blob' do
+ let(:snippet) { create(:snippet) }
+
+ it 'returns a blob representing the snippet data' do
+ blob = snippet.blob
+
+ expect(blob).to be_a(Blob)
+ expect(blob.path).to eq(snippet.file_name)
+ expect(blob.data).to eq(snippet.content)
+ end
+ end
end
diff --git a/spec/models/todo_spec.rb b/spec/models/todo_spec.rb
index 581305ad39f..3f80e1ac534 100644
--- a/spec/models/todo_spec.rb
+++ b/spec/models/todo_spec.rb
@@ -125,4 +125,50 @@ describe Todo, models: true do
expect(subject.target_reference).to eq issue.to_reference(full: true)
end
end
+
+ describe '#self_added?' do
+ let(:user_1) { build(:user) }
+
+ before do
+ subject.user = user_1
+ end
+
+ it 'is true when the user is the author' do
+ subject.author = user_1
+
+ expect(subject).to be_self_added
+ end
+
+ it 'is false when the user is not the author' do
+ subject.author = build(:user)
+
+ expect(subject).not_to be_self_added
+ end
+ end
+
+ describe '#self_assigned?' do
+ let(:user_1) { build(:user) }
+
+ before do
+ subject.user = user_1
+ subject.author = user_1
+ subject.action = Todo::ASSIGNED
+ end
+
+ it 'is true when todo is ASSIGNED and self_added' do
+ expect(subject).to be_self_assigned
+ end
+
+ it 'is false when the todo is not ASSIGNED' do
+ subject.action = Todo::MENTIONED
+
+ expect(subject).not_to be_self_assigned
+ end
+
+ it 'is false when todo is not self_added' do
+ subject.author = build(:user)
+
+ expect(subject).not_to be_self_assigned
+ end
+ end
end
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 0a2860f2505..f2c059010f4 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -97,6 +97,18 @@ describe User, models: true do
expect(user.errors.values).to eq [['dashboard is a reserved name']]
end
+ it 'allows child names' do
+ user = build(:user, username: 'avatar')
+
+ expect(user).to be_valid
+ end
+
+ it 'allows wildcard names' do
+ user = build(:user, username: 'blob')
+
+ expect(user).to be_valid
+ end
+
it 'validates uniqueness' do
expect(subject).to validate_uniqueness_of(:username).case_insensitive
end
@@ -332,6 +344,35 @@ describe User, models: true do
end
end
+ describe '#update_tracked_fields!', :redis do
+ let(:request) { OpenStruct.new(remote_ip: "127.0.0.1") }
+ let(:user) { create(:user) }
+
+ it 'writes trackable attributes' do
+ expect do
+ user.update_tracked_fields!(request)
+ end.to change { user.reload.current_sign_in_at }
+ end
+
+ it 'does not write trackable attributes when called a second time within the hour' do
+ user.update_tracked_fields!(request)
+
+ expect do
+ user.update_tracked_fields!(request)
+ end.not_to change { user.reload.current_sign_in_at }
+ end
+
+ it 'writes trackable attributes for a different user' do
+ user2 = create(:user)
+
+ user.update_tracked_fields!(request)
+
+ expect do
+ user2.update_tracked_fields!(request)
+ end.to change { user2.reload.current_sign_in_at }
+ end
+ end
+
shared_context 'user keys' do
let(:user) { create(:user) }
let!(:key) { create(:key, user: user) }
@@ -635,7 +676,7 @@ describe User, models: true do
protocol_and_expectation = {
'http' => false,
'ssh' => true,
- '' => true,
+ '' => true
}
protocol_and_expectation.each do |protocol, expected|
@@ -837,6 +878,65 @@ describe User, models: true do
end
end
+ describe '.find_by_full_path' do
+ let!(:user) { create(:user) }
+
+ context 'with a route matching the given path' do
+ let!(:route) { user.namespace.route }
+
+ it 'returns the user' do
+ expect(User.find_by_full_path(route.path)).to eq(user)
+ end
+
+ it 'is case-insensitive' do
+ expect(User.find_by_full_path(route.path.upcase)).to eq(user)
+ expect(User.find_by_full_path(route.path.downcase)).to eq(user)
+ end
+ end
+
+ context 'with a redirect route matching the given path' do
+ let!(:redirect_route) { user.namespace.redirect_routes.create(path: 'foo') }
+
+ context 'without the follow_redirects option' do
+ it 'returns nil' do
+ expect(User.find_by_full_path(redirect_route.path)).to eq(nil)
+ end
+ end
+
+ context 'with the follow_redirects option set to true' do
+ it 'returns the user' do
+ expect(User.find_by_full_path(redirect_route.path, follow_redirects: true)).to eq(user)
+ end
+
+ it 'is case-insensitive' do
+ expect(User.find_by_full_path(redirect_route.path.upcase, follow_redirects: true)).to eq(user)
+ expect(User.find_by_full_path(redirect_route.path.downcase, follow_redirects: true)).to eq(user)
+ end
+ end
+ end
+
+ context 'without a route or a redirect route matching the given path' do
+ context 'without the follow_redirects option' do
+ it 'returns nil' do
+ expect(User.find_by_full_path('unknown')).to eq(nil)
+ end
+ end
+ context 'with the follow_redirects option set to true' do
+ it 'returns nil' do
+ expect(User.find_by_full_path('unknown', follow_redirects: true)).to eq(nil)
+ end
+ end
+ end
+
+ context 'with a group route matching the given path' do
+ let!(:group) { create(:group, path: 'group_path') }
+
+ it 'returns nil' do
+ expect(User.find_by_full_path('group_path')).to eq(nil)
+ end
+ end
+ end
+
describe 'all_ssh_keys' do
it { is_expected.to have_many(:keys).dependent(:destroy) }
@@ -862,6 +962,24 @@ describe User, models: true do
end
end
+ describe '#avatar_url' do
+ let(:user) { create(:user, :with_avatar) }
+
+ context 'when avatar file is uploaded' do
+ let(:gitlab_host) { "http://#{Gitlab.config.gitlab.host}" }
+ let(:avatar_path) { "/uploads/user/avatar/#{user.id}/dk.png" }
+
+ it 'shows correct avatar url' do
+ expect(user.avatar_url).to eq(avatar_path)
+ expect(user.avatar_url(only_path: false)).to eq([gitlab_host, avatar_path].join)
+
+ allow(ActionController::Base).to receive(:asset_host).and_return(gitlab_host)
+
+ expect(user.avatar_url).to eq([gitlab_host, avatar_path].join)
+ end
+ end
+ end
+
describe '#requires_ldap_check?' do
let(:user) { User.new }
@@ -1556,6 +1674,16 @@ describe User, models: true do
expect(ghost.email).to eq('ghost1@example.com')
end
end
+
+ context 'when a domain whitelist is in place' do
+ before do
+ stub_application_setting(domain_whitelist: ['gitlab.com'])
+ end
+
+ it 'creates a ghost user' do
+ expect(User.ghost).to be_persisted
+ end
+ end
end
describe '#update_two_factor_requirement' do
@@ -1641,4 +1769,12 @@ describe User, models: true do
expect(User.active.count).to eq(1)
end
end
+
+ describe 'preferred language' do
+ it 'is English by default' do
+ user = create(:user)
+
+ expect(user.preferred_language).to eq('en')
+ end
+ end
end
diff --git a/spec/policies/ci/build_policy_spec.rb b/spec/policies/ci/build_policy_spec.rb
index 0f280f32eac..3f4ce222b60 100644
--- a/spec/policies/ci/build_policy_spec.rb
+++ b/spec/policies/ci/build_policy_spec.rb
@@ -89,5 +89,58 @@ describe Ci::BuildPolicy, :models do
end
end
end
+
+ describe 'rules for manual actions' do
+ let(:project) { create(:project) }
+
+ before do
+ project.add_developer(user)
+ end
+
+ context 'when branch build is assigned to is protected' do
+ before do
+ create(:protected_branch, :no_one_can_push,
+ name: 'some-ref', project: project)
+ end
+
+ context 'when build is a manual action' do
+ let(:build) do
+ create(:ci_build, :manual, ref: 'some-ref', pipeline: pipeline)
+ end
+
+ it 'does not include ability to update build' do
+ expect(policies).not_to include :update_build
+ end
+ end
+
+ context 'when build is not a manual action' do
+ let(:build) do
+ create(:ci_build, ref: 'some-ref', pipeline: pipeline)
+ end
+
+ it 'includes ability to update build' do
+ expect(policies).to include :update_build
+ end
+ end
+ end
+
+ context 'when branch build is assigned to is not protected' do
+ context 'when build is a manual action' do
+ let(:build) { create(:ci_build, :manual, pipeline: pipeline) }
+
+ it 'includes ability to update build' do
+ expect(policies).to include :update_build
+ end
+ end
+
+ context 'when build is not a manual action' do
+ let(:build) { create(:ci_build, pipeline: pipeline) }
+
+ it 'includes ability to update build' do
+ expect(policies).to include :update_build
+ end
+ end
+ end
+ end
end
end
diff --git a/spec/policies/environment_policy_spec.rb b/spec/policies/environment_policy_spec.rb
new file mode 100644
index 00000000000..650432520bb
--- /dev/null
+++ b/spec/policies/environment_policy_spec.rb
@@ -0,0 +1,57 @@
+require 'spec_helper'
+
+describe EnvironmentPolicy do
+ let(:user) { create(:user) }
+ let(:project) { create(:project) }
+
+ let(:environment) do
+ create(:environment, :with_review_app, project: project)
+ end
+
+ let(:policies) do
+ described_class.abilities(user, environment).to_set
+ end
+
+ describe '#rules' do
+ context 'when user does not have access to the project' do
+ let(:project) { create(:project, :private) }
+
+ it 'does not include ability to stop environment' do
+ expect(policies).not_to include :stop_environment
+ end
+ end
+
+ context 'when anonymous user has access to the project' do
+ let(:project) { create(:project, :public) }
+
+ it 'does not include ability to stop environment' do
+ expect(policies).not_to include :stop_environment
+ end
+ end
+
+ context 'when team member has access to the project' do
+ let(:project) { create(:project, :public) }
+
+ before do
+ project.add_developer(user)
+ end
+
+ context 'when team member has ability to stop environment' do
+ it 'does includes ability to stop environment' do
+ expect(policies).to include :stop_environment
+ end
+ end
+
+ context 'when team member has no ability to stop environment' do
+ before do
+ create(:protected_branch, :no_one_can_push,
+ name: 'master', project: project)
+ end
+
+ it 'does not include ability to stop environment' do
+ expect(policies).not_to include :stop_environment
+ end
+ end
+ end
+ end
+end
diff --git a/spec/policies/issue_policy_spec.rb b/spec/policies/issue_policy_spec.rb
index 2905d5b26a5..4a07c864428 100644
--- a/spec/policies/issue_policy_spec.rb
+++ b/spec/policies/issue_policy_spec.rb
@@ -1,118 +1,192 @@
require 'spec_helper'
describe IssuePolicy, models: true do
- let(:user) { create(:user) }
-
- describe '#rules' do
- context 'using a regular issue' do
- let(:project) { create(:empty_project, :public) }
- let(:issue) { create(:issue, project: project) }
- let(:policies) { described_class.abilities(user, issue).to_set }
-
- context 'with a regular user' do
- it 'includes the read_issue permission' do
- expect(policies).to include(:read_issue)
- end
-
- it 'does not include the admin_issue permission' do
- expect(policies).not_to include(:admin_issue)
- end
-
- it 'does not include the update_issue permission' do
- expect(policies).not_to include(:update_issue)
- end
- end
+ let(:guest) { create(:user) }
+ let(:author) { create(:user) }
+ let(:assignee) { create(:user) }
+ let(:reporter) { create(:user) }
+ let(:group) { create(:group, :public) }
+ let(:reporter_from_group_link) { create(:user) }
+
+ def permissions(user, issue)
+ described_class.abilities(user, issue).to_set
+ end
+
+ context 'a private project' do
+ let(:non_member) { create(:user) }
+ let(:project) { create(:empty_project, :private) }
+ let(:issue) { create(:issue, project: project, assignees: [assignee], author: author) }
+ let(:issue_no_assignee) { create(:issue, project: project) }
+
+ before do
+ project.team << [guest, :guest]
+ project.team << [author, :guest]
+ project.team << [assignee, :guest]
+ project.team << [reporter, :reporter]
+
+ group.add_reporter(reporter_from_group_link)
+
+ create(:project_group_link, group: group, project: project)
+ end
+
+ it 'does not allow non-members to read issues' do
+ expect(permissions(non_member, issue)).not_to include(:read_issue, :update_issue, :admin_issue)
+ expect(permissions(non_member, issue_no_assignee)).not_to include(:read_issue, :update_issue, :admin_issue)
+ end
+
+ it 'allows guests to read issues' do
+ expect(permissions(guest, issue)).to include(:read_issue)
+ expect(permissions(guest, issue)).not_to include(:update_issue, :admin_issue)
+
+ expect(permissions(guest, issue_no_assignee)).to include(:read_issue)
+ expect(permissions(guest, issue_no_assignee)).not_to include(:update_issue, :admin_issue)
+ end
+
+ it 'allows reporters to read, update, and admin issues' do
+ expect(permissions(reporter, issue)).to include(:read_issue, :update_issue, :admin_issue)
+ expect(permissions(reporter, issue_no_assignee)).to include(:read_issue, :update_issue, :admin_issue)
+ end
+
+ it 'allows reporters from group links to read, update, and admin issues' do
+ expect(permissions(reporter_from_group_link, issue)).to include(:read_issue, :update_issue, :admin_issue)
+ expect(permissions(reporter_from_group_link, issue_no_assignee)).to include(:read_issue, :update_issue, :admin_issue)
+ end
+
+ it 'allows issue authors to read and update their issues' do
+ expect(permissions(author, issue)).to include(:read_issue, :update_issue)
+ expect(permissions(author, issue)).not_to include(:admin_issue)
+
+ expect(permissions(author, issue_no_assignee)).to include(:read_issue)
+ expect(permissions(author, issue_no_assignee)).not_to include(:update_issue, :admin_issue)
+ end
+
+ it 'allows issue assignees to read and update their issues' do
+ expect(permissions(assignee, issue)).to include(:read_issue, :update_issue)
+ expect(permissions(assignee, issue)).not_to include(:admin_issue)
+
+ expect(permissions(assignee, issue_no_assignee)).to include(:read_issue)
+ expect(permissions(assignee, issue_no_assignee)).not_to include(:update_issue, :admin_issue)
+ end
- context 'with a user that is a project reporter' do
- before do
- project.team << [user, :reporter]
- end
+ context 'with confidential issues' do
+ let(:confidential_issue) { create(:issue, :confidential, project: project, assignees: [assignee], author: author) }
+ let(:confidential_issue_no_assignee) { create(:issue, :confidential, project: project) }
- it 'includes the read_issue permission' do
- expect(policies).to include(:read_issue)
- end
+ it 'does not allow non-members to read confidential issues' do
+ expect(permissions(non_member, confidential_issue)).not_to include(:read_issue, :update_issue, :admin_issue)
+ expect(permissions(non_member, confidential_issue_no_assignee)).not_to include(:read_issue, :update_issue, :admin_issue)
+ end
+
+ it 'does not allow guests to read confidential issues' do
+ expect(permissions(guest, confidential_issue)).not_to include(:read_issue, :update_issue, :admin_issue)
+ expect(permissions(guest, confidential_issue_no_assignee)).not_to include(:read_issue, :update_issue, :admin_issue)
+ end
- it 'includes the admin_issue permission' do
- expect(policies).to include(:admin_issue)
- end
+ it 'allows reporters to read, update, and admin confidential issues' do
+ expect(permissions(reporter, confidential_issue)).to include(:read_issue, :update_issue, :admin_issue)
+ expect(permissions(reporter, confidential_issue_no_assignee)).to include(:read_issue, :update_issue, :admin_issue)
+ end
- it 'includes the update_issue permission' do
- expect(policies).to include(:update_issue)
- end
+ it 'allows reporters from group links to read, update, and admin confidential issues' do
+ expect(permissions(reporter_from_group_link, confidential_issue)).to include(:read_issue, :update_issue, :admin_issue)
+ expect(permissions(reporter_from_group_link, confidential_issue_no_assignee)).to include(:read_issue, :update_issue, :admin_issue)
end
- context 'with a user that is a project guest' do
- before do
- project.team << [user, :guest]
- end
+ it 'allows issue authors to read and update their confidential issues' do
+ expect(permissions(author, confidential_issue)).to include(:read_issue, :update_issue)
+ expect(permissions(author, confidential_issue)).not_to include(:admin_issue)
- it 'includes the read_issue permission' do
- expect(policies).to include(:read_issue)
- end
+ expect(permissions(author, confidential_issue_no_assignee)).not_to include(:read_issue, :update_issue, :admin_issue)
+ end
- it 'does not include the admin_issue permission' do
- expect(policies).not_to include(:admin_issue)
- end
+ it 'allows issue assignees to read and update their confidential issues' do
+ expect(permissions(assignee, confidential_issue)).to include(:read_issue, :update_issue)
+ expect(permissions(assignee, confidential_issue)).not_to include(:admin_issue)
- it 'does not include the update_issue permission' do
- expect(policies).not_to include(:update_issue)
- end
+ expect(permissions(assignee, confidential_issue_no_assignee)).not_to include(:read_issue, :update_issue, :admin_issue)
end
end
+ end
- context 'using a confidential issue' do
- let(:issue) { create(:issue, :confidential) }
+ context 'a public project' do
+ let(:project) { create(:empty_project, :public) }
+ let(:issue) { create(:issue, project: project, assignees: [assignee], author: author) }
+ let(:issue_no_assignee) { create(:issue, project: project) }
- context 'with a regular user' do
- let(:policies) { described_class.abilities(user, issue).to_set }
+ before do
+ project.team << [guest, :guest]
+ project.team << [reporter, :reporter]
- it 'does not include the read_issue permission' do
- expect(policies).not_to include(:read_issue)
- end
+ group.add_reporter(reporter_from_group_link)
- it 'does not include the admin_issue permission' do
- expect(policies).not_to include(:admin_issue)
- end
+ create(:project_group_link, group: group, project: project)
+ end
- it 'does not include the update_issue permission' do
- expect(policies).not_to include(:update_issue)
- end
- end
+ it 'allows guests to read issues' do
+ expect(permissions(guest, issue)).to include(:read_issue)
+ expect(permissions(guest, issue)).not_to include(:update_issue, :admin_issue)
+
+ expect(permissions(guest, issue_no_assignee)).to include(:read_issue)
+ expect(permissions(guest, issue_no_assignee)).not_to include(:update_issue, :admin_issue)
+ end
+
+ it 'allows reporters to read, update, and admin issues' do
+ expect(permissions(reporter, issue)).to include(:read_issue, :update_issue, :admin_issue)
+ expect(permissions(reporter, issue_no_assignee)).to include(:read_issue, :update_issue, :admin_issue)
+ end
+
+ it 'allows reporters from group links to read, update, and admin issues' do
+ expect(permissions(reporter_from_group_link, issue)).to include(:read_issue, :update_issue, :admin_issue)
+ expect(permissions(reporter_from_group_link, issue_no_assignee)).to include(:read_issue, :update_issue, :admin_issue)
+ end
- context 'with a user that is a project member' do
- let(:policies) { described_class.abilities(user, issue).to_set }
+ it 'allows issue authors to read and update their issues' do
+ expect(permissions(author, issue)).to include(:read_issue, :update_issue)
+ expect(permissions(author, issue)).not_to include(:admin_issue)
- before do
- issue.project.team << [user, :reporter]
- end
+ expect(permissions(author, issue_no_assignee)).to include(:read_issue)
+ expect(permissions(author, issue_no_assignee)).not_to include(:update_issue, :admin_issue)
+ end
+
+ it 'allows issue assignees to read and update their issues' do
+ expect(permissions(assignee, issue)).to include(:read_issue, :update_issue)
+ expect(permissions(assignee, issue)).not_to include(:admin_issue)
- it 'includes the read_issue permission' do
- expect(policies).to include(:read_issue)
- end
+ expect(permissions(assignee, issue_no_assignee)).to include(:read_issue)
+ expect(permissions(assignee, issue_no_assignee)).not_to include(:update_issue, :admin_issue)
+ end
- it 'includes the admin_issue permission' do
- expect(policies).to include(:admin_issue)
- end
+ context 'with confidential issues' do
+ let(:confidential_issue) { create(:issue, :confidential, project: project, assignees: [assignee], author: author) }
+ let(:confidential_issue_no_assignee) { create(:issue, :confidential, project: project) }
- it 'includes the update_issue permission' do
- expect(policies).to include(:update_issue)
- end
+ it 'does not allow guests to read confidential issues' do
+ expect(permissions(guest, confidential_issue)).not_to include(:read_issue, :update_issue, :admin_issue)
+ expect(permissions(guest, confidential_issue_no_assignee)).not_to include(:read_issue, :update_issue, :admin_issue)
end
- context 'without a user' do
- let(:policies) { described_class.abilities(nil, issue).to_set }
+ it 'allows reporters to read, update, and admin confidential issues' do
+ expect(permissions(reporter, confidential_issue)).to include(:read_issue, :update_issue, :admin_issue)
+ expect(permissions(reporter, confidential_issue_no_assignee)).to include(:read_issue, :update_issue, :admin_issue)
+ end
+
+ it 'allows reporter from group links to read, update, and admin confidential issues' do
+ expect(permissions(reporter_from_group_link, confidential_issue)).to include(:read_issue, :update_issue, :admin_issue)
+ expect(permissions(reporter_from_group_link, confidential_issue_no_assignee)).to include(:read_issue, :update_issue, :admin_issue)
+ end
- it 'does not include the read_issue permission' do
- expect(policies).not_to include(:read_issue)
- end
+ it 'allows issue authors to read and update their confidential issues' do
+ expect(permissions(author, confidential_issue)).to include(:read_issue, :update_issue)
+ expect(permissions(author, confidential_issue)).not_to include(:admin_issue)
+
+ expect(permissions(author, confidential_issue_no_assignee)).not_to include(:read_issue, :update_issue, :admin_issue)
+ end
- it 'does not include the admin_issue permission' do
- expect(policies).not_to include(:admin_issue)
- end
+ it 'allows issue assignees to read and update their confidential issues' do
+ expect(permissions(assignee, confidential_issue)).to include(:read_issue, :update_issue)
+ expect(permissions(assignee, confidential_issue)).not_to include(:admin_issue)
- it 'does not include the update_issue permission' do
- expect(policies).not_to include(:update_issue)
- end
+ expect(permissions(assignee, confidential_issue_no_assignee)).not_to include(:read_issue, :update_issue, :admin_issue)
end
end
end
diff --git a/spec/policies/issues_policy_spec.rb b/spec/policies/issues_policy_spec.rb
deleted file mode 100644
index 2b7b6cad654..00000000000
--- a/spec/policies/issues_policy_spec.rb
+++ /dev/null
@@ -1,193 +0,0 @@
-require 'spec_helper'
-
-describe IssuePolicy, models: true do
- let(:guest) { create(:user) }
- let(:author) { create(:user) }
- let(:assignee) { create(:user) }
- let(:reporter) { create(:user) }
- let(:group) { create(:group, :public) }
- let(:reporter_from_group_link) { create(:user) }
-
- def permissions(user, issue)
- IssuePolicy.abilities(user, issue).to_set
- end
-
- context 'a private project' do
- let(:non_member) { create(:user) }
- let(:project) { create(:empty_project, :private) }
- let(:issue) { create(:issue, project: project, assignee: assignee, author: author) }
- let(:issue_no_assignee) { create(:issue, project: project) }
-
- before do
- project.team << [guest, :guest]
- project.team << [author, :guest]
- project.team << [assignee, :guest]
- project.team << [reporter, :reporter]
-
- group.add_reporter(reporter_from_group_link)
-
- create(:project_group_link, group: group, project: project)
- end
-
- it 'does not allow non-members to read issues' do
- expect(permissions(non_member, issue)).not_to include(:read_issue, :update_issue, :admin_issue)
- expect(permissions(non_member, issue_no_assignee)).not_to include(:read_issue, :update_issue, :admin_issue)
- end
-
- it 'allows guests to read issues' do
- expect(permissions(guest, issue)).to include(:read_issue)
- expect(permissions(guest, issue)).not_to include(:update_issue, :admin_issue)
-
- expect(permissions(guest, issue_no_assignee)).to include(:read_issue)
- expect(permissions(guest, issue_no_assignee)).not_to include(:update_issue, :admin_issue)
- end
-
- it 'allows reporters to read, update, and admin issues' do
- expect(permissions(reporter, issue)).to include(:read_issue, :update_issue, :admin_issue)
- expect(permissions(reporter, issue_no_assignee)).to include(:read_issue, :update_issue, :admin_issue)
- end
-
- it 'allows reporters from group links to read, update, and admin issues' do
- expect(permissions(reporter_from_group_link, issue)).to include(:read_issue, :update_issue, :admin_issue)
- expect(permissions(reporter_from_group_link, issue_no_assignee)).to include(:read_issue, :update_issue, :admin_issue)
- end
-
- it 'allows issue authors to read and update their issues' do
- expect(permissions(author, issue)).to include(:read_issue, :update_issue)
- expect(permissions(author, issue)).not_to include(:admin_issue)
-
- expect(permissions(author, issue_no_assignee)).to include(:read_issue)
- expect(permissions(author, issue_no_assignee)).not_to include(:update_issue, :admin_issue)
- end
-
- it 'allows issue assignees to read and update their issues' do
- expect(permissions(assignee, issue)).to include(:read_issue, :update_issue)
- expect(permissions(assignee, issue)).not_to include(:admin_issue)
-
- expect(permissions(assignee, issue_no_assignee)).to include(:read_issue)
- expect(permissions(assignee, issue_no_assignee)).not_to include(:update_issue, :admin_issue)
- end
-
- context 'with confidential issues' do
- let(:confidential_issue) { create(:issue, :confidential, project: project, assignee: assignee, author: author) }
- let(:confidential_issue_no_assignee) { create(:issue, :confidential, project: project) }
-
- it 'does not allow non-members to read confidential issues' do
- expect(permissions(non_member, confidential_issue)).not_to include(:read_issue, :update_issue, :admin_issue)
- expect(permissions(non_member, confidential_issue_no_assignee)).not_to include(:read_issue, :update_issue, :admin_issue)
- end
-
- it 'does not allow guests to read confidential issues' do
- expect(permissions(guest, confidential_issue)).not_to include(:read_issue, :update_issue, :admin_issue)
- expect(permissions(guest, confidential_issue_no_assignee)).not_to include(:read_issue, :update_issue, :admin_issue)
- end
-
- it 'allows reporters to read, update, and admin confidential issues' do
- expect(permissions(reporter, confidential_issue)).to include(:read_issue, :update_issue, :admin_issue)
- expect(permissions(reporter, confidential_issue_no_assignee)).to include(:read_issue, :update_issue, :admin_issue)
- end
-
- it 'allows reporters from group links to read, update, and admin confidential issues' do
- expect(permissions(reporter_from_group_link, confidential_issue)).to include(:read_issue, :update_issue, :admin_issue)
- expect(permissions(reporter_from_group_link, confidential_issue_no_assignee)).to include(:read_issue, :update_issue, :admin_issue)
- end
-
- it 'allows issue authors to read and update their confidential issues' do
- expect(permissions(author, confidential_issue)).to include(:read_issue, :update_issue)
- expect(permissions(author, confidential_issue)).not_to include(:admin_issue)
-
- expect(permissions(author, confidential_issue_no_assignee)).not_to include(:read_issue, :update_issue, :admin_issue)
- end
-
- it 'allows issue assignees to read and update their confidential issues' do
- expect(permissions(assignee, confidential_issue)).to include(:read_issue, :update_issue)
- expect(permissions(assignee, confidential_issue)).not_to include(:admin_issue)
-
- expect(permissions(assignee, confidential_issue_no_assignee)).not_to include(:read_issue, :update_issue, :admin_issue)
- end
- end
- end
-
- context 'a public project' do
- let(:project) { create(:empty_project, :public) }
- let(:issue) { create(:issue, project: project, assignee: assignee, author: author) }
- let(:issue_no_assignee) { create(:issue, project: project) }
-
- before do
- project.team << [guest, :guest]
- project.team << [reporter, :reporter]
-
- group.add_reporter(reporter_from_group_link)
-
- create(:project_group_link, group: group, project: project)
- end
-
- it 'allows guests to read issues' do
- expect(permissions(guest, issue)).to include(:read_issue)
- expect(permissions(guest, issue)).not_to include(:update_issue, :admin_issue)
-
- expect(permissions(guest, issue_no_assignee)).to include(:read_issue)
- expect(permissions(guest, issue_no_assignee)).not_to include(:update_issue, :admin_issue)
- end
-
- it 'allows reporters to read, update, and admin issues' do
- expect(permissions(reporter, issue)).to include(:read_issue, :update_issue, :admin_issue)
- expect(permissions(reporter, issue_no_assignee)).to include(:read_issue, :update_issue, :admin_issue)
- end
-
- it 'allows reporters from group links to read, update, and admin issues' do
- expect(permissions(reporter_from_group_link, issue)).to include(:read_issue, :update_issue, :admin_issue)
- expect(permissions(reporter_from_group_link, issue_no_assignee)).to include(:read_issue, :update_issue, :admin_issue)
- end
-
- it 'allows issue authors to read and update their issues' do
- expect(permissions(author, issue)).to include(:read_issue, :update_issue)
- expect(permissions(author, issue)).not_to include(:admin_issue)
-
- expect(permissions(author, issue_no_assignee)).to include(:read_issue)
- expect(permissions(author, issue_no_assignee)).not_to include(:update_issue, :admin_issue)
- end
-
- it 'allows issue assignees to read and update their issues' do
- expect(permissions(assignee, issue)).to include(:read_issue, :update_issue)
- expect(permissions(assignee, issue)).not_to include(:admin_issue)
-
- expect(permissions(assignee, issue_no_assignee)).to include(:read_issue)
- expect(permissions(assignee, issue_no_assignee)).not_to include(:update_issue, :admin_issue)
- end
-
- context 'with confidential issues' do
- let(:confidential_issue) { create(:issue, :confidential, project: project, assignee: assignee, author: author) }
- let(:confidential_issue_no_assignee) { create(:issue, :confidential, project: project) }
-
- it 'does not allow guests to read confidential issues' do
- expect(permissions(guest, confidential_issue)).not_to include(:read_issue, :update_issue, :admin_issue)
- expect(permissions(guest, confidential_issue_no_assignee)).not_to include(:read_issue, :update_issue, :admin_issue)
- end
-
- it 'allows reporters to read, update, and admin confidential issues' do
- expect(permissions(reporter, confidential_issue)).to include(:read_issue, :update_issue, :admin_issue)
- expect(permissions(reporter, confidential_issue_no_assignee)).to include(:read_issue, :update_issue, :admin_issue)
- end
-
- it 'allows reporter from group links to read, update, and admin confidential issues' do
- expect(permissions(reporter_from_group_link, confidential_issue)).to include(:read_issue, :update_issue, :admin_issue)
- expect(permissions(reporter_from_group_link, confidential_issue_no_assignee)).to include(:read_issue, :update_issue, :admin_issue)
- end
-
- it 'allows issue authors to read and update their confidential issues' do
- expect(permissions(author, confidential_issue)).to include(:read_issue, :update_issue)
- expect(permissions(author, confidential_issue)).not_to include(:admin_issue)
-
- expect(permissions(author, confidential_issue_no_assignee)).not_to include(:read_issue, :update_issue, :admin_issue)
- end
-
- it 'allows issue assignees to read and update their confidential issues' do
- expect(permissions(assignee, confidential_issue)).to include(:read_issue, :update_issue)
- expect(permissions(assignee, confidential_issue)).not_to include(:admin_issue)
-
- expect(permissions(assignee, confidential_issue_no_assignee)).not_to include(:read_issue, :update_issue, :admin_issue)
- end
- end
- end
-end
diff --git a/spec/policies/personal_snippet_policy_spec.rb b/spec/policies/personal_snippet_policy_spec.rb
new file mode 100644
index 00000000000..58aa1145c9e
--- /dev/null
+++ b/spec/policies/personal_snippet_policy_spec.rb
@@ -0,0 +1,141 @@
+require 'spec_helper'
+
+describe PersonalSnippetPolicy, models: true do
+ let(:regular_user) { create(:user) }
+ let(:external_user) { create(:user, :external) }
+ let(:admin_user) { create(:user, :admin) }
+
+ let(:author_permissions) do
+ [
+ :update_personal_snippet,
+ :admin_personal_snippet,
+ :destroy_personal_snippet
+ ]
+ end
+
+ def permissions(user)
+ described_class.abilities(user, snippet).to_set
+ end
+
+ context 'public snippet' do
+ let(:snippet) { create(:personal_snippet, :public) }
+
+ context 'no user' do
+ subject { permissions(nil) }
+
+ it do
+ is_expected.to include(:read_personal_snippet)
+ is_expected.not_to include(:comment_personal_snippet)
+ is_expected.not_to include(*author_permissions)
+ end
+ end
+
+ context 'regular user' do
+ subject { permissions(regular_user) }
+
+ it do
+ is_expected.to include(:read_personal_snippet)
+ is_expected.to include(:comment_personal_snippet)
+ is_expected.not_to include(*author_permissions)
+ end
+ end
+
+ context 'author' do
+ subject { permissions(snippet.author) }
+
+ it do
+ is_expected.to include(:read_personal_snippet)
+ is_expected.to include(:comment_personal_snippet)
+ is_expected.to include(*author_permissions)
+ end
+ end
+ end
+
+ context 'internal snippet' do
+ let(:snippet) { create(:personal_snippet, :internal) }
+
+ context 'no user' do
+ subject { permissions(nil) }
+
+ it do
+ is_expected.not_to include(:read_personal_snippet)
+ is_expected.not_to include(:comment_personal_snippet)
+ is_expected.not_to include(*author_permissions)
+ end
+ end
+
+ context 'regular user' do
+ subject { permissions(regular_user) }
+
+ it do
+ is_expected.to include(:read_personal_snippet)
+ is_expected.to include(:comment_personal_snippet)
+ is_expected.not_to include(*author_permissions)
+ end
+ end
+
+ context 'external user' do
+ subject { permissions(external_user) }
+
+ it do
+ is_expected.not_to include(:read_personal_snippet)
+ is_expected.not_to include(:comment_personal_snippet)
+ is_expected.not_to include(*author_permissions)
+ end
+ end
+
+ context 'snippet author' do
+ subject { permissions(snippet.author) }
+
+ it do
+ is_expected.to include(:read_personal_snippet)
+ is_expected.to include(:comment_personal_snippet)
+ is_expected.to include(*author_permissions)
+ end
+ end
+ end
+
+ context 'private snippet' do
+ let(:snippet) { create(:project_snippet, :private) }
+
+ context 'no user' do
+ subject { permissions(nil) }
+
+ it do
+ is_expected.not_to include(:read_personal_snippet)
+ is_expected.not_to include(:comment_personal_snippet)
+ is_expected.not_to include(*author_permissions)
+ end
+ end
+
+ context 'regular user' do
+ subject { permissions(regular_user) }
+
+ it do
+ is_expected.not_to include(:read_personal_snippet)
+ is_expected.not_to include(:comment_personal_snippet)
+ is_expected.not_to include(*author_permissions)
+ end
+ end
+
+ context 'external user' do
+ subject { permissions(external_user) }
+
+ it do
+ is_expected.not_to include(:read_personal_snippet)
+ is_expected.not_to include(:comment_personal_snippet)
+ is_expected.not_to include(*author_permissions)
+ end
+ end
+
+ context 'snippet author' do
+ subject { permissions(snippet.author) }
+
+ it do
+ is_expected.to include(:read_personal_snippet)
+ is_expected.to include(:comment_personal_snippet)
+ is_expected.to include(*author_permissions)
+ end
+ end
+ end
+end
diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb
index 064847ee3dc..0d3af1f4499 100644
--- a/spec/policies/project_policy_spec.rb
+++ b/spec/policies/project_policy_spec.rb
@@ -43,7 +43,7 @@ describe ProjectPolicy, models: true do
let(:master_permissions) do
%i[
- push_code_to_protected_branches update_project_snippet update_environment
+ delete_protected_branch update_project_snippet update_environment
update_deployment admin_milestone admin_project_snippet
admin_project_member admin_note admin_wiki admin_project
admin_commit_status admin_build admin_container_image
diff --git a/spec/policies/project_snippet_policy_spec.rb b/spec/policies/project_snippet_policy_spec.rb
index d0758af57dd..e1771b636b8 100644
--- a/spec/policies/project_snippet_policy_spec.rb
+++ b/spec/policies/project_snippet_policy_spec.rb
@@ -1,7 +1,9 @@
require 'spec_helper'
describe ProjectSnippetPolicy, models: true do
- let(:current_user) { create(:user) }
+ let(:regular_user) { create(:user) }
+ let(:external_user) { create(:user, :external) }
+ let(:project) { create(:empty_project) }
let(:author_permissions) do
[
@@ -10,13 +12,15 @@ describe ProjectSnippetPolicy, models: true do
]
end
- subject { described_class.abilities(current_user, project_snippet).to_set }
+ def abilities(user, snippet_visibility)
+ snippet = create(:project_snippet, snippet_visibility, project: project)
- context 'public snippet' do
- let(:project_snippet) { create(:project_snippet, :public) }
+ described_class.abilities(user, snippet).to_set
+ end
+ context 'public snippet' do
context 'no user' do
- let(:current_user) { nil }
+ subject { abilities(nil, :public) }
it do
is_expected.to include(:read_project_snippet)
@@ -25,6 +29,17 @@ describe ProjectSnippetPolicy, models: true do
end
context 'regular user' do
+ subject { abilities(regular_user, :public) }
+
+ it do
+ is_expected.to include(:read_project_snippet)
+ is_expected.not_to include(*author_permissions)
+ end
+ end
+
+ context 'external user' do
+ subject { abilities(external_user, :public) }
+
it do
is_expected.to include(:read_project_snippet)
is_expected.not_to include(*author_permissions)
@@ -33,10 +48,8 @@ describe ProjectSnippetPolicy, models: true do
end
context 'internal snippet' do
- let(:project_snippet) { create(:project_snippet, :internal) }
-
context 'no user' do
- let(:current_user) { nil }
+ subject { abilities(nil, :internal) }
it do
is_expected.not_to include(:read_project_snippet)
@@ -45,6 +58,28 @@ describe ProjectSnippetPolicy, models: true do
end
context 'regular user' do
+ subject { abilities(regular_user, :internal) }
+
+ it do
+ is_expected.to include(:read_project_snippet)
+ is_expected.not_to include(*author_permissions)
+ end
+ end
+
+ context 'external user' do
+ subject { abilities(external_user, :internal) }
+
+ it do
+ is_expected.not_to include(:read_project_snippet)
+ is_expected.not_to include(*author_permissions)
+ end
+ end
+
+ context 'project team member external user' do
+ subject { abilities(external_user, :internal) }
+
+ before { project.team << [external_user, :developer] }
+
it do
is_expected.to include(:read_project_snippet)
is_expected.not_to include(*author_permissions)
@@ -53,10 +88,8 @@ describe ProjectSnippetPolicy, models: true do
end
context 'private snippet' do
- let(:project_snippet) { create(:project_snippet, :private) }
-
context 'no user' do
- let(:current_user) { nil }
+ subject { abilities(nil, :private) }
it do
is_expected.not_to include(:read_project_snippet)
@@ -65,6 +98,8 @@ describe ProjectSnippetPolicy, models: true do
end
context 'regular user' do
+ subject { abilities(regular_user, :private) }
+
it do
is_expected.not_to include(:read_project_snippet)
is_expected.not_to include(*author_permissions)
@@ -72,7 +107,9 @@ describe ProjectSnippetPolicy, models: true do
end
context 'snippet author' do
- let(:project_snippet) { create(:project_snippet, :private, author: current_user) }
+ let(:snippet) { create(:project_snippet, :private, author: regular_user) }
+
+ subject { described_class.abilities(regular_user, snippet).to_set }
it do
is_expected.to include(:read_project_snippet)
@@ -80,8 +117,21 @@ describe ProjectSnippetPolicy, models: true do
end
end
- context 'project team member' do
- before { project_snippet.project.team << [current_user, :developer] }
+ context 'project team member normal user' do
+ subject { abilities(regular_user, :private) }
+
+ before { project.team << [regular_user, :developer] }
+
+ it do
+ is_expected.to include(:read_project_snippet)
+ is_expected.not_to include(*author_permissions)
+ end
+ end
+
+ context 'project team member external user' do
+ subject { abilities(external_user, :private) }
+
+ before { project.team << [external_user, :developer] }
it do
is_expected.to include(:read_project_snippet)
@@ -90,7 +140,7 @@ describe ProjectSnippetPolicy, models: true do
end
context 'admin user' do
- let(:current_user) { create(:admin) }
+ subject { abilities(create(:admin), :private) }
it do
is_expected.to include(:read_project_snippet)
diff --git a/spec/presenters/merge_request_presenter_spec.rb b/spec/presenters/merge_request_presenter_spec.rb
new file mode 100644
index 00000000000..44720fc4448
--- /dev/null
+++ b/spec/presenters/merge_request_presenter_spec.rb
@@ -0,0 +1,356 @@
+require 'spec_helper'
+
+describe MergeRequestPresenter do
+ let(:resource) { create :merge_request, source_project: project }
+ let(:project) { create :empty_project }
+ let(:user) { create(:user) }
+
+ describe '#ci_status' do
+ subject { described_class.new(resource).ci_status }
+
+ context 'when no head pipeline' do
+ it 'return status using CiService' do
+ ci_service = double(MockCiService)
+ ci_status = double
+
+ allow(resource.source_project)
+ .to receive(:ci_service)
+ .and_return(ci_service)
+
+ allow(resource).to receive(:head_pipeline).and_return(nil)
+
+ expect(ci_service).to receive(:commit_status)
+ .with(resource.diff_head_sha, resource.source_branch)
+ .and_return(ci_status)
+
+ is_expected.to eq(ci_status)
+ end
+ end
+
+ context 'when head pipeline present' do
+ let(:pipeline) { build_stubbed(:ci_pipeline) }
+
+ before do
+ allow(resource).to receive(:head_pipeline).and_return(pipeline)
+ end
+
+ context 'success with warnings' do
+ before do
+ allow(pipeline).to receive(:success?) { true }
+ allow(pipeline).to receive(:has_warnings?) { true }
+ end
+
+ it 'returns "success_with_warnings"' do
+ is_expected.to eq('success_with_warnings')
+ end
+ end
+
+ context 'pipeline HAS status AND its not success with warnings' do
+ before do
+ allow(pipeline).to receive(:success?) { false }
+ allow(pipeline).to receive(:has_warnings?) { false }
+ end
+
+ it 'returns pipeline status' do
+ is_expected.to eq('pending')
+ end
+ end
+
+ context 'pipeline has NO status AND its not success with warnings' do
+ before do
+ allow(pipeline).to receive(:status) { nil }
+ allow(pipeline).to receive(:success?) { false }
+ allow(pipeline).to receive(:has_warnings?) { false }
+ end
+
+ it 'returns "preparing"' do
+ is_expected.to eq('preparing')
+ end
+ end
+ end
+ end
+
+ describe '#conflict_resolution_path' do
+ let(:project) { create :empty_project }
+ let(:user) { create :user }
+ let(:presenter) { described_class.new(resource, current_user: user) }
+ let(:path) { presenter.conflict_resolution_path }
+
+ context 'when MR cannot be resolved in UI' do
+ it 'does not return conflict resolution path' do
+ allow(presenter).to receive_message_chain(:conflicts, :can_be_resolved_in_ui?) { false }
+
+ expect(path).to be_nil
+ end
+ end
+
+ context 'when conflicts cannot be resolved by user' do
+ it 'does not return conflict resolution path' do
+ allow(presenter).to receive_message_chain(:conflicts, :can_be_resolved_in_ui?) { true }
+ allow(presenter).to receive_message_chain(:conflicts, :can_be_resolved_by?).with(user) { false }
+
+ expect(path).to be_nil
+ end
+ end
+
+ context 'when able to access conflict resolution UI' do
+ it 'does return conflict resolution path' do
+ allow(presenter).to receive_message_chain(:conflicts, :can_be_resolved_in_ui?) { true }
+ allow(presenter).to receive_message_chain(:conflicts, :can_be_resolved_by?).with(user) { true }
+
+ expect(path)
+ .to eq("/#{project.full_path}/merge_requests/#{resource.iid}/conflicts")
+ end
+ end
+ end
+
+ context 'issues links' do
+ let(:project) { create(:project, :private, creator: user, namespace: user.namespace) }
+ let(:issue_a) { create(:issue, project: project) }
+ let(:issue_b) { create(:issue, project: project) }
+
+ let(:resource) do
+ create(:merge_request,
+ source_project: project, target_project: project,
+ description: "Fixes #{issue_a.to_reference} Related #{issue_b.to_reference}")
+ end
+
+ before do
+ project.team << [user, :developer]
+
+ allow(resource.project).to receive(:default_branch)
+ .and_return(resource.target_branch)
+ end
+
+ describe '#closing_issues_links' do
+ subject { described_class.new(resource, current_user: user).closing_issues_links }
+
+ it 'presents closing issues links' do
+ is_expected.to match("#{project.full_path}/issues/#{issue_a.iid}")
+ end
+
+ it 'does not present related issues links' do
+ is_expected.not_to match("#{project.full_path}/issues/#{issue_b.iid}")
+ end
+ end
+
+ describe '#mentioned_issues_links' do
+ subject do
+ described_class.new(resource, current_user: user)
+ .mentioned_issues_links
+ end
+
+ it 'presents related issues links' do
+ is_expected.to match("#{project.full_path}/issues/#{issue_b.iid}")
+ end
+
+ it 'does not present closing issues links' do
+ is_expected.not_to match("#{project.full_path}/issues/#{issue_a.iid}")
+ end
+ end
+
+ describe '#assign_to_closing_issues_link' do
+ subject do
+ described_class.new(resource, current_user: user)
+ .assign_to_closing_issues_link
+ end
+
+ before do
+ assign_issues_service = double(MergeRequests::AssignIssuesService, assignable_issues: assignable_issues)
+ allow(MergeRequests::AssignIssuesService).to receive(:new)
+ .and_return(assign_issues_service)
+ end
+
+ context 'single closing issue' do
+ let(:issue) { create(:issue) }
+ let(:assignable_issues) { [issue] }
+
+ it 'returns correct link with correct text' do
+ is_expected
+ .to match("#{project.full_path}/merge_requests/#{resource.iid}/assign_related_issues")
+
+ is_expected
+ .to match("Assign yourself to this issue")
+ end
+ end
+
+ context 'multiple closing issues' do
+ let(:issues) { create_list(:issue, 2) }
+ let(:assignable_issues) { issues }
+
+ it 'returns correct link with correct text' do
+ is_expected
+ .to match("#{project.full_path}/merge_requests/#{resource.iid}/assign_related_issues")
+
+ is_expected
+ .to match("Assign yourself to these issues")
+ end
+ end
+
+ context 'no closing issue' do
+ let(:assignable_issues) { [] }
+
+ it 'returns correct link with correct text' do
+ is_expected.to be_nil
+ end
+ end
+ end
+ end
+
+ describe '#cancel_merge_when_pipeline_succeeds_path' do
+ subject do
+ described_class.new(resource, current_user: user)
+ .cancel_merge_when_pipeline_succeeds_path
+ end
+
+ context 'when can cancel mwps' do
+ it 'returns path' do
+ allow(resource).to receive(:can_cancel_merge_when_pipeline_succeeds?)
+ .with(user)
+ .and_return(true)
+
+ is_expected.to eq("/#{resource.project.full_path}/merge_requests/#{resource.iid}/cancel_merge_when_pipeline_succeeds")
+ end
+ end
+
+ context 'when cannot cancel mwps' do
+ it 'returns nil' do
+ allow(resource).to receive(:can_cancel_merge_when_pipeline_succeeds?)
+ .with(user)
+ .and_return(false)
+
+ is_expected.to be_nil
+ end
+ end
+ end
+
+ describe '#merge_path' do
+ subject do
+ described_class.new(resource, current_user: user).merge_path
+ end
+
+ context 'when can be merged by user' do
+ it 'returns path' do
+ allow(resource).to receive(:can_be_merged_by?)
+ .with(user)
+ .and_return(true)
+
+ is_expected
+ .to eq("/#{resource.project.full_path}/merge_requests/#{resource.iid}/merge")
+ end
+ end
+
+ context 'when cannot be merged by user' do
+ it 'returns nil' do
+ allow(resource).to receive(:can_be_merged_by?)
+ .with(user)
+ .and_return(false)
+
+ is_expected.to be_nil
+ end
+ end
+ end
+
+ describe '#create_issue_to_resolve_discussions_path' do
+ subject do
+ described_class.new(resource, current_user: user)
+ .create_issue_to_resolve_discussions_path
+ end
+
+ context 'when can create issue and issues enabled' do
+ it 'returns path' do
+ allow(project).to receive(:issues_enabled?) { true }
+ project.team << [user, :master]
+
+ is_expected
+ .to eq("/#{resource.project.full_path}/issues/new?merge_request_to_resolve_discussions_of=#{resource.iid}")
+ end
+ end
+
+ context 'when cannot create issue' do
+ it 'returns nil' do
+ allow(project).to receive(:issues_enabled?) { true }
+
+ is_expected.to be_nil
+ end
+ end
+
+ context 'when issues disabled' do
+ it 'returns nil' do
+ allow(project).to receive(:issues_enabled?) { false }
+ project.team << [user, :master]
+
+ is_expected.to be_nil
+ end
+ end
+ end
+
+ describe '#remove_wip_path' do
+ subject do
+ described_class.new(resource, current_user: user).remove_wip_path
+ end
+
+ context 'when merge request enabled and has permission' do
+ it 'has remove_wip_path' do
+ allow(project).to receive(:merge_requests_enabled?) { true }
+ project.team << [user, :master]
+
+ is_expected
+ .to eq("/#{resource.project.full_path}/merge_requests/#{resource.iid}/remove_wip")
+ end
+ end
+
+ context 'when has no permission' do
+ it 'returns nil' do
+ is_expected.to be_nil
+ end
+ end
+ end
+
+ describe '#target_branch_commits_path' do
+ subject do
+ described_class.new(resource, current_user: user)
+ .target_branch_commits_path
+ end
+
+ context 'when target branch exists' do
+ it 'returns path' do
+ allow(resource).to receive(:target_branch_exists?) { true }
+
+ is_expected
+ .to eq("/#{resource.target_project.full_path}/commits/#{resource.target_branch}")
+ end
+ end
+
+ context 'when target branch does not exists' do
+ it 'returns nil' do
+ allow(resource).to receive(:target_branch_exists?) { false }
+
+ is_expected.to be_nil
+ end
+ end
+ end
+
+ describe '#source_branch_path' do
+ subject do
+ described_class.new(resource, current_user: user).source_branch_path
+ end
+
+ context 'when source branch exists' do
+ it 'returns path' do
+ allow(resource).to receive(:source_branch_exists?) { true }
+
+ is_expected
+ .to eq("/#{resource.source_project.full_path}/branches/#{resource.source_branch}")
+ end
+ end
+
+ context 'when source branch does not exists' do
+ it 'returns nil' do
+ allow(resource).to receive(:source_branch_exists?) { false }
+
+ is_expected.to be_nil
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/access_requests_spec.rb b/spec/requests/api/access_requests_spec.rb
index 46edbd49b28..c8eacb38e6f 100644
--- a/spec/requests/api/access_requests_spec.rb
+++ b/spec/requests/api/access_requests_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
-describe API::AccessRequests, api: true do
- include ApiHelpers
-
+describe API::AccessRequests do
let(:master) { create(:user) }
let(:developer) { create(:user) }
let(:access_requester) { create(:user) }
diff --git a/spec/requests/api/api_internal_helpers_spec.rb b/spec/requests/api/api_internal_helpers_spec.rb
deleted file mode 100644
index f5265ea60ff..00000000000
--- a/spec/requests/api/api_internal_helpers_spec.rb
+++ /dev/null
@@ -1,32 +0,0 @@
-require 'spec_helper'
-
-describe ::API::Helpers::InternalHelpers do
- include ::API::Helpers::InternalHelpers
-
- describe '.clean_project_path' do
- project = 'namespace/project'
- namespaced = File.join('namespace2', project)
-
- {
- File.join(Dir.pwd, project) => project,
- File.join(Dir.pwd, namespaced) => namespaced,
- project => project,
- namespaced => namespaced,
- project + '.git' => project,
- namespaced + '.git' => namespaced,
- "/" + project => project,
- "/" + namespaced => namespaced,
- }.each do |project_path, expected|
- context project_path do
- # Relative and absolute storage paths, with and without trailing /
- ['.', './', Dir.pwd, Dir.pwd + '/'].each do |storage_path|
- context "storage path is #{storage_path}" do
- subject { clean_project_path(project_path, [{ 'path' => storage_path }]) }
-
- it { is_expected.to eq(expected) }
- end
- end
- end
- end
- end
-end
diff --git a/spec/requests/api/award_emoji_spec.rb b/spec/requests/api/award_emoji_spec.rb
index f4d4a8a2cc7..bbdef0aeb1b 100644
--- a/spec/requests/api/award_emoji_spec.rb
+++ b/spec/requests/api/award_emoji_spec.rb
@@ -1,7 +1,6 @@
require 'spec_helper'
-describe API::AwardEmoji, api: true do
- include ApiHelpers
+describe API::AwardEmoji do
let(:user) { create(:user) }
let!(:project) { create(:empty_project) }
let(:issue) { create(:issue, project: project) }
diff --git a/spec/requests/api/boards_spec.rb b/spec/requests/api/boards_spec.rb
index 87c36639cd4..c27db716ef8 100644
--- a/spec/requests/api/boards_spec.rb
+++ b/spec/requests/api/boards_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
-describe API::Boards, api: true do
- include ApiHelpers
-
+describe API::Boards do
let(:user) { create(:user) }
let(:user2) { create(:user) }
let(:non_member) { create(:user) }
diff --git a/spec/requests/api/branches_spec.rb b/spec/requests/api/branches_spec.rb
index a70f7beaae0..c64499fc8c0 100644
--- a/spec/requests/api/branches_spec.rb
+++ b/spec/requests/api/branches_spec.rb
@@ -1,9 +1,7 @@
require 'spec_helper'
require 'mime/types'
-describe API::Branches, api: true do
- include ApiHelpers
-
+describe API::Branches do
let(:user) { create(:user) }
let!(:project) { create(:project, :repository, creator: user) }
let!(:master) { create(:project_member, :master, user: user, project: project) }
@@ -408,19 +406,6 @@ describe API::Branches, api: true do
delete api("/projects/#{project.id}/repository/branches/foobar", user)
expect(response).to have_http_status(404)
end
-
- 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 "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')
- end
end
describe "DELETE /projects/:id/repository/merged_branches" do
diff --git a/spec/requests/api/broadcast_messages_spec.rb b/spec/requests/api/broadcast_messages_spec.rb
index 024fa66848c..67989689799 100644
--- a/spec/requests/api/broadcast_messages_spec.rb
+++ b/spec/requests/api/broadcast_messages_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
-describe API::BroadcastMessages, api: true do
- include ApiHelpers
-
+describe API::BroadcastMessages do
let(:user) { create(:user) }
let(:admin) { create(:admin) }
diff --git a/spec/requests/api/commit_statuses_spec.rb b/spec/requests/api/commit_statuses_spec.rb
index d8b3cc041a5..1c163cee152 100644
--- a/spec/requests/api/commit_statuses_spec.rb
+++ b/spec/requests/api/commit_statuses_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
-describe API::CommitStatuses, api: true do
- include ApiHelpers
-
+describe API::CommitStatuses do
let!(:project) { create(:project, :repository) }
let(:commit) { project.repository.commit }
let(:guest) { create_user(:guest) }
@@ -28,8 +26,8 @@ describe API::CommitStatuses, api: true do
create(:commit_status, { pipeline: commit, ref: commit.ref }.merge(opts))
end
- let!(:status1) { create_status(master, status: 'running') }
- let!(:status2) { create_status(master, name: 'coverage', status: 'pending') }
+ let!(:status1) { create_status(master, status: 'running', retried: true) }
+ let!(:status2) { create_status(master, name: 'coverage', status: 'pending', retried: true) }
let!(:status3) { create_status(develop, status: 'running', allow_failure: true) }
let!(:status4) { create_status(master, name: 'coverage', status: 'success') }
let!(:status5) { create_status(develop, name: 'coverage', status: 'success') }
diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb
index 42dbab586cd..0b0e4c2b112 100644
--- a/spec/requests/api/commits_spec.rb
+++ b/spec/requests/api/commits_spec.rb
@@ -1,8 +1,7 @@
require 'spec_helper'
require 'mime/types'
-describe API::Commits, api: true do
- include ApiHelpers
+describe API::Commits do
let(:user) { create(:user) }
let(:user2) { create(:user) }
let!(:project) { create(:project, :repository, creator: user, namespace: user.namespace) }
diff --git a/spec/requests/api/deploy_keys_spec.rb b/spec/requests/api/deploy_keys_spec.rb
index e1beac28dab..843e9862b0c 100644
--- a/spec/requests/api/deploy_keys_spec.rb
+++ b/spec/requests/api/deploy_keys_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
-describe API::DeployKeys, api: true do
- include ApiHelpers
-
+describe API::DeployKeys do
let(:user) { create(:user) }
let(:admin) { create(:admin) }
let(:project) { create(:empty_project, creator_id: user.id) }
diff --git a/spec/requests/api/deployments_spec.rb b/spec/requests/api/deployments_spec.rb
index e55575ffbda..90d78d060ca 100644
--- a/spec/requests/api/deployments_spec.rb
+++ b/spec/requests/api/deployments_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
-describe API::Deployments, api: true do
- include ApiHelpers
-
+describe API::Deployments do
let(:user) { create(:user) }
let(:non_member) { create(:user) }
let(:project) { deployment.environment.project }
diff --git a/spec/requests/api/doorkeeper_access_spec.rb b/spec/requests/api/doorkeeper_access_spec.rb
index f6fd567eca5..868fef65c1c 100644
--- a/spec/requests/api/doorkeeper_access_spec.rb
+++ b/spec/requests/api/doorkeeper_access_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
-describe API::API, api: true do
- include ApiHelpers
-
+describe 'doorkeeper access' do
let!(:user) { create(:user) }
let!(:application) { Doorkeeper::Application.create!(name: "MyApp", redirect_uri: "https://app.com", owner: user) }
let!(:token) { Doorkeeper::AccessToken.create! application_id: application.id, resource_owner_id: user.id, scopes: "api" }
diff --git a/spec/requests/api/environments_spec.rb b/spec/requests/api/environments_spec.rb
index b54ee8e8b85..aae03c84e1f 100644
--- a/spec/requests/api/environments_spec.rb
+++ b/spec/requests/api/environments_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
-describe API::Environments, api: true do
- include ApiHelpers
-
+describe API::Environments do
let(:user) { create(:user) }
let(:non_member) { create(:user) }
let(:project) { create(:empty_project, :private, namespace: user.namespace) }
diff --git a/spec/requests/api/files_spec.rb b/spec/requests/api/files_spec.rb
index 6db2faed76b..deb2cac6869 100644
--- a/spec/requests/api/files_spec.rb
+++ b/spec/requests/api/files_spec.rb
@@ -1,7 +1,6 @@
require 'spec_helper'
-describe API::Files, api: true do
- include ApiHelpers
+describe API::Files do
let(:user) { create(:user) }
let!(:project) { create(:project, :repository, namespace: user.namespace ) }
let(:guest) { create(:user) { |u| project.add_guest(u) } }
@@ -330,7 +329,7 @@ describe API::Files, api: true do
end
let(:get_params) do
{
- ref: 'master',
+ ref: 'master'
}
end
diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb
index 2545da7b1db..90b36374ded 100644
--- a/spec/requests/api/groups_spec.rb
+++ b/spec/requests/api/groups_spec.rb
@@ -1,7 +1,6 @@
require 'spec_helper'
-describe API::Groups, api: true do
- include ApiHelpers
+describe API::Groups do
include UploadHelpers
let(:user1) { create(:user, can_create_group: false) }
@@ -74,7 +73,7 @@ describe API::Groups, api: true do
storage_size: 702,
repository_size: 123,
lfs_objects_size: 234,
- build_artifacts_size: 345,
+ build_artifacts_size: 345
}.stringify_keys
exposed_attributes = attributes.dup
exposed_attributes['job_artifacts_size'] = exposed_attributes.delete('build_artifacts_size')
@@ -179,7 +178,7 @@ describe API::Groups, api: true do
expect(json_response['path']).to eq(group1.path)
expect(json_response['description']).to eq(group1.description)
expect(json_response['visibility']).to eq(Gitlab::VisibilityLevel.string_level(group1.visibility_level))
- expect(json_response['avatar_url']).to eq(group1.avatar_url)
+ expect(json_response['avatar_url']).to eq(group1.avatar_url(only_path: false))
expect(json_response['web_url']).to eq(group1.web_url)
expect(json_response['request_access_enabled']).to eq(group1.request_access_enabled)
expect(json_response['full_name']).to eq(group1.full_name)
diff --git a/spec/requests/api/helpers_spec.rb b/spec/requests/api/helpers_spec.rb
index 988a57a80ea..ed392acc607 100644
--- a/spec/requests/api/helpers_spec.rb
+++ b/spec/requests/api/helpers_spec.rb
@@ -1,8 +1,8 @@
require 'spec_helper'
-describe API::Helpers, api: true do
+describe API::Helpers do
include API::APIGuard::HelperMethods
- include API::Helpers
+ include described_class
include SentryHelper
let(:user) { create(:user) }
@@ -427,6 +427,7 @@ describe API::Helpers, api: true do
context 'current_user is nil' do
before do
expect_any_instance_of(self.class).to receive(:current_user).and_return(nil)
+ allow_any_instance_of(self.class).to receive(:initial_current_user).and_return(nil)
end
it 'returns a 401 response' do
@@ -435,13 +436,38 @@ describe API::Helpers, api: true do
end
context 'current_user is present' do
+ let(:user) { build(:user) }
+
before do
- expect_any_instance_of(self.class).to receive(:current_user).at_least(:once).and_return(User.new)
+ expect_any_instance_of(self.class).to receive(:current_user).at_least(:once).and_return(user)
+ expect_any_instance_of(self.class).to receive(:initial_current_user).and_return(user)
end
it 'does not raise an error' do
expect { authenticate! }.not_to raise_error
end
end
+
+ context 'current_user is blocked' do
+ let(:user) { build(:user, :blocked) }
+
+ before do
+ expect_any_instance_of(self.class).to receive(:current_user).at_least(:once).and_return(user)
+ end
+
+ it 'raises an error' do
+ expect_any_instance_of(self.class).to receive(:initial_current_user).and_return(user)
+
+ expect { authenticate! }.to raise_error '401 - {"message"=>"401 Unauthorized"}'
+ end
+
+ it "doesn't raise an error if an admin user is impersonating a blocked user (via sudo)" do
+ admin_user = build(:user, :admin)
+
+ expect_any_instance_of(self.class).to receive(:initial_current_user).and_return(admin_user)
+
+ expect { authenticate! }.not_to raise_error
+ end
+ end
end
end
diff --git a/spec/requests/api/internal_spec.rb b/spec/requests/api/internal_spec.rb
index 3d6010ede73..2ceb4648ece 100644
--- a/spec/requests/api/internal_spec.rb
+++ b/spec/requests/api/internal_spec.rb
@@ -1,7 +1,6 @@
require 'spec_helper'
-describe API::Internal, api: true do
- include ApiHelpers
+describe API::Internal do
let(:user) { create(:user) }
let(:key) { create(:key, user: user) }
let(:project) { create(:project, :repository) }
@@ -181,6 +180,7 @@ describe API::Internal, api: true do
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)
+ expect(json_response["gl_repository"]).to eq("wiki-#{project.id}")
expect(user).not_to have_an_activity_record
end
end
@@ -192,6 +192,7 @@ describe API::Internal, api: true do
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)
+ expect(json_response["gl_repository"]).to eq("wiki-#{project.id}")
expect(user).to have_an_activity_record
end
end
@@ -203,6 +204,7 @@ describe API::Internal, api: true do
expect(response).to have_http_status(200)
expect(json_response["status"]).to be_truthy
expect(json_response["repository_path"]).to eq(project.repository.path_to_repo)
+ expect(json_response["gl_repository"]).to eq("project-#{project.id}")
expect(user).to have_an_activity_record
end
end
@@ -214,6 +216,7 @@ describe API::Internal, api: true do
expect(response).to have_http_status(200)
expect(json_response["status"]).to be_truthy
expect(json_response["repository_path"]).to eq(project.repository.path_to_repo)
+ expect(json_response["gl_repository"]).to eq("project-#{project.id}")
expect(user).not_to have_an_activity_record
end
@@ -224,6 +227,7 @@ describe API::Internal, api: true do
expect(response).to have_http_status(200)
expect(json_response["status"]).to be_truthy
expect(json_response["repository_path"]).to eq(project.repository.path_to_repo)
+ expect(json_response["gl_repository"]).to eq("project-#{project.id}")
end
end
@@ -234,6 +238,7 @@ describe API::Internal, api: true do
expect(response).to have_http_status(200)
expect(json_response["status"]).to be_truthy
expect(json_response["repository_path"]).to eq(project.repository.path_to_repo)
+ expect(json_response["gl_repository"]).to eq("project-#{project.id}")
end
end
end
@@ -445,18 +450,39 @@ describe API::Internal, api: true do
expect(json_response).to eq([])
end
+
+ context 'with a gl_repository parameter' do
+ let(:gl_repository) { "project-#{project.id}" }
+
+ it 'returns link to create new merge request' do
+ get api("/internal/merge_request_urls?gl_repository=#{gl_repository}&changes=#{changes}"), secret_token: secret_token
+
+ expect(json_response).to match [{
+ "branch_name" => "new_branch",
+ "url" => "http://#{Gitlab.config.gitlab.host}/#{project.namespace.name}/#{project.path}/merge_requests/new?merge_request%5Bsource_branch%5D=new_branch",
+ "new_merge_request" => true
+ }]
+ end
+ end
end
describe 'POST /notify_post_receive' do
let(:valid_params) do
- { repo_path: project.repository.path, secret_token: secret_token }
+ { project: project.repository.path, secret_token: secret_token }
+ end
+
+ let(:valid_wiki_params) do
+ { project: project.wiki.repository.path, secret_token: secret_token }
end
before do
allow(Gitlab.config.gitaly).to receive(:enabled).and_return(true)
end
- it "calls the Gitaly client if it's enabled" do
+ it "calls the Gitaly client with the project's repository" do
+ expect(Gitlab::GitalyClient::Notifications).
+ to receive(:new).with(gitlab_git_repository_with(path: project.repository.path)).
+ and_call_original
expect_any_instance_of(Gitlab::GitalyClient::Notifications).
to receive(:post_receive)
@@ -465,6 +491,18 @@ describe API::Internal, api: true do
expect(response).to have_http_status(200)
end
+ it "calls the Gitaly client with the wiki's repository if it's a wiki" do
+ expect(Gitlab::GitalyClient::Notifications).
+ to receive(:new).with(gitlab_git_repository_with(path: project.wiki.repository.path)).
+ and_call_original
+ expect_any_instance_of(Gitlab::GitalyClient::Notifications).
+ to receive(:post_receive)
+
+ post api("/internal/notify_post_receive"), valid_wiki_params
+
+ expect(response).to have_http_status(200)
+ end
+
it "returns 500 if the gitaly call fails" do
expect_any_instance_of(Gitlab::GitalyClient::Notifications).
to receive(:post_receive).and_raise(GRPC::Unavailable)
@@ -473,6 +511,40 @@ describe API::Internal, api: true do
expect(response).to have_http_status(500)
end
+
+ context 'with a gl_repository parameter' do
+ let(:valid_params) do
+ { gl_repository: "project-#{project.id}", secret_token: secret_token }
+ end
+
+ let(:valid_wiki_params) do
+ { gl_repository: "wiki-#{project.id}", secret_token: secret_token }
+ end
+
+ it "calls the Gitaly client with the project's repository" do
+ expect(Gitlab::GitalyClient::Notifications).
+ to receive(:new).with(gitlab_git_repository_with(path: project.repository.path)).
+ and_call_original
+ expect_any_instance_of(Gitlab::GitalyClient::Notifications).
+ to receive(:post_receive)
+
+ post api("/internal/notify_post_receive"), valid_params
+
+ expect(response).to have_http_status(200)
+ end
+
+ it "calls the Gitaly client with the wiki's repository if it's a wiki" do
+ expect(Gitlab::GitalyClient::Notifications).
+ to receive(:new).with(gitlab_git_repository_with(path: project.wiki.repository.path)).
+ and_call_original
+ expect_any_instance_of(Gitlab::GitalyClient::Notifications).
+ to receive(:post_receive)
+
+ post api("/internal/notify_post_receive"), valid_wiki_params
+
+ expect(response).to have_http_status(200)
+ end
+ end
end
def project_with_repo_path(path)
diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb
index 551aae7d701..79cac721202 100644
--- a/spec/requests/api/issues_spec.rb
+++ b/spec/requests/api/issues_spec.rb
@@ -1,23 +1,25 @@
require 'spec_helper'
-describe API::Issues, api: true do
- include ApiHelpers
+describe API::Issues do
include EmailHelpers
- let(:user) { create(:user) }
+ set(:user) { create(:user) }
+ set(:project) do
+ create(:empty_project, :public, creator_id: user.id, namespace: user.namespace)
+ end
+
let(:user2) { create(:user) }
let(:non_member) { create(:user) }
- let(:guest) { create(:user) }
- let(:author) { create(:author) }
- let(:assignee) { create(:assignee) }
+ set(:guest) { create(:user) }
+ set(:author) { create(:author) }
+ set(:assignee) { create(:assignee) }
let(:admin) { create(:user, :admin) }
- let!(:project) { create(:empty_project, :public, creator_id: user.id, namespace: user.namespace ) }
let(:issue_title) { 'foo' }
let(:issue_description) { 'closed' }
let!(:closed_issue) do
create :closed_issue,
author: user,
- assignee: user,
+ assignees: [user],
project: project,
state: :closed,
milestone: milestone,
@@ -29,14 +31,14 @@ describe API::Issues, api: true do
:confidential,
project: project,
author: author,
- assignee: assignee,
+ assignees: [assignee],
created_at: generate(:past_time),
updated_at: 2.hours.ago
end
let!(:issue) do
create :issue,
author: user,
- assignee: user,
+ assignees: [user],
project: project,
milestone: milestone,
created_at: generate(:past_time),
@@ -44,19 +46,19 @@ describe API::Issues, api: true do
title: issue_title,
description: issue_description
end
- let!(:label) do
+ set(:label) do
create(:label, title: 'label', color: '#FFAABB', project: project)
end
let!(:label_link) { create(:label_link, label: label, target: issue) }
- let!(:milestone) { create(:milestone, title: '1.0.0', project: project) }
- let!(:empty_milestone) do
+ set(:milestone) { create(:milestone, title: '1.0.0', project: project) }
+ set(:empty_milestone) do
create(:milestone, title: '2.0.0', project: project)
end
let!(:note) { create(:note_on_issue, author: user, project: project, noteable: issue) }
let(:no_milestone_title) { URI.escape(Milestone::None.title) }
- before do
+ before(:all) do
project.team << [user, :reporter]
project.team << [guest, :guest]
end
@@ -71,6 +73,8 @@ describe API::Issues, api: true do
end
context "when authenticated" do
+ let(:first_issue) { json_response.first }
+
it "returns an array of issues" do
get api("/issues", user)
@@ -80,46 +84,46 @@ describe API::Issues, api: true do
end
it 'returns an array of closed issues' do
- get api('/issues?state=closed', user)
+ get api('/issues', user), state: :closed
expect_paginated_array_response(size: 1)
- expect(json_response.first['id']).to eq(closed_issue.id)
+ expect(first_issue['id']).to eq(closed_issue.id)
end
it 'returns an array of opened issues' do
- get api('/issues?state=opened', user)
+ get api('/issues', user), state: :opened
expect_paginated_array_response(size: 1)
- expect(json_response.first['id']).to eq(issue.id)
+ expect(first_issue['id']).to eq(issue.id)
end
it 'returns an array of all issues' do
- get api('/issues?state=all', user)
+ get api('/issues', user), state: :all
expect_paginated_array_response(size: 2)
- expect(json_response.first['id']).to eq(issue.id)
+ expect(first_issue['id']).to eq(issue.id)
expect(json_response.second['id']).to eq(closed_issue.id)
end
it 'returns issues matching given search string for title' do
- get api("/issues?search=#{issue.title}", user)
+ get api("/issues", user), search: issue.title
expect_paginated_array_response(size: 1)
expect(json_response.first['id']).to eq(issue.id)
end
it 'returns issues matching given search string for description' do
- get api("/issues?search=#{issue.description}", user)
+ get api("/issues", user), search: issue.description
expect_paginated_array_response(size: 1)
- expect(json_response.first['id']).to eq(issue.id)
+ expect(first_issue['id']).to eq(issue.id)
end
it 'returns an array of labeled issues' do
- get api("/issues?labels=#{label.title}", user)
+ get api("/issues", user), labels: label.title
expect_paginated_array_response(size: 1)
- expect(json_response.first['labels']).to eq([label.title])
+ expect(first_issue['labels']).to eq([label.title])
end
it 'returns an array of labeled issues when all labels matches' do
@@ -136,13 +140,13 @@ describe API::Issues, api: true do
end
it 'returns an empty array if no issue matches labels' do
- get api('/issues?labels=foo,bar', user)
+ get api('/issues', user), labels: 'foo,bar'
expect_paginated_array_response(size: 0)
end
it 'returns an array of labeled issues matching given state' do
- get api("/issues?labels=#{label.title}&state=opened", user)
+ get api("/issues", user), labels: label.title, state: :opened
expect_paginated_array_response(size: 1)
expect(json_response.first['labels']).to eq([label.title])
@@ -261,7 +265,7 @@ describe API::Issues, api: true do
let!(:group_closed_issue) do
create :closed_issue,
author: user,
- assignee: user,
+ assignees: [user],
project: group_project,
state: :closed,
milestone: group_milestone,
@@ -272,13 +276,13 @@ describe API::Issues, api: true do
:confidential,
project: group_project,
author: author,
- assignee: assignee,
+ assignees: [assignee],
updated_at: 2.hours.ago
end
let!(:group_issue) do
create :issue,
author: user,
- assignee: user,
+ assignees: [user],
project: group_project,
milestone: group_milestone,
updated_at: 1.hour.ago,
@@ -683,6 +687,7 @@ describe API::Issues, api: true do
expect(json_response['updated_at']).to be_present
expect(json_response['labels']).to eq(issue.label_names)
expect(json_response['milestone']).to be_a Hash
+ expect(json_response['assignees']).to be_a Array
expect(json_response['assignee']).to be_a Hash
expect(json_response['author']).to be_a Hash
expect(json_response['confidential']).to be_falsy
@@ -755,15 +760,41 @@ describe API::Issues, api: true do
end
describe "POST /projects/:id/issues" do
+ context 'support for deprecated assignee_id' do
+ it 'creates a new project issue' do
+ post api("/projects/#{project.id}/issues", user),
+ title: 'new issue', assignee_id: user2.id
+
+ expect(response).to have_http_status(201)
+ expect(json_response['title']).to eq('new issue')
+ expect(json_response['assignee']['name']).to eq(user2.name)
+ expect(json_response['assignees'].first['name']).to eq(user2.name)
+ end
+ end
+
+ context 'CE restrictions' do
+ it 'creates a new project issue with no more than one assignee' do
+ post api("/projects/#{project.id}/issues", user),
+ title: 'new issue', assignee_ids: [user2.id, guest.id]
+
+ expect(response).to have_http_status(201)
+ expect(json_response['title']).to eq('new issue')
+ expect(json_response['assignees'].count).to eq(1)
+ end
+ end
+
it 'creates a new project issue' do
post api("/projects/#{project.id}/issues", user),
- title: 'new issue', labels: 'label, label2'
+ title: 'new issue', labels: 'label, label2', weight: 3,
+ assignee_ids: [user2.id]
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['labels']).to eq(%w(label label2))
expect(json_response['confidential']).to be_falsy
+ expect(json_response['assignee']['name']).to eq(user2.name)
+ expect(json_response['assignees'].first['name']).to eq(user2.name)
end
it 'creates a new confidential project issue' do
@@ -1053,6 +1084,57 @@ describe API::Issues, api: true do
end
end
+ describe 'PUT /projects/:id/issues/:issue_iid to update assignee' do
+ context 'support for deprecated assignee_id' do
+ it 'removes assignee' do
+ put api("/projects/#{project.id}/issues/#{issue.iid}", user),
+ assignee_id: 0
+
+ expect(response).to have_http_status(200)
+
+ expect(json_response['assignee']).to be_nil
+ end
+
+ it 'updates an issue with new assignee' do
+ put api("/projects/#{project.id}/issues/#{issue.iid}", user),
+ assignee_id: user2.id
+
+ expect(response).to have_http_status(200)
+
+ expect(json_response['assignee']['name']).to eq(user2.name)
+ end
+ end
+
+ it 'removes assignee' do
+ put api("/projects/#{project.id}/issues/#{issue.iid}", user),
+ assignee_ids: [0]
+
+ expect(response).to have_http_status(200)
+
+ expect(json_response['assignees']).to be_empty
+ end
+
+ it 'updates an issue with new assignee' do
+ put api("/projects/#{project.id}/issues/#{issue.iid}", user),
+ assignee_ids: [user2.id]
+
+ expect(response).to have_http_status(200)
+
+ expect(json_response['assignees'].first['name']).to eq(user2.name)
+ end
+
+ context 'CE restrictions' do
+ it 'updates an issue with several assignees but only one has been applied' do
+ put api("/projects/#{project.id}/issues/#{issue.iid}", user),
+ assignee_ids: [user2.id, guest.id]
+
+ expect(response).to have_http_status(200)
+
+ expect(json_response['assignees'].size).to eq(1)
+ end
+ end
+ end
+
describe 'PUT /projects/:id/issues/:issue_iid to update labels' do
let!(:label) { create(:label, title: 'dummy', project: project) }
let!(:label_link) { create(:label_link, label: label, target: issue) }
@@ -1345,6 +1427,41 @@ describe API::Issues, api: true do
include_examples 'time tracking endpoints', 'issue'
end
+ describe 'GET :id/issues/:issue_iid/closed_by' do
+ let(:merge_request) do
+ create(:merge_request,
+ :simple,
+ author: user,
+ source_project: project,
+ target_project: project,
+ description: "closes #{issue.to_reference}")
+ end
+
+ before do
+ create(:merge_requests_closing_issues, issue: issue, merge_request: merge_request)
+ end
+
+ it 'returns merge requests that will close issue on merge' do
+ get api("/projects/#{project.id}/issues/#{issue.iid}/closed_by", user)
+
+ expect_paginated_array_response(size: 1)
+ end
+
+ context 'when no merge requests will close issue' do
+ it 'returns empty array' do
+ get api("/projects/#{project.id}/issues/#{closed_issue.iid}/closed_by", user)
+
+ expect_paginated_array_response(size: 0)
+ end
+ end
+
+ it "returns 404 when issue doesn't exists" do
+ get api("/projects/#{project.id}/issues/9999/closed_by", user)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
def expect_paginated_array_response(size: nil)
expect(response).to have_http_status(200)
expect(response).to include_pagination_headers
diff --git a/spec/requests/api/jobs_spec.rb b/spec/requests/api/jobs_spec.rb
index d8a56c02a63..e5e5872dc1f 100644
--- a/spec/requests/api/jobs_spec.rb
+++ b/spec/requests/api/jobs_spec.rb
@@ -1,16 +1,26 @@
require 'spec_helper'
-describe API::Jobs, api: true do
- include ApiHelpers
+describe API::Jobs, :api do
+ let!(:project) do
+ create(:project, :repository, public_builds: false)
+ end
+
+ let!(:pipeline) do
+ create(:ci_empty_pipeline, project: project,
+ sha: project.commit.id,
+ ref: project.default_branch)
+ end
+
+ let!(:build) { create(:ci_build, pipeline: pipeline) }
let(:user) { create(:user) }
let(:api_user) { user }
- let!(:project) { create(:project, :repository, creator: user, public_builds: false) }
- let!(:developer) { create(:project_member, :developer, user: user, project: project) }
- 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) }
+ let(:reporter) { create(:project_member, :reporter, project: project).user }
+ let(:guest) { create(:project_member, :guest, project: project).user }
+
+ before do
+ project.add_developer(user)
+ end
describe 'GET /projects/:id/jobs' do
let(:query) { Hash.new }
@@ -213,7 +223,7 @@ describe API::Jobs, api: true do
end
describe 'GET /projects/:id/artifacts/:ref_name/download?job=name' do
- let(:api_user) { reporter.user }
+ let(:api_user) { reporter }
let(:build) { create(:ci_build, :artifacts, pipeline: pipeline) }
before do
@@ -237,7 +247,7 @@ describe API::Jobs, api: true do
end
context 'when logging as guest' do
- let(:api_user) { guest.user }
+ let(:api_user) { guest }
before do
get_for_ref
@@ -347,7 +357,7 @@ describe API::Jobs, api: true do
end
context 'user without :update_build permission' do
- let(:api_user) { reporter.user }
+ let(:api_user) { reporter }
it 'does not cancel job' do
expect(response).to have_http_status(403)
@@ -381,7 +391,7 @@ describe API::Jobs, api: true do
end
context 'user without :update_build permission' do
- let(:api_user) { reporter.user }
+ let(:api_user) { reporter }
it 'does not retry job' do
expect(response).to have_http_status(403)
@@ -457,16 +467,39 @@ describe API::Jobs, api: true do
describe 'POST /projects/:id/jobs/:job_id/play' do
before do
- post api("/projects/#{project.id}/jobs/#{build.id}/play", user)
+ post api("/projects/#{project.id}/jobs/#{build.id}/play", api_user)
end
context 'on an playable job' do
let(:build) { create(:ci_build, :manual, project: project, pipeline: pipeline) }
- it 'plays the job' 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)
+ context 'when user is authorized to trigger a manual action' do
+ it 'plays the job' 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)
+ expect(build.reload).to be_pending
+ end
+ end
+
+ context 'when user is not authorized to trigger a manual action' do
+ context 'when user does not have access to the project' do
+ let(:api_user) { create(:user) }
+
+ it 'does not trigger a manual action' do
+ expect(build.reload).to be_manual
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ context 'when user is not allowed to trigger the manual action' do
+ let(:api_user) { reporter }
+
+ it 'does not trigger a manual action' do
+ expect(build.reload).to be_manual
+ expect(response).to have_http_status(403)
+ end
+ end
end
end
diff --git a/spec/requests/api/keys_spec.rb b/spec/requests/api/keys_spec.rb
index 4c80987d680..ab957c72984 100644
--- a/spec/requests/api/keys_spec.rb
+++ b/spec/requests/api/keys_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
-describe API::Keys, api: true do
- include ApiHelpers
-
+describe API::Keys do
let(:user) { create(:user) }
let(:admin) { create(:admin) }
let(:key) { create(:key, user: user) }
@@ -34,6 +32,12 @@ describe API::Keys, api: true do
expect(json_response['user']['id']).to eq(user.id)
expect(json_response['user']['username']).to eq(user.username)
end
+
+ it "does not include the user's `is_admin` flag" do
+ get api("/keys/#{key.id}", admin)
+
+ expect(json_response['user']['is_admin']).to be_nil
+ end
end
end
end
diff --git a/spec/requests/api/labels_spec.rb b/spec/requests/api/labels_spec.rb
index a1adaba7b98..0c6b55c1630 100644
--- a/spec/requests/api/labels_spec.rb
+++ b/spec/requests/api/labels_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
-describe API::Labels, api: true do
- include ApiHelpers
-
+describe API::Labels do
let(:user) { create(:user) }
let(:project) { create(:empty_project, creator_id: user.id, namespace: user.namespace) }
let!(:label1) { create(:label, title: 'label1', project: project) }
diff --git a/spec/requests/api/lint_spec.rb b/spec/requests/api/lint_spec.rb
index 391fc13a380..df7c91b5bc1 100644
--- a/spec/requests/api/lint_spec.rb
+++ b/spec/requests/api/lint_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
-describe API::Lint, api: true do
- include ApiHelpers
-
+describe API::Lint do
describe 'POST /ci/lint' do
context 'with valid .gitlab-ci.yaml content' do
let(:yaml_content) do
diff --git a/spec/requests/api/members_spec.rb b/spec/requests/api/members_spec.rb
index 84dca51801f..e095053fa03 100644
--- a/spec/requests/api/members_spec.rb
+++ b/spec/requests/api/members_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
-describe API::Members, api: true do
- include ApiHelpers
-
+describe API::Members do
let(:master) { create(:user, username: 'master_user') }
let(:developer) { create(:user) }
let(:access_requester) { create(:user) }
diff --git a/spec/requests/api/merge_request_diffs_spec.rb b/spec/requests/api/merge_request_diffs_spec.rb
index 79f3151ba52..d1b22179888 100644
--- a/spec/requests/api/merge_request_diffs_spec.rb
+++ b/spec/requests/api/merge_request_diffs_spec.rb
@@ -1,8 +1,6 @@
require "spec_helper"
-describe API::MergeRequestDiffs, 'MergeRequestDiffs', api: true do
- include ApiHelpers
-
+describe API::MergeRequestDiffs, 'MergeRequestDiffs' do
let!(:user) { create(:user) }
let!(:merge_request) { create(:merge_request, importing: true) }
let!(:project) { merge_request.target_project }
diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb
index 61d965e8974..16e5efb2f5b 100644
--- a/spec/requests/api/merge_requests_spec.rb
+++ b/spec/requests/api/merge_requests_spec.rb
@@ -1,16 +1,22 @@
require "spec_helper"
-describe API::MergeRequests, api: true do
- include ApiHelpers
+describe API::MergeRequests do
let(:base_time) { Time.now }
let(:user) { create(:user) }
let(:admin) { create(:user, :admin) }
let(:non_member) { create(:user) }
- let!(:project) { create(:project, :public, :repository, creator: user, namespace: user.namespace) }
- let!(:merge_request) { create(:merge_request, :simple, author: user, assignee: user, source_project: project, title: "Test", created_at: base_time) }
- let!(:merge_request_closed) { create(:merge_request, state: "closed", author: user, assignee: user, source_project: project, title: "Closed test", created_at: base_time + 1.second) }
- let!(:merge_request_merged) { create(:merge_request, state: "merged", author: user, assignee: user, source_project: project, title: "Merged test", created_at: base_time + 2.seconds, merge_commit_sha: '9999999999999999999999999999999999999999') }
+ let!(:project) { create(:project, :public, :repository, creator: user, namespace: user.namespace, only_allow_merge_if_pipeline_succeeds: false) }
let(:milestone) { create(:milestone, title: '1.0.0', project: project) }
+ let(:milestone1) { create(:milestone, title: '0.9', project: project) }
+ let!(:merge_request) { create(:merge_request, :simple, milestone: milestone1, author: user, assignee: user, source_project: project, target_project: project, title: "Test", created_at: base_time) }
+ let!(:merge_request_closed) { create(:merge_request, state: "closed", milestone: milestone1, author: user, assignee: user, source_project: project, target_project: project, title: "Closed test", created_at: base_time + 1.second) }
+ let!(:merge_request_merged) { create(:merge_request, state: "merged", author: user, assignee: user, source_project: project, target_project: project, title: "Merged test", created_at: base_time + 2.seconds, merge_commit_sha: '9999999999999999999999999999999999999999') }
+ let!(:note) { create(:note_on_merge_request, author: user, project: project, noteable: merge_request, note: "a comment on a MR") }
+ let!(:note2) { create(:note_on_merge_request, author: user, project: project, noteable: merge_request, note: "another comment on a MR") }
+ let!(:label) do
+ create(:label, title: 'label', color: '#FFAABB', project: project)
+ end
+ let!(:label_link) { create(:label_link, label: label, target: merge_request) }
before do
project.team << [user, :reporter]
@@ -20,6 +26,7 @@ describe API::MergeRequests, api: true do
context "when unauthenticated" do
it "returns authentication error" do
get api("/projects/#{project.id}/merge_requests")
+
expect(response).to have_http_status(401)
end
end
@@ -100,6 +107,63 @@ describe API::MergeRequests, api: true do
expect(response).to match_response_schema('public_api/v4/merge_requests')
end
+ it 'returns an empty array if no issue matches milestone' do
+ get api("/projects/#{project.id}/merge_requests", user), milestone: '1.0.0'
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(0)
+ end
+
+ it 'returns an empty array if milestone does not exist' do
+ get api("/projects/#{project.id}/merge_requests", user), milestone: 'foo'
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(0)
+ end
+
+ it 'returns an array of merge requests in given milestone' do
+ get api("/projects/#{project.id}/merge_requests", user), milestone: '0.9'
+
+ expect(json_response.first['title']).to eq merge_request_closed.title
+ expect(json_response.first['id']).to eq merge_request_closed.id
+ end
+
+ it 'returns an array of merge requests matching state in milestone' do
+ get api("/projects/#{project.id}/merge_requests", user), milestone: '0.9', state: 'closed'
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(1)
+ expect(json_response.first['id']).to eq(merge_request_closed.id)
+ end
+
+ it 'returns an array of labeled merge requests' do
+ get api("/projects/#{project.id}/merge_requests?labels=#{label.title}", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(1)
+ expect(json_response.first['labels']).to eq([label.title])
+ end
+
+ it 'returns an array of labeled merge requests where all labels match' do
+ get api("/projects/#{project.id}/merge_requests?labels=#{label.title},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 'returns an empty array if no merge request matches labels' do
+ get api("/projects/#{project.id}/merge_requests?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
+
context "with ordering" do
before do
@mr_later = mr_with_later_created_and_updated_at_time
@@ -167,7 +231,7 @@ describe API::MergeRequests, api: true do
expect(json_response['created_at']).to be_present
expect(json_response['updated_at']).to be_present
expect(json_response['labels']).to eq(merge_request.label_names)
- expect(json_response['milestone']).to be_nil
+ expect(json_response['milestone']).to be_a Hash
expect(json_response['assignee']).to be_a Hash
expect(json_response['author']).to be_a Hash
expect(json_response['target_branch']).to eq(merge_request.target_branch)
@@ -370,6 +434,19 @@ describe API::MergeRequests, api: true do
expect(json_response['title']).to eq('Test merge_request')
end
+ it 'returns 422 when target project has disabled merge requests' do
+ project.project_feature.update(merge_requests_access_level: 0)
+
+ post api("/projects/#{fork_project.id}/merge_requests", user2),
+ title: 'Test',
+ target_branch: 'master',
+ source_branch: 'markdown',
+ author: user2,
+ target_project_id: project.id
+
+ expect(response).to have_http_status(422)
+ end
+
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
@@ -527,6 +604,18 @@ describe API::MergeRequests, api: true do
expect(json_response['merge_when_pipeline_succeeds']).to eq(true)
end
+ it "enables merge when pipeline succeeds if the pipeline is active and only_allow_merge_if_pipeline_succeeds is true" do
+ allow_any_instance_of(MergeRequest).to receive(:head_pipeline).and_return(pipeline)
+ allow(pipeline).to receive(:active?).and_return(true)
+ project.update_attribute(:only_allow_merge_if_pipeline_succeeds, true)
+
+ put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/merge", user), merge_when_pipeline_succeeds: true
+
+ expect(response).to have_http_status(200)
+ expect(json_response['title']).to eq('Test')
+ expect(json_response['merge_when_pipeline_succeeds']).to eq(true)
+ end
+
it "returns 404 for an invalid merge request IID" do
put api("/projects/#{project.id}/merge_requests/12345/merge", user)
diff --git a/spec/requests/api/milestones_spec.rb b/spec/requests/api/milestones_spec.rb
index 598968aff70..dd74351a2b1 100644
--- a/spec/requests/api/milestones_spec.rb
+++ b/spec/requests/api/milestones_spec.rb
@@ -1,7 +1,6 @@
require 'spec_helper'
-describe API::Milestones, api: true do
- include ApiHelpers
+describe API::Milestones do
let(:user) { create(:user) }
let!(:project) { create(:empty_project, namespace: user.namespace ) }
let!(:closed_milestone) { create(:closed_milestone, project: project, title: 'version1', description: 'closed milestone') }
diff --git a/spec/requests/api/namespaces_spec.rb b/spec/requests/api/namespaces_spec.rb
index da8fa06d0af..3bf16a3ae27 100644
--- a/spec/requests/api/namespaces_spec.rb
+++ b/spec/requests/api/namespaces_spec.rb
@@ -1,7 +1,6 @@
require 'spec_helper'
-describe API::Namespaces, api: true do
- include ApiHelpers
+describe API::Namespaces do
let(:admin) { create(:admin) }
let(:user) { create(:user) }
let!(:group1) { create(:group) }
diff --git a/spec/requests/api/notes_spec.rb b/spec/requests/api/notes_spec.rb
index d8eb8ce921e..6afcd237c3c 100644
--- a/spec/requests/api/notes_spec.rb
+++ b/spec/requests/api/notes_spec.rb
@@ -1,7 +1,6 @@
require 'spec_helper'
-describe API::Notes, api: true do
- include ApiHelpers
+describe API::Notes do
let(:user) { create(:user) }
let!(:project) { create(:empty_project, :public, namespace: user.namespace) }
let!(:issue) { create(:issue, project: project, author: user) }
diff --git a/spec/requests/api/notification_settings_spec.rb b/spec/requests/api/notification_settings_spec.rb
index 39d3afcb78f..f619b7e6eaf 100644
--- a/spec/requests/api/notification_settings_spec.rb
+++ b/spec/requests/api/notification_settings_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
-describe API::NotificationSettings, api: true do
- include ApiHelpers
-
+describe API::NotificationSettings do
let(:user) { create(:user) }
let!(:group) { create(:group) }
let!(:project) { create(:empty_project, :public, creator_id: user.id, namespace: group) }
diff --git a/spec/requests/api/oauth_tokens_spec.rb b/spec/requests/api/oauth_tokens_spec.rb
index 367225df717..0d56e1f732e 100644
--- a/spec/requests/api/oauth_tokens_spec.rb
+++ b/spec/requests/api/oauth_tokens_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
-describe API::API, api: true do
- include ApiHelpers
-
+describe 'OAuth tokens' do
context 'Resource Owner Password Credentials' do
def request_oauth_token(user)
post '/oauth/token', username: user.username, password: user.password, grant_type: 'password'
diff --git a/spec/requests/api/pipelines_spec.rb b/spec/requests/api/pipelines_spec.rb
index 51af999b455..f9e5316b3de 100644
--- a/spec/requests/api/pipelines_spec.rb
+++ b/spec/requests/api/pipelines_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
-describe API::Pipelines, api: true do
- include ApiHelpers
-
+describe API::Pipelines do
let(:user) { create(:user) }
let(:non_member) { create(:user) }
let(:project) { create(:project, :repository, creator: user) }
@@ -26,6 +24,245 @@ describe API::Pipelines, api: true do
expect(json_response.first['id']).to eq pipeline.id
expect(json_response.first.keys).to contain_exactly(*%w[id sha ref status])
end
+
+ context 'when parameter is passed' do
+ %w[running pending].each do |target|
+ context "when scope is #{target}" do
+ before do
+ create(:ci_pipeline, project: project, status: target)
+ end
+
+ it 'returns matched pipelines' do
+ get api("/projects/#{project.id}/pipelines", user), scope: target
+
+ expect(response).to have_http_status(:ok)
+ expect(response).to include_pagination_headers
+ expect(json_response).not_to be_empty
+ json_response.each { |r| expect(r['status']).to eq(target) }
+ end
+ end
+ end
+
+ context 'when scope is finished' do
+ before do
+ create(:ci_pipeline, project: project, status: 'success')
+ create(:ci_pipeline, project: project, status: 'failed')
+ create(:ci_pipeline, project: project, status: 'canceled')
+ end
+
+ it 'returns matched pipelines' do
+ get api("/projects/#{project.id}/pipelines", user), scope: 'finished'
+
+ expect(response).to have_http_status(:ok)
+ expect(response).to include_pagination_headers
+ expect(json_response).not_to be_empty
+ json_response.each { |r| expect(r['status']).to be_in(%w[success failed canceled]) }
+ end
+ end
+
+ context 'when scope is branches or tags' do
+ let!(:pipeline_branch) { create(:ci_pipeline, project: project) }
+ let!(:pipeline_tag) { create(:ci_pipeline, project: project, ref: 'v1.0.0', tag: true) }
+
+ context 'when scope is branches' do
+ it 'returns matched pipelines' do
+ get api("/projects/#{project.id}/pipelines", user), scope: 'branches'
+
+ expect(response).to have_http_status(:ok)
+ expect(response).to include_pagination_headers
+ expect(json_response).not_to be_empty
+ expect(json_response.last['id']).to eq(pipeline_branch.id)
+ end
+ end
+
+ context 'when scope is tags' do
+ it 'returns matched pipelines' do
+ get api("/projects/#{project.id}/pipelines", user), scope: 'tags'
+
+ expect(response).to have_http_status(:ok)
+ expect(response).to include_pagination_headers
+ expect(json_response).not_to be_empty
+ expect(json_response.last['id']).to eq(pipeline_tag.id)
+ end
+ end
+ end
+
+ context 'when scope is invalid' do
+ it 'returns bad_request' do
+ get api("/projects/#{project.id}/pipelines", user), scope: 'invalid-scope'
+
+ expect(response).to have_http_status(:bad_request)
+ end
+ end
+
+ HasStatus::AVAILABLE_STATUSES.each do |target|
+ context "when status is #{target}" do
+ before do
+ create(:ci_pipeline, project: project, status: target)
+ exception_status = HasStatus::AVAILABLE_STATUSES - [target]
+ create(:ci_pipeline, project: project, status: exception_status.sample)
+ end
+
+ it 'returns matched pipelines' do
+ get api("/projects/#{project.id}/pipelines", user), status: target
+
+ expect(response).to have_http_status(:ok)
+ expect(response).to include_pagination_headers
+ expect(json_response).not_to be_empty
+ json_response.each { |r| expect(r['status']).to eq(target) }
+ end
+ end
+ end
+
+ context 'when status is invalid' do
+ it 'returns bad_request' do
+ get api("/projects/#{project.id}/pipelines", user), status: 'invalid-status'
+
+ expect(response).to have_http_status(:bad_request)
+ end
+ end
+
+ context 'when ref is specified' do
+ before do
+ create(:ci_pipeline, project: project)
+ end
+
+ context 'when ref exists' do
+ it 'returns matched pipelines' do
+ get api("/projects/#{project.id}/pipelines", user), ref: 'master'
+
+ expect(response).to have_http_status(:ok)
+ expect(response).to include_pagination_headers
+ expect(json_response).not_to be_empty
+ json_response.each { |r| expect(r['ref']).to eq('master') }
+ end
+ end
+
+ context 'when ref does not exist' do
+ it 'returns empty' do
+ get api("/projects/#{project.id}/pipelines", user), ref: 'invalid-ref'
+
+ expect(response).to have_http_status(:ok)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_empty
+ end
+ end
+ end
+
+ context 'when name is specified' do
+ let!(:pipeline) { create(:ci_pipeline, project: project, user: user) }
+
+ context 'when name exists' do
+ it 'returns matched pipelines' do
+ get api("/projects/#{project.id}/pipelines", user), name: user.name
+
+ expect(response).to have_http_status(:ok)
+ expect(response).to include_pagination_headers
+ expect(json_response.first['id']).to eq(pipeline.id)
+ end
+ end
+
+ context 'when name does not exist' do
+ it 'returns empty' do
+ get api("/projects/#{project.id}/pipelines", user), name: 'invalid-name'
+
+ expect(response).to have_http_status(:ok)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_empty
+ end
+ end
+ end
+
+ context 'when username is specified' do
+ let!(:pipeline) { create(:ci_pipeline, project: project, user: user) }
+
+ context 'when username exists' do
+ it 'returns matched pipelines' do
+ get api("/projects/#{project.id}/pipelines", user), username: user.username
+
+ expect(response).to have_http_status(:ok)
+ expect(response).to include_pagination_headers
+ expect(json_response.first['id']).to eq(pipeline.id)
+ end
+ end
+
+ context 'when username does not exist' do
+ it 'returns empty' do
+ get api("/projects/#{project.id}/pipelines", user), username: 'invalid-username'
+
+ expect(response).to have_http_status(:ok)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_empty
+ end
+ end
+ end
+
+ context 'when yaml_errors is specified' do
+ let!(:pipeline1) { create(:ci_pipeline, project: project, yaml_errors: 'Syntax error') }
+ let!(:pipeline2) { create(:ci_pipeline, project: project) }
+
+ context 'when yaml_errors is true' do
+ it 'returns matched pipelines' do
+ get api("/projects/#{project.id}/pipelines", user), yaml_errors: true
+
+ expect(response).to have_http_status(:ok)
+ expect(response).to include_pagination_headers
+ expect(json_response.first['id']).to eq(pipeline1.id)
+ end
+ end
+
+ context 'when yaml_errors is false' do
+ it 'returns matched pipelines' do
+ get api("/projects/#{project.id}/pipelines", user), yaml_errors: false
+
+ expect(response).to have_http_status(:ok)
+ expect(response).to include_pagination_headers
+ expect(json_response.first['id']).to eq(pipeline2.id)
+ end
+ end
+
+ context 'when yaml_errors is invalid' do
+ it 'returns bad_request' do
+ get api("/projects/#{project.id}/pipelines", user), yaml_errors: 'invalid-yaml_errors'
+
+ expect(response).to have_http_status(:bad_request)
+ end
+ end
+ end
+
+ context 'when order_by and sort are specified' do
+ context 'when order_by user_id' do
+ let!(:pipeline) { create_list(:ci_pipeline, 2, project: project, user: create(:user)) }
+
+ it 'sorts as user_id: :asc' do
+ get api("/projects/#{project.id}/pipelines", user), order_by: 'user_id', sort: 'asc'
+
+ expect(response).to have_http_status(:ok)
+ expect(response).to include_pagination_headers
+ expect(json_response).not_to be_empty
+ pipeline.sort_by { |p| p.user.id }.tap do |sorted_pipeline|
+ json_response.each_with_index { |r, i| expect(r['id']).to eq(sorted_pipeline[i].id) }
+ end
+ end
+
+ context 'when sort is invalid' do
+ it 'returns bad_request' do
+ get api("/projects/#{project.id}/pipelines", user), order_by: 'user_id', sort: 'invalid_sort'
+
+ expect(response).to have_http_status(:bad_request)
+ end
+ end
+ end
+
+ context 'when order_by is invalid' do
+ it 'returns bad_request' do
+ get api("/projects/#{project.id}/pipelines", user), order_by: 'lock_version', sort: 'asc'
+
+ expect(response).to have_http_status(:bad_request)
+ end
+ end
+ end
+ end
end
context 'unauthorized user' do
diff --git a/spec/requests/api/project_hooks_spec.rb b/spec/requests/api/project_hooks_spec.rb
index b1603233f9e..aee0e17a153 100644
--- a/spec/requests/api/project_hooks_spec.rb
+++ b/spec/requests/api/project_hooks_spec.rb
@@ -1,7 +1,6 @@
require 'spec_helper'
-describe API::ProjectHooks, 'ProjectHooks', api: true do
- include ApiHelpers
+describe API::ProjectHooks, 'ProjectHooks' do
let(:user) { create(:user) }
let(:user3) { create(:user) }
let!(:project) { create(:empty_project, creator_id: user.id, namespace: user.namespace) }
diff --git a/spec/requests/api/project_snippets_spec.rb b/spec/requests/api/project_snippets_spec.rb
index 9e88c19b0bc..3ab1764f5c3 100644
--- a/spec/requests/api/project_snippets_spec.rb
+++ b/spec/requests/api/project_snippets_spec.rb
@@ -1,8 +1,6 @@
require 'rails_helper'
-describe API::ProjectSnippets, api: true do
- include ApiHelpers
-
+describe API::ProjectSnippets do
let(:project) { create(:empty_project, :public) }
let(:user) { create(:user) }
let(:admin) { create(:admin) }
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index 40365585a56..d5c3b5b34ad 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
require 'spec_helper'
-describe API::Projects, :api do
+describe API::Projects do
include Gitlab::CurrentSettings
let(:user) { create(:user) }
@@ -661,10 +661,24 @@ describe API::Projects, :api do
'name' => user.namespace.name,
'path' => user.namespace.path,
'kind' => user.namespace.kind,
- 'full_path' => user.namespace.full_path,
+ 'full_path' => user.namespace.full_path
})
end
+ it "does not include statistics by default" do
+ get api("/projects/#{project.id}", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).not_to include 'statistics'
+ end
+
+ it "includes statistics if requested" do
+ get api("/projects/#{project.id}", user), statistics: true
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to include 'statistics'
+ end
+
describe 'permissions' do
context 'all projects' do
before { project.team << [user, :master] }
diff --git a/spec/requests/api/repositories_spec.rb b/spec/requests/api/repositories_spec.rb
index 4783d011d54..1a0695615e3 100644
--- a/spec/requests/api/repositories_spec.rb
+++ b/spec/requests/api/repositories_spec.rb
@@ -1,8 +1,7 @@
require 'spec_helper'
require 'mime/types'
-describe API::Repositories, api: true do
- include ApiHelpers
+describe API::Repositories do
include RepoHelpers
include WorkhorseHelpers
diff --git a/spec/requests/api/runner_spec.rb b/spec/requests/api/runner_spec.rb
index 409a59d6c23..be83514ed9c 100644
--- a/spec/requests/api/runner_spec.rb
+++ b/spec/requests/api/runner_spec.rb
@@ -1,7 +1,6 @@
require 'spec_helper'
describe API::Runner do
- include ApiHelpers
include StubGitlabCalls
let(:registration_token) { 'abcdefg123456' }
diff --git a/spec/requests/api/runners_spec.rb b/spec/requests/api/runners_spec.rb
index 8a82543a830..645a5389850 100644
--- a/spec/requests/api/runners_spec.rb
+++ b/spec/requests/api/runners_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
-describe API::Runners, api: true do
- include ApiHelpers
-
+describe API::Runners do
let(:admin) { create(:user, :admin) }
let(:user) { create(:user) }
let(:user2) { create(:user) }
diff --git a/spec/requests/api/services_spec.rb b/spec/requests/api/services_spec.rb
index fd334934ca5..95df3429314 100644
--- a/spec/requests/api/services_spec.rb
+++ b/spec/requests/api/services_spec.rb
@@ -1,8 +1,6 @@
require "spec_helper"
-describe API::Services, api: true do
- include ApiHelpers
-
+describe API::Services do
let(:user) { create(:user) }
let(:admin) { create(:admin) }
let(:user2) { create(:user) }
diff --git a/spec/requests/api/session_spec.rb b/spec/requests/api/session_spec.rb
index 393bf076616..5e77519c867 100644
--- a/spec/requests/api/session_spec.rb
+++ b/spec/requests/api/session_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
-describe API::Session, api: true do
- include ApiHelpers
-
+describe API::Session do
let(:user) { create(:user) }
describe "POST /session" do
diff --git a/spec/requests/api/settings_spec.rb b/spec/requests/api/settings_spec.rb
index 11b4b718e2c..2398ae6219c 100644
--- a/spec/requests/api/settings_spec.rb
+++ b/spec/requests/api/settings_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
-describe API::Settings, 'Settings', api: true do
- include ApiHelpers
-
+describe API::Settings, 'Settings' do
let(:user) { create(:user) }
let(:admin) { create(:admin) }
diff --git a/spec/requests/api/sidekiq_metrics_spec.rb b/spec/requests/api/sidekiq_metrics_spec.rb
index 28067f8ca88..83042d0cb12 100644
--- a/spec/requests/api/sidekiq_metrics_spec.rb
+++ b/spec/requests/api/sidekiq_metrics_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
-describe API::SidekiqMetrics, api: true do
- include ApiHelpers
-
+describe API::SidekiqMetrics do
let(:admin) { create(:user, :admin) }
describe 'GET sidekiq/*' do
diff --git a/spec/requests/api/snippets_spec.rb b/spec/requests/api/snippets_spec.rb
index 5d75b47b3cd..e429cddcf6a 100644
--- a/spec/requests/api/snippets_spec.rb
+++ b/spec/requests/api/snippets_spec.rb
@@ -1,7 +1,6 @@
require 'rails_helper'
-describe API::Snippets, api: true do
- include ApiHelpers
+describe API::Snippets do
let!(:user) { create(:user) }
describe 'GET /snippets/' do
diff --git a/spec/requests/api/system_hooks_spec.rb b/spec/requests/api/system_hooks_spec.rb
index d1e10f12657..2eb191d6049 100644
--- a/spec/requests/api/system_hooks_spec.rb
+++ b/spec/requests/api/system_hooks_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
-describe API::SystemHooks, api: true do
- include ApiHelpers
-
+describe API::SystemHooks do
let(:user) { create(:user) }
let(:admin) { create(:admin) }
let!(:hook) { create(:system_hook, url: "http://example.com") }
@@ -34,8 +32,9 @@ describe API::SystemHooks, api: true do
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.first['url']).to eq(hook.url)
- expect(json_response.first['push_events']).to be true
+ expect(json_response.first['push_events']).to be false
expect(json_response.first['tag_push_events']).to be false
+ expect(json_response.first['repository_update_events']).to be true
end
end
end
diff --git a/spec/requests/api/tags_spec.rb b/spec/requests/api/tags_spec.rb
index b132d033a61..ef7d0c3ee41 100644
--- a/spec/requests/api/tags_spec.rb
+++ b/spec/requests/api/tags_spec.rb
@@ -1,8 +1,7 @@
require 'spec_helper'
require 'mime/types'
-describe API::Tags, api: true do
- include ApiHelpers
+describe API::Tags do
include RepoHelpers
let(:user) { create(:user) }
diff --git a/spec/requests/api/templates_spec.rb b/spec/requests/api/templates_spec.rb
index 2c83e119065..cb55985e3f5 100644
--- a/spec/requests/api/templates_spec.rb
+++ b/spec/requests/api/templates_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
-describe API::Templates, api: true do
- include ApiHelpers
-
+describe API::Templates do
context 'the Template Entity' do
before { get api('/templates/gitignores/Ruby') }
diff --git a/spec/requests/api/todos_spec.rb b/spec/requests/api/todos_spec.rb
index b789284fa8d..92533f4dfea 100644
--- a/spec/requests/api/todos_spec.rb
+++ b/spec/requests/api/todos_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
-describe API::Todos, api: true do
- include ApiHelpers
-
+describe API::Todos do
let(:project_1) { create(:empty_project, :test_repo) }
let(:project_2) { create(:empty_project) }
let(:author_1) { create(:user) }
diff --git a/spec/requests/api/triggers_spec.rb b/spec/requests/api/triggers_spec.rb
index d93a734f5b6..16ddade27d9 100644
--- a/spec/requests/api/triggers_spec.rb
+++ b/spec/requests/api/triggers_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
describe API::Triggers do
- include ApiHelpers
-
let(:user) { create(:user) }
let(:user2) { create(:user) }
let!(:trigger_token) { 'secure_token' }
diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb
index 165ab389917..4919ad19833 100644
--- a/spec/requests/api/users_spec.rb
+++ b/spec/requests/api/users_spec.rb
@@ -1,9 +1,7 @@
require 'spec_helper'
-describe API::Users, api: true do
- include ApiHelpers
-
- let(:user) { create(:user) }
+describe API::Users do
+ let(:user) { create(:user) }
let(:admin) { create(:admin) }
let(:key) { create(:key, user: user) }
let(:email) { create(:email, user: user) }
@@ -137,6 +135,12 @@ describe API::Users, api: true do
expect(json_response['username']).to eq(user.username)
end
+ it "does not return the user's `is_admin` flag" do
+ get api("/users/#{user.id}", user)
+
+ expect(json_response['is_admin']).to be_nil
+ end
+
it "returns a 401 if unauthenticated" do
get api("/users/9998")
expect(response).to have_http_status(401)
@@ -399,7 +403,6 @@ describe API::Users, api: true 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
@@ -413,7 +416,6 @@ describe API::Users, api: true 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)
expect(admin_user.reload.admin).to eq(true)
expect(admin_user.can_create_group).to eq(false)
end
diff --git a/spec/requests/api/v3/award_emoji_spec.rb b/spec/requests/api/v3/award_emoji_spec.rb
index eeb4d128c1b..9234710f488 100644
--- a/spec/requests/api/v3/award_emoji_spec.rb
+++ b/spec/requests/api/v3/award_emoji_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
-describe API::V3::AwardEmoji, api: true do
- include ApiHelpers
-
+describe API::V3::AwardEmoji do
let(:user) { create(:user) }
let!(:project) { create(:empty_project) }
let(:issue) { create(:issue, project: project) }
diff --git a/spec/requests/api/v3/boards_spec.rb b/spec/requests/api/v3/boards_spec.rb
index eb95934f354..4d786331d1b 100644
--- a/spec/requests/api/v3/boards_spec.rb
+++ b/spec/requests/api/v3/boards_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
-describe API::V3::Boards, api: true do
- include ApiHelpers
-
+describe API::V3::Boards do
let(:user) { create(:user) }
let(:guest) { create(:user) }
let(:non_member) { create(:user) }
diff --git a/spec/requests/api/v3/branches_spec.rb b/spec/requests/api/v3/branches_spec.rb
index 5dcd4f21f4e..c88f7788697 100644
--- a/spec/requests/api/v3/branches_spec.rb
+++ b/spec/requests/api/v3/branches_spec.rb
@@ -1,9 +1,7 @@
require 'spec_helper'
require 'mime/types'
-describe API::V3::Branches, api: true do
- include ApiHelpers
-
+describe API::V3::Branches do
let(:user) { create(:user) }
let(:user2) { create(:user) }
let!(:project) { create(:project, :repository, creator: user) }
@@ -49,19 +47,6 @@ describe API::V3::Branches, api: true do
delete v3_api("/projects/#{project.id}/repository/branches/foobar", user)
expect(response).to have_http_status(404)
end
-
- it "removes protected branch" do
- create(:protected_branch, project: project, name: branch_name)
- delete v3_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 "does not remove HEAD branch" do
- delete v3_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')
- end
end
describe "DELETE /projects/:id/repository/merged_branches" do
diff --git a/spec/requests/api/v3/broadcast_messages_spec.rb b/spec/requests/api/v3/broadcast_messages_spec.rb
index 06556401a29..948cd78c177 100644
--- a/spec/requests/api/v3/broadcast_messages_spec.rb
+++ b/spec/requests/api/v3/broadcast_messages_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
-describe API::V3::BroadcastMessages, api: true do
- include ApiHelpers
-
+describe API::V3::BroadcastMessages do
let(:user) { create(:user) }
let(:admin) { create(:admin) }
diff --git a/spec/requests/api/v3/builds_spec.rb b/spec/requests/api/v3/builds_spec.rb
index e97d2b0cee0..dc95599546c 100644
--- a/spec/requests/api/v3/builds_spec.rb
+++ b/spec/requests/api/v3/builds_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
-describe API::V3::Builds, api: true do
- include ApiHelpers
-
+describe API::V3::Builds do
let(:user) { create(:user) }
let(:api_user) { user }
let!(:project) { create(:project, :repository, creator: user, public_builds: false) }
diff --git a/spec/requests/api/v3/commits_spec.rb b/spec/requests/api/v3/commits_spec.rb
index 0a28cb9bddb..c2e8c3ae6f7 100644
--- a/spec/requests/api/v3/commits_spec.rb
+++ b/spec/requests/api/v3/commits_spec.rb
@@ -1,8 +1,7 @@
require 'spec_helper'
require 'mime/types'
-describe API::V3::Commits, api: true do
- include ApiHelpers
+describe API::V3::Commits do
let(:user) { create(:user) }
let(:user2) { create(:user) }
let!(:project) { create(:project, :repository, creator: user, namespace: user.namespace) }
diff --git a/spec/requests/api/v3/deploy_keys_spec.rb b/spec/requests/api/v3/deploy_keys_spec.rb
index f5bdf408c5e..b61b2b618a6 100644
--- a/spec/requests/api/v3/deploy_keys_spec.rb
+++ b/spec/requests/api/v3/deploy_keys_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
-describe API::V3::DeployKeys, api: true do
- include ApiHelpers
-
+describe API::V3::DeployKeys do
let(:user) { create(:user) }
let(:admin) { create(:admin) }
let(:project) { create(:empty_project, creator_id: user.id) }
diff --git a/spec/requests/api/v3/deployments_spec.rb b/spec/requests/api/v3/deployments_spec.rb
index 3c5ce407b32..0389a264781 100644
--- a/spec/requests/api/v3/deployments_spec.rb
+++ b/spec/requests/api/v3/deployments_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
-describe API::Deployments, api: true do
- include ApiHelpers
-
+describe API::V3::Deployments do
let(:user) { create(:user) }
let(:non_member) { create(:user) }
let(:project) { deployment.environment.project }
@@ -26,11 +24,11 @@ describe API::Deployments, api: true do
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) }
+ let(:request) { get v3_api("/projects/#{project.id}/deployments", user) }
end
it 'returns projects deployments' do
- get api("/projects/#{project.id}/deployments", user)
+ get v3_api("/projects/#{project.id}/deployments", user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
@@ -42,7 +40,7 @@ describe API::Deployments, api: true do
context 'as non member' do
it 'returns a 404 status code' do
- get api("/projects/#{project.id}/deployments", non_member)
+ get v3_api("/projects/#{project.id}/deployments", non_member)
expect(response).to have_http_status(404)
end
@@ -52,7 +50,7 @@ describe API::Deployments, api: true do
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)
+ get v3_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/
@@ -62,7 +60,7 @@ describe API::Deployments, api: true do
context 'as non member' do
it 'returns a 404 status code' do
- get api("/projects/#{project.id}/deployments/#{deployment.id}", non_member)
+ get v3_api("/projects/#{project.id}/deployments/#{deployment.id}", non_member)
expect(response).to have_http_status(404)
end
diff --git a/spec/requests/api/v3/environments_spec.rb b/spec/requests/api/v3/environments_spec.rb
index 216192c9d34..99f35723974 100644
--- a/spec/requests/api/v3/environments_spec.rb
+++ b/spec/requests/api/v3/environments_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
-describe API::V3::Environments, api: true do
- include ApiHelpers
-
+describe API::V3::Environments do
let(:user) { create(:user) }
let(:non_member) { create(:user) }
let(:project) { create(:empty_project, :private, namespace: user.namespace) }
diff --git a/spec/requests/api/v3/files_spec.rb b/spec/requests/api/v3/files_spec.rb
index c45e2028e1d..378ca1720ff 100644
--- a/spec/requests/api/v3/files_spec.rb
+++ b/spec/requests/api/v3/files_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
-describe API::V3::Files, api: true do
- include ApiHelpers
-
+describe API::V3::Files do
# I have to remove periods from the end of the name
# This happened when the user's name had a suffix (i.e. "Sr.")
# This seems to be what git does under the hood. For example, this commit:
@@ -55,7 +53,7 @@ describe API::V3::Files, api: true do
let(:params) do
{
file_path: 'app/models/application.rb',
- ref: 'master',
+ ref: 'master'
}
end
@@ -265,7 +263,7 @@ describe API::V3::Files, api: true do
let(:get_params) do
{
file_path: file_path,
- ref: 'master',
+ ref: 'master'
}
end
diff --git a/spec/requests/api/v3/groups_spec.rb b/spec/requests/api/v3/groups_spec.rb
index a71b7d4b008..bc261b5e07c 100644
--- a/spec/requests/api/v3/groups_spec.rb
+++ b/spec/requests/api/v3/groups_spec.rb
@@ -1,7 +1,6 @@
require 'spec_helper'
-describe API::V3::Groups, api: true do
- include ApiHelpers
+describe API::V3::Groups do
include UploadHelpers
let(:user1) { create(:user, can_create_group: false) }
@@ -70,7 +69,7 @@ describe API::V3::Groups, api: true do
storage_size: 702,
repository_size: 123,
lfs_objects_size: 234,
- build_artifacts_size: 345,
+ build_artifacts_size: 345
}.stringify_keys
project1.statistics.update!(attributes)
@@ -177,7 +176,7 @@ describe API::V3::Groups, api: true do
expect(json_response['path']).to eq(group1.path)
expect(json_response['description']).to eq(group1.description)
expect(json_response['visibility_level']).to eq(group1.visibility_level)
- expect(json_response['avatar_url']).to eq(group1.avatar_url)
+ expect(json_response['avatar_url']).to eq(group1.avatar_url(only_path: false))
expect(json_response['web_url']).to eq(group1.web_url)
expect(json_response['request_access_enabled']).to eq(group1.request_access_enabled)
expect(json_response['full_name']).to eq(group1.full_name)
diff --git a/spec/requests/api/v3/issues_spec.rb b/spec/requests/api/v3/issues_spec.rb
index 91d9057075f..cc81922697a 100644
--- a/spec/requests/api/v3/issues_spec.rb
+++ b/spec/requests/api/v3/issues_spec.rb
@@ -1,7 +1,6 @@
require 'spec_helper'
-describe API::V3::Issues, api: true do
- include ApiHelpers
+describe API::V3::Issues do
include EmailHelpers
let(:user) { create(:user) }
@@ -15,7 +14,7 @@ describe API::V3::Issues, api: true do
let!(:closed_issue) do
create :closed_issue,
author: user,
- assignee: user,
+ assignees: [user],
project: project,
state: :closed,
milestone: milestone,
@@ -27,14 +26,14 @@ describe API::V3::Issues, api: true do
:confidential,
project: project,
author: author,
- assignee: assignee,
+ assignees: [assignee],
created_at: generate(:past_time),
updated_at: 2.hours.ago
end
let!(:issue) do
create :issue,
author: user,
- assignee: user,
+ assignees: [user],
project: project,
milestone: milestone,
created_at: generate(:past_time),
@@ -248,7 +247,7 @@ describe API::V3::Issues, api: true do
let!(:group_closed_issue) do
create :closed_issue,
author: user,
- assignee: user,
+ assignees: [user],
project: group_project,
state: :closed,
milestone: group_milestone,
@@ -259,13 +258,13 @@ describe API::V3::Issues, api: true do
:confidential,
project: group_project,
author: author,
- assignee: assignee,
+ assignees: [assignee],
updated_at: 2.hours.ago
end
let!(:group_issue) do
create :issue,
author: user,
- assignee: user,
+ assignees: [user],
project: group_project,
milestone: group_milestone,
updated_at: 1.hour.ago
@@ -738,13 +737,14 @@ describe API::V3::Issues, api: true do
describe "POST /projects/:id/issues" do
it 'creates a new project issue' do
post v3_api("/projects/#{project.id}/issues", user),
- title: 'new issue', labels: 'label, label2'
+ title: 'new issue', labels: 'label, label2', assignee_id: assignee.id
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['labels']).to eq(%w(label label2))
expect(json_response['confidential']).to be_falsy
+ expect(json_response['assignee']['name']).to eq(assignee.name)
end
it 'creates a new confidential project issue' do
@@ -1141,6 +1141,22 @@ describe API::V3::Issues, api: true do
end
end
+ describe 'PUT /projects/:id/issues/:issue_id to update assignee' do
+ it 'updates an issue with no assignee' do
+ put v3_api("/projects/#{project.id}/issues/#{issue.id}", user), assignee_id: 0
+
+ expect(response).to have_http_status(200)
+ expect(json_response['assignee']).to eq(nil)
+ end
+
+ it 'updates an issue with assignee' do
+ put v3_api("/projects/#{project.id}/issues/#{issue.id}", user), assignee_id: user2.id
+
+ expect(response).to have_http_status(200)
+ expect(json_response['assignee']['name']).to eq(user2.name)
+ end
+ end
+
describe "DELETE /projects/:id/issues/:issue_id" do
it "rejects a non member from deleting an issue" do
delete v3_api("/projects/#{project.id}/issues/#{issue.id}", non_member)
diff --git a/spec/requests/api/v3/labels_spec.rb b/spec/requests/api/v3/labels_spec.rb
index dfac357d37c..62faa1cb129 100644
--- a/spec/requests/api/v3/labels_spec.rb
+++ b/spec/requests/api/v3/labels_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
-describe API::V3::Labels, api: true do
- include ApiHelpers
-
+describe API::V3::Labels do
let(:user) { create(:user) }
let(:project) { create(:empty_project, creator_id: user.id, namespace: user.namespace) }
let!(:label1) { create(:label, title: 'label1', project: project) }
diff --git a/spec/requests/api/v3/members_spec.rb b/spec/requests/api/v3/members_spec.rb
index af1c5cff67f..623f02902b8 100644
--- a/spec/requests/api/v3/members_spec.rb
+++ b/spec/requests/api/v3/members_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
-describe API::V3::Members, api: true do
- include ApiHelpers
-
+describe API::V3::Members do
let(:master) { create(:user, username: 'master_user') }
let(:developer) { create(:user) }
let(:access_requester) { create(:user) }
diff --git a/spec/requests/api/v3/merge_request_diffs_spec.rb b/spec/requests/api/v3/merge_request_diffs_spec.rb
index c53800eef30..8020ddab4c8 100644
--- a/spec/requests/api/v3/merge_request_diffs_spec.rb
+++ b/spec/requests/api/v3/merge_request_diffs_spec.rb
@@ -1,8 +1,6 @@
require "spec_helper"
-describe API::V3::MergeRequestDiffs, 'MergeRequestDiffs', api: true do
- include ApiHelpers
-
+describe API::V3::MergeRequestDiffs, 'MergeRequestDiffs' do
let!(:user) { create(:user) }
let!(:merge_request) { create(:merge_request, importing: true) }
let!(:project) { merge_request.target_project }
diff --git a/spec/requests/api/v3/merge_requests_spec.rb b/spec/requests/api/v3/merge_requests_spec.rb
index d73e9635c9b..f6ff96be566 100644
--- a/spec/requests/api/v3/merge_requests_spec.rb
+++ b/spec/requests/api/v3/merge_requests_spec.rb
@@ -1,7 +1,6 @@
require "spec_helper"
-describe API::MergeRequests, api: true do
- include ApiHelpers
+describe API::MergeRequests do
let(:base_time) { Time.now }
let(:user) { create(:user) }
let(:admin) { create(:user, :admin) }
@@ -339,6 +338,19 @@ describe API::MergeRequests, api: true do
expect(json_response['title']).to eq('Test merge_request')
end
+ it "returns 422 when target project has disabled merge requests" do
+ project.project_feature.update(merge_requests_access_level: 0)
+
+ post v3_api("/projects/#{fork_project.id}/merge_requests", user2),
+ title: 'Test',
+ target_branch: "master",
+ source_branch: 'markdown',
+ author: user2,
+ target_project_id: project.id
+
+ expect(response).to have_http_status(422)
+ end
+
it "returns 400 when source_branch is missing" do
post v3_api("/projects/#{fork_project.id}/merge_requests", user2),
title: 'Test merge_request', target_branch: "master", author: user2, target_project_id: project.id
diff --git a/spec/requests/api/v3/milestones_spec.rb b/spec/requests/api/v3/milestones_spec.rb
index 127c0eec881..f04efc990a7 100644
--- a/spec/requests/api/v3/milestones_spec.rb
+++ b/spec/requests/api/v3/milestones_spec.rb
@@ -1,7 +1,6 @@
require 'spec_helper'
-describe API::V3::Milestones, api: true do
- include ApiHelpers
+describe API::V3::Milestones do
let(:user) { create(:user) }
let!(:project) { create(:empty_project, namespace: user.namespace ) }
let!(:closed_milestone) { create(:closed_milestone, project: project) }
diff --git a/spec/requests/api/v3/notes_spec.rb b/spec/requests/api/v3/notes_spec.rb
index ddef2d5eb04..2bae4a60931 100644
--- a/spec/requests/api/v3/notes_spec.rb
+++ b/spec/requests/api/v3/notes_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
-describe API::V3::Notes, api: true do
- include ApiHelpers
-
+describe API::V3::Notes do
let(:user) { create(:user) }
let!(:project) { create(:empty_project, :public, namespace: user.namespace) }
let!(:issue) { create(:issue, project: project, author: user) }
diff --git a/spec/requests/api/v3/pipelines_spec.rb b/spec/requests/api/v3/pipelines_spec.rb
index 3786eb06932..e1d036ff365 100644
--- a/spec/requests/api/v3/pipelines_spec.rb
+++ b/spec/requests/api/v3/pipelines_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
-describe API::V3::Pipelines, api: true do
- include ApiHelpers
-
+describe API::V3::Pipelines do
let(:user) { create(:user) }
let(:non_member) { create(:user) }
let(:project) { create(:project, :repository, creator: user) }
diff --git a/spec/requests/api/v3/project_hooks_spec.rb b/spec/requests/api/v3/project_hooks_spec.rb
index a981119dc5a..a3a4c77d09d 100644
--- a/spec/requests/api/v3/project_hooks_spec.rb
+++ b/spec/requests/api/v3/project_hooks_spec.rb
@@ -1,7 +1,6 @@
require 'spec_helper'
-describe API::ProjectHooks, 'ProjectHooks', api: true do
- include ApiHelpers
+describe API::ProjectHooks, 'ProjectHooks' do
let(:user) { create(:user) }
let(:user3) { create(:user) }
let!(:project) { create(:project, creator_id: user.id, namespace: user.namespace) }
diff --git a/spec/requests/api/v3/project_snippets_spec.rb b/spec/requests/api/v3/project_snippets_spec.rb
index 957a3bf97ef..365e7365fda 100644
--- a/spec/requests/api/v3/project_snippets_spec.rb
+++ b/spec/requests/api/v3/project_snippets_spec.rb
@@ -1,8 +1,6 @@
require 'rails_helper'
-describe API::ProjectSnippets, api: true do
- include ApiHelpers
-
+describe API::ProjectSnippets do
let(:project) { create(:empty_project, :public) }
let(:user) { create(:user) }
let(:admin) { create(:admin) }
diff --git a/spec/requests/api/v3/projects_spec.rb b/spec/requests/api/v3/projects_spec.rb
index 40531fe7545..dc7c3d125b1 100644
--- a/spec/requests/api/v3/projects_spec.rb
+++ b/spec/requests/api/v3/projects_spec.rb
@@ -1,7 +1,6 @@
require 'spec_helper'
-describe API::V3::Projects, api: true do
- include ApiHelpers
+describe API::V3::Projects do
include Gitlab::CurrentSettings
let(:user) { create(:user) }
@@ -228,7 +227,7 @@ describe API::V3::Projects, api: true do
storage_size: 702,
repository_size: 123,
lfs_objects_size: 234,
- build_artifacts_size: 345,
+ build_artifacts_size: 345
}
project4.statistics.update!(attributes)
@@ -707,7 +706,7 @@ describe API::V3::Projects, api: true do
'name' => user.namespace.name,
'path' => user.namespace.path,
'kind' => user.namespace.kind,
- 'full_path' => user.namespace.full_path,
+ 'full_path' => user.namespace.full_path
})
end
diff --git a/spec/requests/api/v3/repositories_spec.rb b/spec/requests/api/v3/repositories_spec.rb
index fef6fb641fa..1a55e2a71cd 100644
--- a/spec/requests/api/v3/repositories_spec.rb
+++ b/spec/requests/api/v3/repositories_spec.rb
@@ -1,8 +1,7 @@
require 'spec_helper'
require 'mime/types'
-describe API::V3::Repositories, api: true do
- include ApiHelpers
+describe API::V3::Repositories do
include RepoHelpers
include WorkhorseHelpers
diff --git a/spec/requests/api/v3/runners_spec.rb b/spec/requests/api/v3/runners_spec.rb
index ca335ce9cf0..dbda2cf34c3 100644
--- a/spec/requests/api/v3/runners_spec.rb
+++ b/spec/requests/api/v3/runners_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
-describe API::V3::Runners, api: true do
- include ApiHelpers
-
+describe API::V3::Runners do
let(:admin) { create(:user, :admin) }
let(:user) { create(:user) }
let(:user2) { create(:user) }
diff --git a/spec/requests/api/v3/services_spec.rb b/spec/requests/api/v3/services_spec.rb
index 3a760a8f25c..3ba62de822a 100644
--- a/spec/requests/api/v3/services_spec.rb
+++ b/spec/requests/api/v3/services_spec.rb
@@ -1,8 +1,6 @@
require "spec_helper"
-describe API::V3::Services, api: true do
- include ApiHelpers
-
+describe API::V3::Services do
let(:user) { create(:user) }
let(:project) { create(:empty_project, creator_id: user.id, namespace: user.namespace) }
diff --git a/spec/requests/api/v3/settings_spec.rb b/spec/requests/api/v3/settings_spec.rb
index a9fa5adac17..41d039b7da0 100644
--- a/spec/requests/api/v3/settings_spec.rb
+++ b/spec/requests/api/v3/settings_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
-describe API::V3::Settings, 'Settings', api: true do
- include ApiHelpers
-
+describe API::V3::Settings, 'Settings' do
let(:user) { create(:user) }
let(:admin) { create(:admin) }
diff --git a/spec/requests/api/v3/snippets_spec.rb b/spec/requests/api/v3/snippets_spec.rb
index 05653bd0d51..4f02b7b1a54 100644
--- a/spec/requests/api/v3/snippets_spec.rb
+++ b/spec/requests/api/v3/snippets_spec.rb
@@ -1,7 +1,6 @@
require 'rails_helper'
-describe API::V3::Snippets, api: true do
- include ApiHelpers
+describe API::V3::Snippets do
let!(:user) { create(:user) }
describe 'GET /snippets/' do
diff --git a/spec/requests/api/v3/system_hooks_spec.rb b/spec/requests/api/v3/system_hooks_spec.rb
index 91038977c82..ae427541abb 100644
--- a/spec/requests/api/v3/system_hooks_spec.rb
+++ b/spec/requests/api/v3/system_hooks_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
-describe API::V3::SystemHooks, api: true do
- include ApiHelpers
-
+describe API::V3::SystemHooks do
let(:user) { create(:user) }
let(:admin) { create(:admin) }
let!(:hook) { create(:system_hook, url: "http://example.com") }
@@ -33,8 +31,9 @@ describe API::V3::SystemHooks, api: true do
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.first['url']).to eq(hook.url)
- expect(json_response.first['push_events']).to be true
+ expect(json_response.first['push_events']).to be false
expect(json_response.first['tag_push_events']).to be false
+ expect(json_response.first['repository_update_events']).to be true
end
end
end
diff --git a/spec/requests/api/v3/tags_spec.rb b/spec/requests/api/v3/tags_spec.rb
index 6870cfd2668..1c4b25c47c3 100644
--- a/spec/requests/api/v3/tags_spec.rb
+++ b/spec/requests/api/v3/tags_spec.rb
@@ -1,8 +1,7 @@
require 'spec_helper'
require 'mime/types'
-describe API::V3::Tags, api: true do
- include ApiHelpers
+describe API::V3::Tags do
include RepoHelpers
let(:user) { create(:user) }
diff --git a/spec/requests/api/v3/templates_spec.rb b/spec/requests/api/v3/templates_spec.rb
index f1e554b98cc..00446c7f29c 100644
--- a/spec/requests/api/v3/templates_spec.rb
+++ b/spec/requests/api/v3/templates_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
-describe API::V3::Templates, api: true do
- include ApiHelpers
-
+describe API::V3::Templates do
shared_examples_for 'the Template Entity' do |path|
before { get v3_api(path) }
diff --git a/spec/requests/api/v3/todos_spec.rb b/spec/requests/api/v3/todos_spec.rb
index 80fa697e949..9c2c4d64257 100644
--- a/spec/requests/api/v3/todos_spec.rb
+++ b/spec/requests/api/v3/todos_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
-describe API::V3::Todos, api: true do
- include ApiHelpers
-
+describe API::V3::Todos do
let(:project_1) { create(:empty_project) }
let(:project_2) { create(:empty_project) }
let(:author_1) { create(:user) }
diff --git a/spec/requests/api/v3/triggers_spec.rb b/spec/requests/api/v3/triggers_spec.rb
index 9233e9621bf..d3de6bf13bc 100644
--- a/spec/requests/api/v3/triggers_spec.rb
+++ b/spec/requests/api/v3/triggers_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
describe API::V3::Triggers do
- include ApiHelpers
-
let(:user) { create(:user) }
let(:user2) { create(:user) }
let!(:trigger_token) { 'secure_token' }
diff --git a/spec/requests/api/v3/users_spec.rb b/spec/requests/api/v3/users_spec.rb
index b38cbe74b85..e9c57f7c6c3 100644
--- a/spec/requests/api/v3/users_spec.rb
+++ b/spec/requests/api/v3/users_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
-describe API::V3::Users, api: true do
- include ApiHelpers
-
+describe API::V3::Users do
let(:user) { create(:user) }
let(:admin) { create(:admin) }
let(:key) { create(:key, user: user) }
@@ -276,5 +274,11 @@ describe API::V3::Users, api: true do
expect(new_user).to be_confirmed
end
+
+ it 'does not reveal the `is_admin` flag of the user' do
+ post v3_api('/users', admin), attributes_for(:user)
+
+ expect(json_response['is_admin']).to be_nil
+ end
end
end
diff --git a/spec/requests/api/variables_spec.rb b/spec/requests/api/variables_spec.rb
index 0c1413119e0..63d6d3001ac 100644
--- a/spec/requests/api/variables_spec.rb
+++ b/spec/requests/api/variables_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
-describe API::Variables, api: true do
- include ApiHelpers
-
+describe API::Variables do
let(:user) { create(:user) }
let(:user2) { create(:user) }
let!(:project) { create(:empty_project, creator_id: user.id) }
diff --git a/spec/requests/api/version_spec.rb b/spec/requests/api/version_spec.rb
index da1b2fda70e..8870d48bbc9 100644
--- a/spec/requests/api/version_spec.rb
+++ b/spec/requests/api/version_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
-describe API::Version, api: true do
- include ApiHelpers
-
+describe API::Version do
describe 'GET /version' do
context 'when unauthenticated' do
it 'returns authentication error' do
diff --git a/spec/requests/ci/api/builds_spec.rb b/spec/requests/ci/api/builds_spec.rb
index ef30d8638dd..286de277ae7 100644
--- a/spec/requests/ci/api/builds_spec.rb
+++ b/spec/requests/ci/api/builds_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
describe Ci::API::Builds do
- include ApiHelpers
-
let(:runner) { FactoryGirl.create(:ci_runner, tag_list: %w(mysql ruby)) }
let(:project) { FactoryGirl.create(:empty_project, shared_runners_enabled: false) }
let(:last_update) { nil }
@@ -187,7 +185,7 @@ describe Ci::API::Builds do
{ "key" => "CI_PIPELINE_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 },
+ { "key" => "TRIGGER_KEY_1", "value" => "TRIGGER_VALUE_1", "public" => false }
)
end
end
diff --git a/spec/requests/ci/api/runners_spec.rb b/spec/requests/ci/api/runners_spec.rb
index d50cdfdc2d6..0b9733221d8 100644
--- a/spec/requests/ci/api/runners_spec.rb
+++ b/spec/requests/ci/api/runners_spec.rb
@@ -1,7 +1,6 @@
require 'spec_helper'
describe Ci::API::Runners do
- include ApiHelpers
include StubGitlabCalls
let(:registration_token) { 'abcdefg123456' }
diff --git a/spec/requests/ci/api/triggers_spec.rb b/spec/requests/ci/api/triggers_spec.rb
index 5321f8b134f..26b03c0f148 100644
--- a/spec/requests/ci/api/triggers_spec.rb
+++ b/spec/requests/ci/api/triggers_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
describe Ci::API::Triggers do
- include ApiHelpers
-
describe 'POST /projects/:project_id/refs/:ref/trigger' do
let!(:trigger_token) { 'secure token' }
let!(:project) { create(:project, :repository, ci_id: 10) }
diff --git a/spec/requests/git_http_spec.rb b/spec/requests/git_http_spec.rb
index 316742ff076..6ca3ef18fe6 100644
--- a/spec/requests/git_http_spec.rb
+++ b/spec/requests/git_http_spec.rb
@@ -279,10 +279,10 @@ describe 'Git HTTP requests', lib: true do
expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
end
- it "uploads get status 401 (no project existence information leak)" do
+ it "uploads get status 200" do
push_get "#{project.path_with_namespace}.git", user: 'oauth2', password: @token.token
- expect(response).to have_http_status(401)
+ expect(response).to have_http_status(200)
end
end
diff --git a/spec/requests/lfs_http_spec.rb b/spec/requests/lfs_http_spec.rb
index 5d495bc9e7d..0c9b4121adf 100644
--- a/spec/requests/lfs_http_spec.rb
+++ b/spec/requests/lfs_http_spec.rb
@@ -425,7 +425,7 @@ describe 'Git LFS API and storage' do
'size' => sample_size,
'error' => {
'code' => 404,
- 'message' => "Object does not exist on the server or you don't have permissions to access it",
+ 'message' => "Object does not exist on the server or you don't have permissions to access it"
}
}
]
@@ -456,7 +456,7 @@ describe 'Git LFS API and storage' do
'size' => 1575078,
'error' => {
'code' => 404,
- 'message' => "Object does not exist on the server or you don't have permissions to access it",
+ 'message' => "Object does not exist on the server or you don't have permissions to access it"
}
}
]
@@ -493,7 +493,7 @@ describe 'Git LFS API and storage' do
'size' => 1575078,
'error' => {
'code' => 404,
- 'message' => "Object does not exist on the server or you don't have permissions to access it",
+ 'message' => "Object does not exist on the server or you don't have permissions to access it"
}
},
{
diff --git a/spec/requests/openid_connect_spec.rb b/spec/requests/openid_connect_spec.rb
index 5206634bca5..05176c3beaa 100644
--- a/spec/requests/openid_connect_spec.rb
+++ b/spec/requests/openid_connect_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
describe 'OpenID Connect requests' do
- include ApiHelpers
-
let(:user) { create :user }
let(:access_grant) { create :oauth_access_grant, application: application, resource_owner_id: user.id }
let(:access_token) { create :oauth_access_token, application: application, resource_owner_id: user.id }
@@ -63,7 +61,7 @@ describe 'OpenID Connect requests' do
email: private_email.email,
public_email: public_email.email,
website_url: 'https://example.com',
- avatar: fixture_file_upload(Rails.root + "spec/fixtures/dk.png"),
+ avatar: fixture_file_upload(Rails.root + "spec/fixtures/dk.png")
)
end
@@ -81,7 +79,7 @@ describe 'OpenID Connect requests' do
'email_verified' => true,
'website' => 'https://example.com',
'profile' => 'http://localhost/alice',
- 'picture' => "http://localhost/uploads/user/avatar/#{user.id}/dk.png",
+ 'picture' => "http://localhost/uploads/user/avatar/#{user.id}/dk.png"
})
end
end
@@ -100,7 +98,7 @@ describe 'OpenID Connect requests' do
expect(@payload['sub']).to eq hashed_subject
end
- it 'includes the time of the last authentication' do
+ it 'includes the time of the last authentication', :redis do
expect(@payload['auth_time']).to eq user.current_sign_in_at.to_i
end
diff --git a/spec/requests/projects/artifacts_controller_spec.rb b/spec/requests/projects/artifacts_controller_spec.rb
deleted file mode 100644
index d20866c0d44..00000000000
--- a/spec/requests/projects/artifacts_controller_spec.rb
+++ /dev/null
@@ -1,117 +0,0 @@
-require 'spec_helper'
-
-describe Projects::ArtifactsController do
- let(:user) { create(:user) }
- let(:project) { create(:project, :repository) }
-
- let(:pipeline) do
- create(:ci_pipeline,
- project: project,
- sha: project.commit.sha,
- ref: project.default_branch,
- status: 'success')
- end
-
- let(:build) { create(:ci_build, :success, :artifacts, pipeline: pipeline) }
-
- describe 'GET /:project/builds/artifacts/:ref_name/browse?job=name' do
- before do
- project.team << [user, :developer]
-
- login_as(user)
- end
-
- def path_from_ref(
- ref = pipeline.ref, job = build.name, path = 'browse')
- latest_succeeded_namespace_project_artifacts_path(
- project.namespace,
- project,
- [ref, path].join('/'),
- job: job)
- end
-
- context 'cannot find the 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_from_ref('TAIL', build.name)
- end
-
- it_behaves_like 'not found'
- end
-
- context 'has no such build' do
- before do
- get path_from_ref(pipeline.ref, 'NOBUILD')
- end
-
- it_behaves_like 'not found'
- end
-
- context 'has no path' do
- before do
- get path_from_ref(pipeline.sha, build.name, '')
- end
-
- it_behaves_like 'not found'
- end
- end
-
- context 'found the build and redirect' do
- shared_examples 'redirect to the build' do
- it 'redirects' do
- path = browse_namespace_project_build_artifacts_path(
- project.namespace,
- project,
- build)
-
- expect(response).to redirect_to(path)
- end
- end
-
- context 'with regular branch' do
- before do
- pipeline.update(ref: 'master',
- sha: project.commit('master').sha)
-
- get path_from_ref('master')
- end
-
- it_behaves_like 'redirect to the build'
- end
-
- context 'with branch name containing slash' do
- before do
- pipeline.update(ref: 'improve/awesome',
- sha: project.commit('improve/awesome').sha)
-
- get path_from_ref('improve/awesome')
- end
-
- it_behaves_like 'redirect to the build'
- end
-
- context 'with branch name and path containing slashes' do
- before do
- pipeline.update(ref: 'improve/awesome',
- sha: project.commit('improve/awesome').sha)
-
- get path_from_ref('improve/awesome', build.name, 'file/README.md')
- end
-
- it 'redirects' do
- path = file_namespace_project_build_artifacts_path(
- project.namespace,
- project,
- build,
- 'README.md')
-
- expect(response).to redirect_to(path)
- end
- end
- end
- end
-end
diff --git a/spec/requests/projects/cycle_analytics_events_spec.rb b/spec/requests/projects/cycle_analytics_events_spec.rb
index 0edbffbcd3b..d92daa345b3 100644
--- a/spec/requests/projects/cycle_analytics_events_spec.rb
+++ b/spec/requests/projects/cycle_analytics_events_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
-describe 'cycle analytics events' do
- include ApiHelpers
-
+describe 'cycle analytics events', api: true do
let(:user) { create(:user) }
let(:project) { create(:project, :repository, public_builds: false) }
let(:issue) { create(:issue, project: project, created_at: 2.days.ago) }
@@ -11,8 +9,6 @@ describe 'cycle analytics events' do
before do
project.team << [user, :developer]
- allow_any_instance_of(Gitlab::ReferenceExtractor).to receive(:issues).and_return([issue])
-
3.times do |count|
Timecop.freeze(Time.now + count.days) do
create_cycle
@@ -123,9 +119,10 @@ describe 'cycle analytics events' do
def create_cycle
milestone = create(:milestone, project: project)
issue.update(milestone: milestone)
- mr = create_merge_request_closing_issue(issue)
+ mr = create_merge_request_closing_issue(issue, commit_message: "References #{issue.to_reference}")
pipeline = create(:ci_empty_pipeline, status: 'created', project: project, ref: mr.source_branch, sha: mr.source_branch_sha)
+ mr.update(head_pipeline_id: pipeline.id)
pipeline.run
create(:ci_build, pipeline: pipeline, status: :success, author: user)
diff --git a/spec/requests/request_profiler_spec.rb b/spec/requests/request_profiler_spec.rb
new file mode 100644
index 00000000000..51fbfecec4b
--- /dev/null
+++ b/spec/requests/request_profiler_spec.rb
@@ -0,0 +1,44 @@
+require 'spec_helper'
+
+describe 'Request Profiler' do
+ let(:user) { create(:user) }
+
+ shared_examples 'profiling a request' do
+ before do
+ allow(Rails).to receive(:cache).and_return(ActiveSupport::Cache::MemoryStore.new)
+ allow(RubyProf::Profile).to receive(:profile) do |&blk|
+ blk.call
+ RubyProf::Profile.new
+ end
+ end
+
+ it 'creates a profile of the request' do
+ project = create(:project, namespace: user.namespace)
+ time = Time.now
+ path = "/#{project.path_with_namespace}"
+
+ Timecop.freeze(time) do
+ get path, nil, 'X-Profile-Token' => Gitlab::RequestProfiler.profile_token
+ end
+
+ profile_path = "#{Gitlab.config.shared.path}/tmp/requests_profiles/#{path.tr('/', '|')}_#{time.to_i}.html"
+ expect(File.exist?(profile_path)).to be true
+ end
+
+ after do
+ Gitlab::RequestProfiler.remove_all_profiles
+ end
+ end
+
+ context "when user is logged-in" do
+ before do
+ login_as(user)
+ end
+
+ include_examples 'profiling a request'
+ end
+
+ context "when user is not logged-in" do
+ include_examples 'profiling a request'
+ end
+end
diff --git a/spec/routing/admin_routing_spec.rb b/spec/routing/admin_routing_spec.rb
index 99c44bde151..e5fc0b676af 100644
--- a/spec/routing/admin_routing_spec.rb
+++ b/spec/routing/admin_routing_spec.rb
@@ -71,13 +71,15 @@ describe Admin::ProjectsController, "routing" do
end
end
-# admin_hook_test GET /admin/hooks/:hook_id/test(.:format) admin/hooks#test
+# admin_hook_test GET /admin/hooks/:id/test(.:format) admin/hooks#test
# admin_hooks GET /admin/hooks(.:format) admin/hooks#index
# POST /admin/hooks(.:format) admin/hooks#create
# admin_hook DELETE /admin/hooks/:id(.:format) admin/hooks#destroy
+# PUT /admin/hooks/:id(.:format) admin/hooks#update
+# edit_admin_hook GET /admin/hooks/:id(.:format) admin/hooks#edit
describe Admin::HooksController, "routing" do
it "to #test" do
- expect(get("/admin/hooks/1/test")).to route_to('admin/hooks#test', hook_id: '1')
+ expect(get("/admin/hooks/1/test")).to route_to('admin/hooks#test', id: '1')
end
it "to #index" do
@@ -88,6 +90,14 @@ describe Admin::HooksController, "routing" do
expect(post("/admin/hooks")).to route_to('admin/hooks#create')
end
+ it "to #edit" do
+ expect(get("/admin/hooks/1/edit")).to route_to('admin/hooks#edit', id: '1')
+ end
+
+ it "to #update" do
+ expect(put("/admin/hooks/1")).to route_to('admin/hooks#update', id: '1')
+ end
+
it "to #destroy" do
expect(delete("/admin/hooks/1")).to route_to('admin/hooks#destroy', id: '1')
end
diff --git a/spec/routing/environments_spec.rb b/spec/routing/environments_spec.rb
index ba124de70bb..624f3c43f0a 100644
--- a/spec/routing/environments_spec.rb
+++ b/spec/routing/environments_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Projects::EnvironmentsController, :routing do
+describe 'environments routing', :routing do
let(:project) { create(:empty_project) }
let(:environment) do
diff --git a/spec/routing/notifications_routing_spec.rb b/spec/routing/notifications_routing_spec.rb
index 24592942a96..54ed87b5520 100644
--- a/spec/routing/notifications_routing_spec.rb
+++ b/spec/routing/notifications_routing_spec.rb
@@ -1,13 +1,11 @@
require "spec_helper"
-describe Profiles::NotificationsController do
- describe "routing" do
- it "routes to #show" do
- expect(get("/profile/notifications")).to route_to("profiles/notifications#show")
- end
+describe "notifications routing" do
+ it "routes to #show" do
+ expect(get("/profile/notifications")).to route_to("profiles/notifications#show")
+ end
- it "routes to #update" do
- expect(put("/profile/notifications")).to route_to("profiles/notifications#update")
- end
+ it "routes to #update" do
+ expect(put("/profile/notifications")).to route_to("profiles/notifications#update")
end
end
diff --git a/spec/routing/project_routing_spec.rb b/spec/routing/project_routing_spec.rb
index a3de022d242..50e96d56191 100644
--- a/spec/routing/project_routing_spec.rb
+++ b/spec/routing/project_routing_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
describe 'project routing' do
before do
allow(Project).to receive(:find_by_full_path).and_return(false)
- allow(Project).to receive(:find_by_full_path).with('gitlab/gitlabhq').and_return(true)
+ allow(Project).to receive(:find_by_full_path).with('gitlab/gitlabhq', any_args).and_return(true)
end
# Shared examples for a resource inside a Project
@@ -93,13 +93,13 @@ describe 'project routing' do
end
context 'name with dot' do
- before { allow(Project).to receive(:find_by_full_path).with('gitlab/gitlabhq.keys').and_return(true) }
+ before { allow(Project).to receive(:find_by_full_path).with('gitlab/gitlabhq.keys', any_args).and_return(true) }
it { expect(get('/gitlab/gitlabhq.keys')).to route_to('projects#show', namespace_id: 'gitlab', id: 'gitlabhq.keys') }
end
context 'with nested group' do
- before { allow(Project).to receive(:find_by_full_path).with('gitlab/subgroup/gitlabhq').and_return(true) }
+ before { allow(Project).to receive(:find_by_full_path).with('gitlab/subgroup/gitlabhq', any_args).and_return(true) }
it { expect(get('/gitlab/subgroup/gitlabhq')).to route_to('projects#show', namespace_id: 'gitlab/subgroup', id: 'gitlabhq') }
end
@@ -340,14 +340,16 @@ describe 'project routing' do
# test_project_hook GET /:project_id/hooks/:id/test(.:format) hooks#test
# project_hooks GET /:project_id/hooks(.:format) hooks#index
# POST /:project_id/hooks(.:format) hooks#create
- # project_hook DELETE /:project_id/hooks/:id(.:format) hooks#destroy
+ # edit_project_hook GET /:project_id/hooks/:id/edit(.:format) hooks#edit
+ # project_hook PUT /:project_id/hooks/:id(.:format) hooks#update
+ # DELETE /:project_id/hooks/:id(.:format) hooks#destroy
describe Projects::HooksController, 'routing' do
it 'to #test' do
expect(get('/gitlab/gitlabhq/hooks/1/test')).to route_to('projects/hooks#test', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1')
end
it_behaves_like 'RESTful project resources' do
- let(:actions) { [:index, :create, :destroy] }
+ let(:actions) { [:index, :create, :destroy, :edit, :update] }
let(:controller) { 'hooks' }
end
end
diff --git a/spec/rubocop/cop/migration/add_column_with_default_to_large_table_spec.rb b/spec/rubocop/cop/migration/add_column_with_default_to_large_table_spec.rb
new file mode 100644
index 00000000000..07cb3fc4a2e
--- /dev/null
+++ b/spec/rubocop/cop/migration/add_column_with_default_to_large_table_spec.rb
@@ -0,0 +1,44 @@
+require 'spec_helper'
+
+require 'rubocop'
+require 'rubocop/rspec/support'
+
+require_relative '../../../../rubocop/cop/migration/add_column_with_default_to_large_table'
+
+describe RuboCop::Cop::Migration::AddColumnWithDefaultToLargeTable do
+ include CopHelper
+
+ subject(:cop) { described_class.new }
+
+ context 'in migration' do
+ before do
+ allow(cop).to receive(:in_migration?).and_return(true)
+ end
+
+ described_class::LARGE_TABLES.each do |table|
+ it "registers an offense for the #{table} table" do
+ inspect_source(cop, "add_column_with_default :#{table}, :column, default: true")
+
+ aggregate_failures do
+ expect(cop.offenses.size).to eq(1)
+ expect(cop.offenses.map(&:line)).to eq([1])
+ end
+ end
+ end
+
+ it 'registers no offense for non-blacklisted tables' do
+ inspect_source(cop, "add_column_with_default :table, :column, default: true")
+
+ expect(cop.offenses).to be_empty
+ end
+ end
+
+ context 'outside of migration' do
+ it 'registers no offense' do
+ table = described_class::LARGE_TABLES.sample
+ inspect_source(cop, "add_column_with_default :#{table}, :column, default: true")
+
+ expect(cop.offenses).to be_empty
+ end
+ end
+end
diff --git a/spec/rubocop/cop/migration/add_column_with_default_spec.rb b/spec/rubocop/cop/migration/reversible_add_column_with_default_spec.rb
index 6b9b6b19650..3723d635083 100644
--- a/spec/rubocop/cop/migration/add_column_with_default_spec.rb
+++ b/spec/rubocop/cop/migration/reversible_add_column_with_default_spec.rb
@@ -3,9 +3,9 @@ require 'spec_helper'
require 'rubocop'
require 'rubocop/rspec/support'
-require_relative '../../../../rubocop/cop/migration/add_column_with_default'
+require_relative '../../../../rubocop/cop/migration/reversible_add_column_with_default'
-describe RuboCop::Cop::Migration::AddColumnWithDefault do
+describe RuboCop::Cop::Migration::ReversibleAddColumnWithDefault do
include CopHelper
subject(:cop) { described_class.new }
diff --git a/spec/serializers/analytics_generic_entity_spec.rb b/spec/serializers/analytics_issue_entity_spec.rb
index 68086216ba9..75d606d5eb3 100644
--- a/spec/serializers/analytics_generic_entity_spec.rb
+++ b/spec/serializers/analytics_issue_entity_spec.rb
@@ -9,7 +9,7 @@ describe AnalyticsIssueEntity do
iid: "1",
id: "1",
created_at: "2016-11-12 15:04:02.948604",
- author: user,
+ author: user
}
end
diff --git a/spec/serializers/analytics_issue_serializer_spec.rb b/spec/serializers/analytics_issue_serializer_spec.rb
index ba24cf8e481..7c14c198a74 100644
--- a/spec/serializers/analytics_issue_serializer_spec.rb
+++ b/spec/serializers/analytics_issue_serializer_spec.rb
@@ -16,7 +16,7 @@ describe AnalyticsIssueSerializer do
iid: "1",
id: "1",
created_at: "2016-11-12 15:04:02.948604",
- author: user,
+ author: user
}
end
diff --git a/spec/serializers/build_action_entity_spec.rb b/spec/serializers/build_action_entity_spec.rb
index 54ac17447b1..059deba5416 100644
--- a/spec/serializers/build_action_entity_spec.rb
+++ b/spec/serializers/build_action_entity_spec.rb
@@ -2,9 +2,10 @@ require 'spec_helper'
describe BuildActionEntity do
let(:build) { create(:ci_build, name: 'test_build') }
+ let(:request) { double('request') }
let(:entity) do
- described_class.new(build, request: double)
+ described_class.new(build, request: spy('request'))
end
describe '#as_json' do
diff --git a/spec/serializers/build_entity_spec.rb b/spec/serializers/build_entity_spec.rb
index f76a5cf72d1..b5eb84ae43b 100644
--- a/spec/serializers/build_entity_spec.rb
+++ b/spec/serializers/build_entity_spec.rb
@@ -6,7 +6,7 @@ describe BuildEntity do
let(:request) { double('request') }
before do
- allow(request).to receive(:user).and_return(user)
+ allow(request).to receive(:current_user).and_return(user)
end
let(:entity) do
@@ -41,13 +41,37 @@ describe BuildEntity do
it 'does not contain path to play action' do
expect(subject).not_to include(:play_path)
end
+
+ it 'is not a playable job' do
+ expect(subject[:playable]).to be false
+ end
end
context 'when build is a manual action' do
let(:build) { create(:ci_build, :manual) }
- it 'contains path to play action' do
- expect(subject).to include(:play_path)
+ context 'when user is allowed to trigger action' do
+ before do
+ build.project.add_master(user)
+ end
+
+ it 'contains path to play action' do
+ expect(subject).to include(:play_path)
+ end
+
+ it 'is a playable action' do
+ expect(subject[:playable]).to be true
+ end
+ end
+
+ context 'when user is not allowed to trigger action' do
+ it 'does not contain path to play action' do
+ expect(subject).not_to include(:play_path)
+ end
+
+ it 'is not a playable action' do
+ expect(subject[:playable]).to be false
+ end
end
end
end
diff --git a/spec/serializers/build_serializer_spec.rb b/spec/serializers/build_serializer_spec.rb
index 7f1abecfafe..01e2cfed6f8 100644
--- a/spec/serializers/build_serializer_spec.rb
+++ b/spec/serializers/build_serializer_spec.rb
@@ -4,7 +4,7 @@ describe BuildSerializer do
let(:user) { create(:user) }
let(:serializer) do
- described_class.new(user: user)
+ described_class.new(current_user: user)
end
subject { serializer.represent(resource) }
diff --git a/spec/serializers/deploy_key_entity_spec.rb b/spec/serializers/deploy_key_entity_spec.rb
new file mode 100644
index 00000000000..e73fbe190ca
--- /dev/null
+++ b/spec/serializers/deploy_key_entity_spec.rb
@@ -0,0 +1,38 @@
+require 'spec_helper'
+
+describe DeployKeyEntity do
+ include RequestAwareEntity
+
+ let(:user) { create(:user) }
+ let(:project) { create(:empty_project, :internal)}
+ let(:project_private) { create(:empty_project, :private)}
+ let(:deploy_key) { create(:deploy_key) }
+ let!(:deploy_key_internal) { create(:deploy_keys_project, project: project, deploy_key: deploy_key) }
+ let!(:deploy_key_private) { create(:deploy_keys_project, project: project_private, deploy_key: deploy_key) }
+
+ let(:entity) { described_class.new(deploy_key, user: user) }
+
+ it 'returns deploy keys with projects a user can read' do
+ expected_result = {
+ id: deploy_key.id,
+ user_id: deploy_key.user_id,
+ title: deploy_key.title,
+ fingerprint: deploy_key.fingerprint,
+ can_push: deploy_key.can_push,
+ destroyed_when_orphaned: true,
+ almost_orphaned: false,
+ created_at: deploy_key.created_at,
+ updated_at: deploy_key.updated_at,
+ projects: [
+ {
+ id: project.id,
+ name: project.name,
+ full_path: namespace_project_path(project.namespace, project),
+ full_name: project.full_name
+ }
+ ]
+ }
+
+ expect(entity.as_json).to eq(expected_result)
+ end
+end
diff --git a/spec/serializers/deployment_entity_spec.rb b/spec/serializers/deployment_entity_spec.rb
index 95eca5463eb..522c92ce295 100644
--- a/spec/serializers/deployment_entity_spec.rb
+++ b/spec/serializers/deployment_entity_spec.rb
@@ -3,25 +3,23 @@ require 'spec_helper'
describe DeploymentEntity do
let(:user) { create(:user) }
let(:request) { double('request') }
+ let(:deployment) { create(:deployment) }
+ let(:entity) { described_class.new(deployment, request: request) }
+ subject { entity.as_json }
before do
- allow(request).to receive(:user).and_return(user)
+ allow(request).to receive(:current_user).and_return(user)
end
- let(:entity) do
- described_class.new(deployment, request: request)
- end
-
- let(:deployment) { create(:deployment) }
-
- subject { entity.as_json }
-
it 'exposes internal deployment id' do
expect(subject).to include(:iid)
end
it 'exposes nested information about branch' do
expect(subject[:ref][:name]).to eq 'master'
- expect(subject[:ref][:ref_path]).not_to be_empty
+ end
+
+ it 'exposes creation date' do
+ expect(subject).to include(:created_at)
end
end
diff --git a/spec/serializers/environment_serializer_spec.rb b/spec/serializers/environment_serializer_spec.rb
index 1909e6385b5..d2ad6c44702 100644
--- a/spec/serializers/environment_serializer_spec.rb
+++ b/spec/serializers/environment_serializer_spec.rb
@@ -6,7 +6,7 @@ describe EnvironmentSerializer do
let(:json) do
described_class
- .new(user: user, project: project)
+ .new(current_user: user, project: project)
.represent(resource)
end
diff --git a/spec/serializers/event_entity_spec.rb b/spec/serializers/event_entity_spec.rb
new file mode 100644
index 00000000000..bb54597c967
--- /dev/null
+++ b/spec/serializers/event_entity_spec.rb
@@ -0,0 +1,13 @@
+require 'spec_helper'
+
+describe EventEntity do
+ subject { described_class.represent(create(:event)).as_json }
+
+ it 'exposes author' do
+ expect(subject).to include(:author)
+ end
+
+ it 'exposes core elements of event' do
+ expect(subject).to include(:updated_at)
+ end
+end
diff --git a/spec/serializers/label_serializer_spec.rb b/spec/serializers/label_serializer_spec.rb
new file mode 100644
index 00000000000..c58c7da1f9e
--- /dev/null
+++ b/spec/serializers/label_serializer_spec.rb
@@ -0,0 +1,46 @@
+require 'spec_helper'
+
+describe LabelSerializer do
+ let(:user) { create(:user) }
+
+ let(:serializer) do
+ described_class.new(user: user)
+ end
+
+ subject { serializer.represent(resource) }
+
+ describe '#represent' do
+ context 'when a single object is being serialized' do
+ let(:resource) { create(:label) }
+
+ it 'serializes the label object' do
+ expect(subject[:id]).to eq resource.id
+ end
+ end
+
+ context 'when multiple objects are being serialized' do
+ let(:num_labels) { 2 }
+ let(:resource) { create_list(:label, num_labels) }
+
+ it 'serializes the array of labels' do
+ expect(subject.size).to eq(num_labels)
+ end
+ end
+ end
+
+ describe '#represent_appearance' do
+ context 'when represents only appearance' do
+ let(:resource) { create(:label) }
+
+ subject { serializer.represent_appearance(resource) }
+
+ it 'serializes only attributes used for appearance' do
+ expect(subject.keys).to eq([:id, :title, :color, :text_color])
+ expect(subject[:id]).to eq(resource.id)
+ expect(subject[:title]).to eq(resource.title)
+ expect(subject[:color]).to eq(resource.color)
+ expect(subject[:text_color]).to eq(resource.text_color)
+ end
+ end
+ end
+end
diff --git a/spec/serializers/merge_request_basic_serializer_spec.rb b/spec/serializers/merge_request_basic_serializer_spec.rb
new file mode 100644
index 00000000000..4daf5a59d0c
--- /dev/null
+++ b/spec/serializers/merge_request_basic_serializer_spec.rb
@@ -0,0 +1,12 @@
+require 'spec_helper'
+
+describe MergeRequestBasicSerializer do
+ let(:resource) { create(:merge_request) }
+ let(:user) { create(:user) }
+
+ subject { described_class.new.represent(resource) }
+
+ it 'has important MergeRequest attributes' do
+ expect(subject).to include(:merge_status)
+ end
+end
diff --git a/spec/serializers/merge_request_entity_spec.rb b/spec/serializers/merge_request_entity_spec.rb
new file mode 100644
index 00000000000..bb6e83ae4bd
--- /dev/null
+++ b/spec/serializers/merge_request_entity_spec.rb
@@ -0,0 +1,128 @@
+require 'spec_helper'
+
+describe MergeRequestEntity do
+ let(:project) { create :empty_project }
+ let(:resource) { create(:merge_request, source_project: project, target_project: project) }
+ let(:user) { create(:user) }
+
+ let(:request) { double('request', current_user: user) }
+
+ subject do
+ described_class.new(resource, request: request).as_json
+ end
+
+ it 'includes author' do
+ req = double('request')
+
+ author_payload = UserEntity
+ .represent(resource.author, request: req)
+ .as_json
+
+ expect(subject[:author]).to eq(author_payload)
+ end
+
+ it 'includes pipeline' do
+ req = double('request', current_user: user)
+ pipeline = build_stubbed(:ci_pipeline)
+ allow(resource).to receive(:head_pipeline).and_return(pipeline)
+
+ pipeline_payload = PipelineEntity
+ .represent(pipeline, request: req)
+ .as_json
+
+ expect(subject[:pipeline]).to eq(pipeline_payload)
+ end
+
+ it 'includes issues_links' do
+ issues_links = subject[:issues_links]
+
+ expect(issues_links).to include(:closing, :mentioned_but_not_closing,
+ :assign_to_closing)
+ end
+
+ it 'has important MergeRequest attributes' do
+ expect(subject).to include(:diff_head_sha, :merge_commit_message,
+ :has_conflicts, :has_ci, :merge_path,
+ :conflict_resolution_path,
+ :cancel_merge_when_pipeline_succeeds_path,
+ :create_issue_to_resolve_discussions_path,
+ :source_branch_path, :target_branch_commits_path,
+ :commits_count)
+ end
+
+ it 'has email_patches_path' do
+ expect(subject[:email_patches_path])
+ .to eq("/#{resource.project.full_path}/merge_requests/#{resource.iid}.patch")
+ end
+
+ it 'has plain_diff_path' do
+ expect(subject[:plain_diff_path])
+ .to eq("/#{resource.project.full_path}/merge_requests/#{resource.iid}.diff")
+ end
+
+ it 'has merge_commit_message_with_description' do
+ expect(subject[:merge_commit_message_with_description])
+ .to eq(resource.merge_commit_message(include_description: true))
+ end
+
+ describe 'diff_head_sha' do
+ before do
+ allow(resource).to receive(:diff_head_sha) { 'sha' }
+ end
+
+ context 'when no diff head commit' do
+ it 'returns nil' do
+ allow(resource).to receive(:diff_head_commit) { nil }
+
+ expect(subject[:diff_head_sha]).to be_nil
+ end
+ end
+
+ context 'when diff head commit present' do
+ it 'returns diff head commit short id' do
+ allow(resource).to receive(:diff_head_commit) { double }
+
+ expect(subject[:diff_head_sha]).to eq('sha')
+ end
+ end
+ end
+
+ it 'includes merge_event' do
+ create(:event, :merged, author: user, project: resource.project, target: resource)
+
+ expect(subject[:merge_event]).to include(:author, :updated_at)
+ end
+
+ it 'includes closed_event' do
+ create(:event, :closed, author: user, project: resource.project, target: resource)
+
+ expect(subject[:closed_event]).to include(:author, :updated_at)
+ end
+
+ describe 'diverged_commits_count' do
+ context 'when MR open and its diverging' do
+ it 'returns diverged commits count' do
+ allow(resource).to receive_messages(open?: true, diverged_from_target_branch?: true,
+ diverged_commits_count: 10)
+
+ expect(subject[:diverged_commits_count]).to eq(10)
+ end
+ end
+
+ context 'when MR is not open' do
+ it 'returns 0' do
+ allow(resource).to receive_messages(open?: false)
+
+ expect(subject[:diverged_commits_count]).to be_zero
+ end
+ end
+
+ context 'when MR is not diverging' do
+ it 'returns 0' do
+ allow(resource).to receive_messages(open?: true, diverged_from_target_branch?: false)
+
+ expect(subject[:diverged_commits_count]).to be_zero
+ end
+ end
+ end
+end
diff --git a/spec/serializers/merge_request_serializer_spec.rb b/spec/serializers/merge_request_serializer_spec.rb
new file mode 100644
index 00000000000..73fbecc153d
--- /dev/null
+++ b/spec/serializers/merge_request_serializer_spec.rb
@@ -0,0 +1,37 @@
+require 'spec_helper'
+
+describe MergeRequestSerializer do
+ let(:user) { build_stubbed(:user) }
+ let(:merge_request) { build_stubbed(:merge_request) }
+
+ let(:serializer) do
+ described_class.new(current_user: user)
+ end
+
+ describe '#represent' do
+ let(:opts) { { basic: basic } }
+ subject { serializer.represent(merge_request, basic: basic) }
+
+ context 'when basic param is truthy' do
+ let(:basic) { true }
+
+ it 'calls super class #represent with correct params' do
+ expect_any_instance_of(BaseSerializer).to receive(:represent)
+ .with(merge_request, opts, MergeRequestBasicEntity)
+
+ subject
+ end
+ end
+
+ context 'when basic param is falsy' do
+ let(:basic) { false }
+
+ it 'calls super class #represent with correct params' do
+ expect_any_instance_of(BaseSerializer).to receive(:represent)
+ .with(merge_request, opts, MergeRequestEntity)
+
+ subject
+ end
+ end
+ end
+end
diff --git a/spec/serializers/pipeline_entity_spec.rb b/spec/serializers/pipeline_entity_spec.rb
index 93d5a21419d..d2482ac434b 100644
--- a/spec/serializers/pipeline_entity_spec.rb
+++ b/spec/serializers/pipeline_entity_spec.rb
@@ -5,7 +5,7 @@ describe PipelineEntity do
let(:request) { double('request') }
before do
- allow(request).to receive(:user).and_return(user)
+ allow(request).to receive(:current_user).and_return(user)
end
let(:entity) do
@@ -19,7 +19,7 @@ describe PipelineEntity do
let(:pipeline) { create(:ci_empty_pipeline) }
it 'contains required fields' do
- expect(subject).to include :id, :user, :path
+ expect(subject).to include :id, :user, :path, :coverage
expect(subject).to include :ref, :commit
expect(subject).to include :updated_at, :created_at
end
diff --git a/spec/serializers/pipeline_serializer_spec.rb b/spec/serializers/pipeline_serializer_spec.rb
index ecde45a6d44..f2426db6d81 100644
--- a/spec/serializers/pipeline_serializer_spec.rb
+++ b/spec/serializers/pipeline_serializer_spec.rb
@@ -4,7 +4,7 @@ describe PipelineSerializer do
let(:user) { create(:user) }
let(:serializer) do
- described_class.new(user: user)
+ described_class.new(current_user: user)
end
subject { serializer.represent(resource) }
@@ -44,7 +44,7 @@ describe PipelineSerializer do
end
let(:serializer) do
- described_class.new(user: user)
+ described_class.new(current_user: user)
.with_pagination(request, response)
end
@@ -113,7 +113,7 @@ describe PipelineSerializer do
it "verifies number of queries" do
recorded = ActiveRecord::QueryRecorder.new { subject }
- expect(recorded.count).to be_within(1).of(50)
+ expect(recorded.count).to be_within(1).of(58)
expect(recorded.cached_count).to eq(0)
end
diff --git a/spec/serializers/stage_entity_spec.rb b/spec/serializers/stage_entity_spec.rb
index 4ab40d08432..64b3217b809 100644
--- a/spec/serializers/stage_entity_spec.rb
+++ b/spec/serializers/stage_entity_spec.rb
@@ -14,7 +14,7 @@ describe StageEntity do
end
before do
- allow(request).to receive(:user).and_return(user)
+ allow(request).to receive(:current_user).and_return(user)
create(:ci_build, :success, pipeline: pipeline)
end
@@ -47,5 +47,13 @@ describe StageEntity do
it 'contains stage title' do
expect(subject[:title]).to eq 'test: passed'
end
+
+ context 'when the jobs should be grouped' do
+ let(:entity) { described_class.new(stage, request: request, grouped: true) }
+
+ it 'exposes the group key' do
+ expect(subject).to include :groups
+ end
+ end
end
end
diff --git a/spec/serializers/status_entity_spec.rb b/spec/serializers/status_entity_spec.rb
index c94902dbab8..3964b998084 100644
--- a/spec/serializers/status_entity_spec.rb
+++ b/spec/serializers/status_entity_spec.rb
@@ -18,6 +18,12 @@ describe StatusEntity do
it 'contains status details' do
expect(subject).to include :text, :icon, :favicon, :label, :group
expect(subject).to include :has_details, :details_path
+ expect(subject[:favicon]).to eq('/assets/ci_favicons/favicon_status_success.ico')
+ end
+
+ it 'contains a dev namespaced favicon if dev env' do
+ allow(Rails.env).to receive(:development?) { true }
+ expect(entity.as_json[:favicon]).to eq('/assets/ci_favicons/dev/favicon_status_success.ico')
end
end
end
diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb
index fa5014cee07..1ff1438ba06 100644
--- a/spec/services/ci/create_pipeline_service_spec.rb
+++ b/spec/services/ci/create_pipeline_service_spec.rb
@@ -34,6 +34,42 @@ describe Ci::CreatePipelineService, services: true do
it { expect(pipeline).to have_attributes(status: 'pending') }
it { expect(pipeline.builds.first).to be_kind_of(Ci::Build) }
+ context '#update_merge_requests_head_pipeline' do
+ it 'updates head pipeline of each merge request' do
+ merge_request_1 = create(:merge_request, source_branch: 'master', target_branch: "branch_1", source_project: project)
+ merge_request_2 = create(:merge_request, source_branch: 'master', target_branch: "branch_2", source_project: project)
+
+ head_pipeline = pipeline
+
+ expect(merge_request_1.reload.head_pipeline).to eq(head_pipeline)
+ expect(merge_request_2.reload.head_pipeline).to eq(head_pipeline)
+ end
+
+ context 'when there is no pipeline for source branch' do
+ it "does not update merge request head pipeline" do
+ merge_request = create(:merge_request, source_branch: 'other_branch', target_branch: "branch_1", source_project: project)
+
+ head_pipeline = pipeline
+
+ expect(merge_request.reload.head_pipeline).not_to eq(head_pipeline)
+ end
+ end
+
+ context 'when merge request target project is different from source project' do
+ let!(:target_project) { create(:empty_project) }
+ let!(:forked_project_link) { create(:forked_project_link, forked_to_project: project, forked_from_project: target_project) }
+
+ it 'updates head pipeline for merge request' do
+ merge_request =
+ create(:merge_request, source_branch: 'master', target_branch: "branch_1", source_project: project, target_project: target_project)
+
+ head_pipeline = pipeline
+
+ expect(merge_request.reload.head_pipeline).to eq(head_pipeline)
+ end
+ end
+ end
+
context 'auto-cancel enabled' do
before do
project.update(auto_cancel_pending_pipelines: 'enabled')
diff --git a/spec/services/ci/expire_pipeline_cache_service_spec.rb b/spec/services/ci/expire_pipeline_cache_service_spec.rb
deleted file mode 100644
index 166c6dfc93e..00000000000
--- a/spec/services/ci/expire_pipeline_cache_service_spec.rb
+++ /dev/null
@@ -1,27 +0,0 @@
-require 'spec_helper'
-
-describe Ci::ExpirePipelineCacheService, services: true do
- let(:user) { create(:user) }
- let(:project) { create(:empty_project) }
- let(:pipeline) { create(:ci_pipeline, project: project) }
- subject { described_class.new(project, user) }
-
- describe '#execute' do
- it 'invalidate Etag caching for project pipelines path' do
- pipelines_path = "/#{project.full_path}/pipelines.json"
- new_mr_pipelines_path = "/#{project.full_path}/merge_requests/new.json"
-
- expect_any_instance_of(Gitlab::EtagCaching::Store).to receive(:touch).with(pipelines_path)
- expect_any_instance_of(Gitlab::EtagCaching::Store).to receive(:touch).with(new_mr_pipelines_path)
-
- subject.execute(pipeline)
- end
-
- it 'updates the cached status for a project' do
- expect(Gitlab::Cache::Ci::ProjectPipelineStatus).to receive(:update_for_pipeline).
- with(pipeline)
-
- subject.execute(pipeline)
- end
- end
-end
diff --git a/spec/services/ci/play_build_service_spec.rb b/spec/services/ci/play_build_service_spec.rb
new file mode 100644
index 00000000000..d6f9fa42045
--- /dev/null
+++ b/spec/services/ci/play_build_service_spec.rb
@@ -0,0 +1,105 @@
+require 'spec_helper'
+
+describe Ci::PlayBuildService, '#execute', :services do
+ let(:user) { create(:user) }
+ let(:project) { create(:empty_project) }
+ let(:pipeline) { create(:ci_pipeline, project: project) }
+ let(:build) { create(:ci_build, :manual, pipeline: pipeline) }
+
+ let(:service) do
+ described_class.new(project, user)
+ end
+
+ context 'when project does not have repository yet' do
+ let(:project) { create(:empty_project) }
+
+ it 'allows user with master role to play build' do
+ project.add_master(user)
+
+ service.execute(build)
+
+ expect(build.reload).to be_pending
+ end
+
+ it 'does not allow user with developer role to play build' do
+ project.add_developer(user)
+
+ expect { service.execute(build) }
+ .to raise_error Gitlab::Access::AccessDeniedError
+ end
+ end
+
+ context 'when project has repository' do
+ let(:project) { create(:project) }
+
+ it 'allows user with developer role to play a build' do
+ project.add_developer(user)
+
+ service.execute(build)
+
+ expect(build.reload).to be_pending
+ end
+ end
+
+ context 'when build is a playable manual action' do
+ let(:build) { create(:ci_build, :manual, pipeline: pipeline) }
+
+ before do
+ project.add_master(user)
+ end
+
+ it 'enqueues the build' do
+ expect(service.execute(build)).to eq build
+ expect(build.reload).to be_pending
+ end
+
+ it 'reassignes build user correctly' do
+ service.execute(build)
+
+ expect(build.reload.user).to eq user
+ end
+ end
+
+ context 'when build is not a playable manual action' do
+ let(:build) { create(:ci_build, when: :manual, pipeline: pipeline) }
+
+ before do
+ project.add_master(user)
+ end
+
+ it 'duplicates the build' do
+ duplicate = service.execute(build)
+
+ expect(duplicate).not_to eq build
+ expect(duplicate).to be_pending
+ end
+
+ it 'assigns users correctly' do
+ duplicate = service.execute(build)
+
+ expect(build.user).not_to eq user
+ expect(duplicate.user).to eq user
+ end
+ end
+
+ context 'when build is not action' do
+ let(:build) { create(:ci_build, :success, pipeline: pipeline) }
+
+ it 'raises an error' do
+ expect { service.execute(build) }
+ .to raise_error Gitlab::Access::AccessDeniedError
+ end
+ end
+
+ context 'when user does not have ability to trigger action' do
+ before do
+ create(:protected_branch, :no_one_can_push,
+ name: build.ref, project: project)
+ end
+
+ it 'raises an error' do
+ expect { service.execute(build) }
+ .to raise_error Gitlab::Access::AccessDeniedError
+ end
+ end
+end
diff --git a/spec/services/ci/process_pipeline_service_spec.rb b/spec/services/ci/process_pipeline_service_spec.rb
index 245e19822f3..fc5de5d069a 100644
--- a/spec/services/ci/process_pipeline_service_spec.rb
+++ b/spec/services/ci/process_pipeline_service_spec.rb
@@ -268,6 +268,24 @@ describe Ci::ProcessPipelineService, '#execute', :services do
end
end
+ context 'when there are only manual actions in stages' do
+ before do
+ create_build('image', stage_idx: 0, when: 'manual', allow_failure: true)
+ create_build('build', stage_idx: 1, when: 'manual', allow_failure: true)
+ create_build('deploy', stage_idx: 2, when: 'manual')
+ create_build('check', stage_idx: 3)
+
+ process_pipeline
+ end
+
+ it 'processes all jobs until blocking actions encountered' do
+ expect(all_builds_statuses).to eq(%w[manual manual manual created])
+ expect(all_builds_names).to eq(%w[image build deploy check])
+
+ expect(pipeline.reload).to be_blocked
+ end
+ end
+
context 'when blocking manual actions are defined' do
before do
create_build('code:test', stage_idx: 0)
@@ -314,6 +332,13 @@ describe Ci::ProcessPipelineService, '#execute', :services do
end
context 'when pipeline is promoted sequentially up to the end' do
+ before do
+ # We are using create(:empty_project), and users has to be master in
+ # order to execute manual action when repository does not exist.
+ #
+ project.add_master(user)
+ end
+
it 'properly processes entire pipeline' do
process_pipeline
@@ -418,6 +443,21 @@ describe Ci::ProcessPipelineService, '#execute', :services do
end
end
+ context 'updates a list of retried builds' do
+ subject { described_class.retried.order(:id) }
+
+ let!(:build_retried) { create_build('build') }
+ let!(:build) { create_build('build') }
+ let!(:test) { create_build('test') }
+
+ it 'returns unique statuses' do
+ process_pipeline
+
+ expect(all_builds.latest).to contain_exactly(build, test)
+ expect(all_builds.retried).to contain_exactly(build_retried)
+ end
+ end
+
def process_pipeline
described_class.new(pipeline.project, user).execute(pipeline)
end
@@ -434,6 +474,10 @@ describe Ci::ProcessPipelineService, '#execute', :services do
builds.pluck(:name)
end
+ def all_builds_names
+ all_builds.pluck(:name)
+ end
+
def builds_statuses
builds.pluck(:status)
end
diff --git a/spec/services/ci/retry_build_service_spec.rb b/spec/services/ci/retry_build_service_spec.rb
index b2d37657770..7254e6b357a 100644
--- a/spec/services/ci/retry_build_service_spec.rb
+++ b/spec/services/ci/retry_build_service_spec.rb
@@ -22,7 +22,7 @@ describe Ci::RetryBuildService, :services do
%i[type lock_version target_url base_tags
commit_id deployments erased_by_id last_deployment project_id
runner_id tag_taggings taggings tags trigger_request_id
- user_id auto_canceled_by_id].freeze
+ user_id auto_canceled_by_id retried].freeze
shared_examples 'build duplication' do
let(:build) do
@@ -115,7 +115,7 @@ describe Ci::RetryBuildService, :services do
end
describe '#reprocess' do
- let(:new_build) { service.reprocess(build) }
+ let(:new_build) { service.reprocess!(build) }
context 'when user has ability to execute build' do
before do
@@ -131,11 +131,16 @@ describe Ci::RetryBuildService, :services do
it 'does not enqueue the new build' do
expect(new_build).to be_created
end
+
+ it 'does mark old build as retried' do
+ expect(new_build).to be_latest
+ expect(build.reload).to be_retried
+ end
end
context 'when user does not have ability to execute build' do
it 'raises an error' do
- expect { service.reprocess(build) }
+ expect { service.reprocess!(build) }
.to raise_error Gitlab::Access::AccessDeniedError
end
end
diff --git a/spec/services/ci/retry_pipeline_service_spec.rb b/spec/services/ci/retry_pipeline_service_spec.rb
index f1b2d3a4798..d941d56c0d8 100644
--- a/spec/services/ci/retry_pipeline_service_spec.rb
+++ b/spec/services/ci/retry_pipeline_service_spec.rb
@@ -7,11 +7,13 @@ describe Ci::RetryPipelineService, '#execute', :services do
let(:service) { described_class.new(project, user) }
context 'when user has ability to modify pipeline' do
- let(:user) { create(:admin) }
+ before do
+ project.add_master(user)
+ end
context 'when there are already retried jobs present' do
before do
- create_build('rspec', :canceled, 0)
+ create_build('rspec', :canceled, 0, retried: true)
create_build('rspec', :failed, 0)
end
@@ -227,6 +229,46 @@ describe Ci::RetryPipelineService, '#execute', :services do
end
end
+ context 'when user is not allowed to trigger manual action' do
+ before do
+ project.add_developer(user)
+ end
+
+ context 'when there is a failed manual action present' do
+ before do
+ create_build('test', :failed, 0)
+ create_build('deploy', :failed, 0, when: :manual)
+ create_build('verify', :canceled, 1)
+ end
+
+ it 'does not reprocess manual action' do
+ service.execute(pipeline)
+
+ expect(build('test')).to be_pending
+ expect(build('deploy')).to be_failed
+ expect(build('verify')).to be_created
+ expect(pipeline.reload).to be_running
+ end
+ end
+
+ context 'when there is a failed manual action in later stage' do
+ before do
+ create_build('test', :failed, 0)
+ create_build('deploy', :failed, 1, when: :manual)
+ create_build('verify', :canceled, 2)
+ end
+
+ it 'does not reprocess manual action' do
+ service.execute(pipeline)
+
+ expect(build('test')).to be_pending
+ expect(build('deploy')).to be_failed
+ expect(build('verify')).to be_created
+ expect(pipeline.reload).to be_running
+ end
+ end
+ end
+
def statuses
pipeline.reload.statuses
end
diff --git a/spec/services/ci/stop_environments_service_spec.rb b/spec/services/ci/stop_environments_service_spec.rb
index 32c72a9cf5e..98044ad232e 100644
--- a/spec/services/ci/stop_environments_service_spec.rb
+++ b/spec/services/ci/stop_environments_service_spec.rb
@@ -55,8 +55,22 @@ describe Ci::StopEnvironmentsService, services: true do
end
context 'when user does not have permission to stop environment' do
+ context 'when user has no access to manage deployments' do
+ before do
+ project.team << [user, :guest]
+ end
+
+ it 'does not stop environment' do
+ expect_environment_not_stopped_on('master')
+ end
+ end
+ end
+
+ context 'when branch for stop action is protected' do
before do
- project.team << [user, :guest]
+ project.add_developer(user)
+ create(:protected_branch, :no_one_can_push,
+ name: 'master', project: project)
end
it 'does not stop environment' do
diff --git a/spec/services/cohorts_service_spec.rb b/spec/services/cohorts_service_spec.rb
index 1e99442fdcb..77595d7ba2d 100644
--- a/spec/services/cohorts_service_spec.rb
+++ b/spec/services/cohorts_service_spec.rb
@@ -89,7 +89,7 @@ describe CohortsService do
activity_months: [{ total: 2, percentage: 100 }],
total: 2,
inactive: 1
- },
+ }
]
expect(described_class.new.execute).to eq(months_included: 12,
diff --git a/spec/services/create_deployment_service_spec.rb b/spec/services/create_deployment_service_spec.rb
index a883705bd45..f35d7a33548 100644
--- a/spec/services/create_deployment_service_spec.rb
+++ b/spec/services/create_deployment_service_spec.rb
@@ -255,7 +255,7 @@ describe CreateDeploymentService, services: true do
environment: 'production',
ref: 'master',
tag: false,
- sha: '97de212e80737a608d939f648d959671fb0a0142b',
+ sha: '97de212e80737a608d939f648d959671fb0a0142b'
}
end
diff --git a/spec/services/delete_merged_branches_service_spec.rb b/spec/services/delete_merged_branches_service_spec.rb
index 7b921f606f8..cae74df9c90 100644
--- a/spec/services/delete_merged_branches_service_spec.rb
+++ b/spec/services/delete_merged_branches_service_spec.rb
@@ -6,33 +6,22 @@ describe DeleteMergedBranchesService, services: true do
let(:project) { create(:project, :repository) }
context '#execute' do
- context 'unprotected branches' do
- before do
- service.execute
- end
+ it 'deletes a branch that was merged' do
+ service.execute
- it 'deletes a branch that was merged' do
- expect(project.repository.branch_names).not_to include('improve/awesome')
- end
+ expect(project.repository.branch_names).not_to include('improve/awesome')
+ end
- it 'keeps branch that is unmerged' do
- expect(project.repository.branch_names).to include('feature')
- end
+ it 'keeps branch that is unmerged' do
+ service.execute
- it 'keeps "master"' do
- expect(project.repository.branch_names).to include('master')
- end
+ expect(project.repository.branch_names).to include('feature')
end
- context 'protected branches' do
- before do
- create(:protected_branch, name: 'improve/awesome', project: project)
- service.execute
- end
+ it 'keeps "master"' do
+ service.execute
- it 'keeps protected branch' do
- expect(project.repository.branch_names).to include('improve/awesome')
- end
+ expect(project.repository.branch_names).to include('master')
end
context 'user without rights' do
diff --git a/spec/services/git_push_service_spec.rb b/spec/services/git_push_service_spec.rb
index 0477cac6677..ab06f45dbb9 100644
--- a/spec/services/git_push_service_spec.rb
+++ b/spec/services/git_push_service_spec.rb
@@ -584,7 +584,7 @@ describe GitPushService, services: true do
commit = double(:commit)
diff = double(:diff, new_path: 'README.md')
- expect(commit).to receive(:raw_diffs).with(deltas_only: true).
+ expect(commit).to receive(:raw_deltas).
and_return([diff])
service.push_commits = [commit]
@@ -622,12 +622,21 @@ describe GitPushService, services: true do
it 'only schedules a limited number of commits' do
allow(service).to receive(:push_commits).
- and_return(Array.new(1000, double(:commit, to_hash: {})))
+ and_return(Array.new(1000, double(:commit, to_hash: {}, matches_cross_reference_regex?: true)))
expect(ProcessCommitWorker).to receive(:perform_async).exactly(100).times
service.process_commit_messages
end
+
+ it "skips commits which don't include cross-references" do
+ allow(service).to receive(:push_commits).
+ and_return([double(:commit, to_hash: {}, matches_cross_reference_regex?: false)])
+
+ expect(ProcessCommitWorker).not_to receive(:perform_async)
+
+ service.process_commit_messages
+ end
end
def execute_service(project, user, oldrev, newrev, ref)
diff --git a/spec/services/issuable/bulk_update_service_spec.rb b/spec/services/issuable/bulk_update_service_spec.rb
index 7a1ac027310..6437d00e451 100644
--- a/spec/services/issuable/bulk_update_service_spec.rb
+++ b/spec/services/issuable/bulk_update_service_spec.rb
@@ -4,11 +4,12 @@ describe Issuable::BulkUpdateService, services: true do
let(:user) { create(:user) }
let(:project) { create(:empty_project, namespace: user.namespace) }
- def bulk_update(issues, extra_params = {})
+ def bulk_update(issuables, extra_params = {})
bulk_update_params = extra_params
- .reverse_merge(issuable_ids: Array(issues).map(&:id).join(','))
+ .reverse_merge(issuable_ids: Array(issuables).map(&:id).join(','))
- Issuable::BulkUpdateService.new(project, user, bulk_update_params).execute('issue')
+ type = Array(issuables).first.model_name.param_key
+ Issuable::BulkUpdateService.new(project, user, bulk_update_params).execute(type)
end
describe 'close issues' do
@@ -47,40 +48,77 @@ describe Issuable::BulkUpdateService, services: true do
end
end
- describe 'updating assignee' do
- let(:issue) { create(:issue, project: project, assignee: user) }
+ describe 'updating merge request assignee' do
+ let(:merge_request) { create(:merge_request, target_project: project, source_project: project, assignee: user) }
context 'when the new assignee ID is a valid user' do
it 'succeeds' do
new_assignee = create(:user)
project.team << [new_assignee, :developer]
- result = bulk_update(issue, assignee_id: new_assignee.id)
+ result = bulk_update(merge_request, assignee_id: new_assignee.id)
expect(result[:success]).to be_truthy
expect(result[:count]).to eq(1)
end
- it 'updates the assignee to the use ID passed' do
+ it 'updates the assignee to the user ID passed' do
assignee = create(:user)
project.team << [assignee, :developer]
- expect { bulk_update(issue, assignee_id: assignee.id) }
- .to change { issue.reload.assignee }.from(user).to(assignee)
+ expect { bulk_update(merge_request, assignee_id: assignee.id) }
+ .to change { merge_request.reload.assignee }.from(user).to(assignee)
end
end
context "when the new assignee ID is #{IssuableFinder::NONE}" do
it "unassigns the issues" do
- expect { bulk_update(issue, assignee_id: IssuableFinder::NONE) }
- .to change { issue.reload.assignee }.to(nil)
+ expect { bulk_update(merge_request, assignee_id: IssuableFinder::NONE) }
+ .to change { merge_request.reload.assignee }.to(nil)
end
end
context 'when the new assignee ID is not present' do
it 'does not unassign' do
- expect { bulk_update(issue, assignee_id: nil) }
- .not_to change { issue.reload.assignee }
+ expect { bulk_update(merge_request, assignee_id: nil) }
+ .not_to change { merge_request.reload.assignee }
+ end
+ end
+ end
+
+ describe 'updating issue assignee' do
+ let(:issue) { create(:issue, project: project, assignees: [user]) }
+
+ context 'when the new assignee ID is a valid user' do
+ it 'succeeds' do
+ new_assignee = create(:user)
+ project.team << [new_assignee, :developer]
+
+ result = bulk_update(issue, assignee_ids: [new_assignee.id])
+
+ expect(result[:success]).to be_truthy
+ expect(result[:count]).to eq(1)
+ end
+
+ it 'updates the assignee to the user ID passed' do
+ assignee = create(:user)
+ project.team << [assignee, :developer]
+ expect { bulk_update(issue, assignee_ids: [assignee.id]) }
+ .to change { issue.reload.assignees.first }.from(user).to(assignee)
+ end
+ end
+
+ context "when the new assignee ID is #{IssuableFinder::NONE}" do
+ it "unassigns the issues" do
+ expect { bulk_update(issue, assignee_ids: [IssuableFinder::NONE.to_s]) }
+ .to change { issue.reload.assignees.count }.from(1).to(0)
+ end
+ end
+
+ context 'when the new assignee ID is not present' do
+ it 'does not unassign' do
+ expect { bulk_update(issue, assignee_ids: []) }
+ .not_to change{ issue.reload.assignees }
end
end
end
@@ -125,7 +163,7 @@ describe Issuable::BulkUpdateService, services: true do
{
label_ids: labels.map(&:id),
add_label_ids: add_labels.map(&:id),
- remove_label_ids: remove_labels.map(&:id),
+ remove_label_ids: remove_labels.map(&:id)
}
end
diff --git a/spec/services/issues/build_service_spec.rb b/spec/services/issues/build_service_spec.rb
index 55d635235b0..bed25fe7ccf 100644
--- a/spec/services/issues/build_service_spec.rb
+++ b/spec/services/issues/build_service_spec.rb
@@ -136,7 +136,7 @@ describe Issues::BuildService, services: true do
user,
title: 'Issue #1',
description: 'Issue description',
- milestone_id: milestone.id,
+ milestone_id: milestone.id
).execute
expect(issue.title).to eq('Issue #1')
diff --git a/spec/services/issues/close_service_spec.rb b/spec/services/issues/close_service_spec.rb
index 7a54373963e..51840531711 100644
--- a/spec/services/issues/close_service_spec.rb
+++ b/spec/services/issues/close_service_spec.rb
@@ -4,7 +4,7 @@ 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(:issue) { create(:issue, assignees: [user2]) }
let(:project) { issue.project }
let!(:todo) { create(:todo, :assigned, user: user, project: project, target: issue, author: user2) }
diff --git a/spec/services/issues/create_service_spec.rb b/spec/services/issues/create_service_spec.rb
index 80bfb731550..dab1a3469f7 100644
--- a/spec/services/issues/create_service_spec.rb
+++ b/spec/services/issues/create_service_spec.rb
@@ -6,10 +6,10 @@ describe Issues::CreateService, services: true do
describe '#execute' do
let(:issue) { described_class.new(project, user, opts).execute }
+ let(:assignee) { create(:user) }
+ let(:milestone) { create(:milestone, project: project) }
context 'when params are valid' do
- let(:assignee) { create(:user) }
- let(:milestone) { create(:milestone, project: project) }
let(:labels) { create_pair(:label, project: project) }
before do
@@ -20,7 +20,7 @@ describe Issues::CreateService, services: true do
let(:opts) do
{ title: 'Awesome issue',
description: 'please fix',
- assignee_id: assignee.id,
+ assignee_ids: [assignee.id],
label_ids: labels.map(&:id),
milestone_id: milestone.id,
due_date: Date.tomorrow }
@@ -29,7 +29,7 @@ describe Issues::CreateService, services: true do
it 'creates the issue with the given params' do
expect(issue).to be_persisted
expect(issue.title).to eq('Awesome issue')
- expect(issue.assignee).to eq assignee
+ expect(issue.assignees).to eq [assignee]
expect(issue.labels).to match_array labels
expect(issue.milestone).to eq milestone
expect(issue.due_date).to eq Date.tomorrow
@@ -37,6 +37,7 @@ describe Issues::CreateService, services: true do
context 'when current user cannot admin issues in the project' do
let(:guest) { create(:user) }
+
before do
project.team << [guest, :guest]
end
@@ -47,7 +48,7 @@ describe Issues::CreateService, services: true do
expect(issue).to be_persisted
expect(issue.title).to eq('Awesome issue')
expect(issue.description).to eq('please fix')
- expect(issue.assignee).to be_nil
+ expect(issue.assignees).to be_empty
expect(issue.labels).to be_empty
expect(issue.milestone).to be_nil
expect(issue.due_date).to be_nil
@@ -117,6 +118,22 @@ describe Issues::CreateService, services: true do
end
end
+ context 'when assignee is set' do
+ let(:opts) do
+ { title: 'Title',
+ description: 'Description',
+ assignees: [assignee] }
+ end
+
+ it 'invalidates open issues counter for assignees when issue is assigned' do
+ project.team << [assignee, :master]
+
+ described_class.new(project, user, opts).execute
+
+ expect(assignee.assigned_open_issues_count).to eq 1
+ end
+ end
+
it 'executes issue hooks when issue is not confidential' do
opts = { title: 'Title', description: 'Description', confidential: false }
@@ -136,10 +153,83 @@ describe Issues::CreateService, services: true do
end
end
- it_behaves_like 'issuable create service'
+ context 'issue create service' do
+ context 'assignees' do
+ before { project.team << [user, :master] }
+
+ it 'removes assignee when user id is invalid' do
+ opts = { title: 'Title', description: 'Description', assignee_ids: [-1] }
+
+ issue = described_class.new(project, user, opts).execute
+
+ expect(issue.assignees).to be_empty
+ end
+
+ it 'removes assignee when user id is 0' do
+ opts = { title: 'Title', description: 'Description', assignee_ids: [0] }
+
+ issue = described_class.new(project, user, opts).execute
+
+ expect(issue.assignees).to be_empty
+ end
+
+ it 'saves assignee when user id is valid' do
+ project.team << [assignee, :master]
+ opts = { title: 'Title', description: 'Description', assignee_ids: [assignee.id] }
+
+ issue = described_class.new(project, user, opts).execute
+
+ expect(issue.assignees).to eq([assignee])
+ end
+
+ context "when issuable feature is private" do
+ before do
+ project.project_feature.update(issues_access_level: ProjectFeature::PRIVATE,
+ merge_requests_access_level: ProjectFeature::PRIVATE)
+ end
+
+ levels = [Gitlab::VisibilityLevel::INTERNAL, Gitlab::VisibilityLevel::PUBLIC]
+
+ levels.each do |level|
+ it "removes not authorized assignee when project is #{Gitlab::VisibilityLevel.level_name(level)}" do
+ project.update(visibility_level: level)
+ opts = { title: 'Title', description: 'Description', assignee_ids: [assignee.id] }
+
+ issue = described_class.new(project, user, opts).execute
+
+ expect(issue.assignees).to be_empty
+ end
+ end
+ end
+ end
+ end
it_behaves_like 'new issuable record that supports slash commands'
+ context 'Slash commands' do
+ context 'with assignee and milestone in params and command' do
+ let(:opts) do
+ {
+ assignee_ids: [create(:user).id],
+ milestone_id: 1,
+ title: 'Title',
+ description: %(/assign @#{assignee.username}\n/milestone %"#{milestone.name}")
+ }
+ end
+
+ before do
+ project.team << [user, :master]
+ project.team << [assignee, :master]
+ end
+
+ it 'assigns and sets milestone to issuable from command' do
+ expect(issue).to be_persisted
+ expect(issue.assignees).to eq([assignee])
+ expect(issue.milestone).to eq(milestone)
+ end
+ end
+ end
+
context 'resolving discussions' do
let(:discussion) { create(:diff_note_on_merge_request).to_discussion }
let(:merge_request) { discussion.noteable }
diff --git a/spec/services/issues/resolve_discussions_spec.rb b/spec/services/issues/resolve_discussions_spec.rb
index 4a4929daefc..86f218dec12 100644
--- a/spec/services/issues/resolve_discussions_spec.rb
+++ b/spec/services/issues/resolve_discussions_spec.rb
@@ -1,15 +1,15 @@
require 'spec_helper.rb'
-class DummyService < Issues::BaseService
- include ::Issues::ResolveDiscussions
+describe Issues::ResolveDiscussions, services: true do
+ class DummyService < Issues::BaseService
+ include ::Issues::ResolveDiscussions
- def initialize(*args)
- super
- filter_resolve_discussion_params
+ def initialize(*args)
+ super
+ filter_resolve_discussion_params
+ end
end
-end
-describe DummyService, services: true do
let(:project) { create(:project, :repository) }
let(:user) { create(:user) }
@@ -23,7 +23,7 @@ describe DummyService, services: true do
let(:other_merge_request) { create(:merge_request, source_project: project, source_branch: "other") }
describe "#merge_request_for_resolving_discussion" do
- let(:service) { described_class.new(project, user, merge_request_to_resolve_discussions_of: merge_request.iid) }
+ let(:service) { DummyService.new(project, user, merge_request_to_resolve_discussions_of: merge_request.iid) }
it "finds the merge request" do
expect(service.merge_request_to_resolve_discussions_of).to eq(merge_request)
@@ -43,7 +43,7 @@ describe DummyService, services: true do
describe "#discussions_to_resolve" do
it "contains a single discussion when matching merge request and discussion are passed" do
- service = described_class.new(
+ service = DummyService.new(
project,
user,
discussion_to_resolve: discussion.id,
@@ -61,7 +61,7 @@ describe DummyService, services: true do
noteable: merge_request,
project: merge_request.target_project,
line_number: 15)])
- service = described_class.new(
+ service = DummyService.new(
project,
user,
merge_request_to_resolve_discussions_of: merge_request.iid
@@ -77,9 +77,9 @@ describe DummyService, services: true do
_second_discussion = Discussion.new([create(:diff_note_on_merge_request, :resolved,
noteable: merge_request,
project: merge_request.target_project,
- line_number: 15,
+ line_number: 15
)])
- service = described_class.new(
+ service = DummyService.new(
project,
user,
merge_request_to_resolve_discussions_of: merge_request.iid
@@ -92,7 +92,7 @@ describe DummyService, services: true do
end
it "is empty when a discussion and another merge request are passed" do
- service = described_class.new(
+ service = DummyService.new(
project,
user,
discussion_to_resolve: discussion.id,
diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb
index 5b324f3c706..5184c1d5f19 100644
--- a/spec/services/issues/update_service_spec.rb
+++ b/spec/services/issues/update_service_spec.rb
@@ -14,7 +14,7 @@ describe Issues::UpdateService, services: true do
let(:issue) do
create(:issue, title: 'Old title',
description: "for #{user2.to_reference}",
- assignee_id: user3.id,
+ assignee_ids: [user3.id],
project: project)
end
@@ -40,7 +40,7 @@ describe Issues::UpdateService, services: true do
{
title: 'New title',
description: 'Also please fix',
- assignee_id: user2.id,
+ assignee_ids: [user2.id],
state_event: 'close',
label_ids: [label.id],
due_date: Date.tomorrow
@@ -53,15 +53,22 @@ describe Issues::UpdateService, services: true do
expect(issue).to be_valid
expect(issue.title).to eq 'New title'
expect(issue.description).to eq 'Also please fix'
- expect(issue.assignee).to eq user2
+ expect(issue.assignees).to match_array([user2])
expect(issue).to be_closed
expect(issue.labels).to match_array [label]
expect(issue.due_date).to eq Date.tomorrow
end
+ it 'updates open issue counter for assignees when issue is reassigned' do
+ update_issue(assignee_ids: [user2.id])
+
+ expect(user3.assigned_open_issues_count).to eq 0
+ expect(user2.assigned_open_issues_count).to eq 1
+ end
+
it 'sorts issues as specified by parameters' do
- issue1 = create(:issue, project: project, assignee_id: user3.id)
- issue2 = create(:issue, project: project, assignee_id: user3.id)
+ issue1 = create(:issue, project: project, assignees: [user3])
+ issue2 = create(:issue, project: project, assignees: [user3])
[issue, issue1, issue2].each do |issue|
issue.move_to_end
@@ -87,7 +94,7 @@ describe Issues::UpdateService, services: true do
expect(issue).to be_valid
expect(issue.title).to eq 'New title'
expect(issue.description).to eq 'Also please fix'
- expect(issue.assignee).to eq user3
+ expect(issue.assignees).to match_array [user3]
expect(issue.labels).to be_empty
expect(issue.milestone).to be_nil
expect(issue.due_date).to be_nil
@@ -132,12 +139,23 @@ describe Issues::UpdateService, services: true do
end
end
+ context 'when description changed' do
+ it 'creates system note about description change' do
+ update_issue(description: 'Changed description')
+
+ note = find_note('changed the description')
+
+ expect(note).not_to be_nil
+ expect(note.note).to eq('changed the description')
+ end
+ end
+
context 'when issue turns confidential' do
let(:opts) do
{
title: 'New title',
description: 'Also please fix',
- assignee_id: user2.id,
+ assignee_ids: [user2],
state_event: 'close',
label_ids: [label.id],
confidential: true
@@ -163,12 +181,12 @@ describe Issues::UpdateService, services: true do
it 'does not update assignee_id with unauthorized users' do
project.update(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
update_issue(confidential: true)
- non_member = create(:user)
- original_assignee = issue.assignee
+ non_member = create(:user)
+ original_assignees = issue.assignees
- update_issue(assignee_id: non_member.id)
+ update_issue(assignee_ids: [non_member.id])
- expect(issue.reload.assignee_id).to eq(original_assignee.id)
+ expect(issue.reload.assignees).to eq(original_assignees)
end
end
@@ -205,7 +223,7 @@ describe Issues::UpdateService, services: true do
context 'when is reassigned' do
before do
- update_issue(assignee: user2)
+ update_issue(assignees: [user2])
end
it 'marks previous assignee todos as done' do
@@ -408,6 +426,41 @@ describe Issues::UpdateService, services: true do
end
end
+ context 'updating asssignee_id' do
+ it 'does not update assignee when assignee_id is invalid' do
+ update_issue(assignee_ids: [-1])
+
+ expect(issue.reload.assignees).to eq([user3])
+ end
+
+ it 'unassigns assignee when user id is 0' do
+ update_issue(assignee_ids: [0])
+
+ expect(issue.reload.assignees).to be_empty
+ end
+
+ it 'does not update assignee_id when user cannot read issue' do
+ update_issue(assignee_ids: [create(:user).id])
+
+ expect(issue.reload.assignees).to eq([user3])
+ end
+
+ context "when issuable feature is private" do
+ levels = [Gitlab::VisibilityLevel::INTERNAL, Gitlab::VisibilityLevel::PUBLIC]
+
+ levels.each do |level|
+ it "does not update with unauthorized assignee when project is #{Gitlab::VisibilityLevel.level_name(level)}" do
+ assignee = create(:user)
+ project.update(visibility_level: level)
+ feature_visibility_attr = :"#{issue.model_name.plural}_access_level"
+ project.project_feature.update_attribute(feature_visibility_attr, ProjectFeature::PRIVATE)
+
+ expect{ update_issue(assignee_ids: [assignee.id]) }.not_to change{ issue.assignees }
+ end
+ end
+ end
+ end
+
context 'updating mentions' do
let(:mentionable) { issue }
include_examples 'updating mentions', Issues::UpdateService
diff --git a/spec/services/members/authorized_destroy_service_spec.rb b/spec/services/members/authorized_destroy_service_spec.rb
index 3b35a3b8e3a..ab440d18e9f 100644
--- a/spec/services/members/authorized_destroy_service_spec.rb
+++ b/spec/services/members/authorized_destroy_service_spec.rb
@@ -14,8 +14,8 @@ describe Members::AuthorizedDestroyService, services: true do
it "unassigns issues and merge requests" do
group.add_developer(member_user)
- issue = create :issue, project: group_project, assignee: member_user
- create :issue, assignee: member_user
+ issue = create :issue, project: group_project, assignees: [member_user]
+ create :issue, assignees: [member_user]
merge_request = create :merge_request, target_project: group_project, source_project: group_project, assignee: member_user
create :merge_request, target_project: project, source_project: project, assignee: member_user
@@ -33,7 +33,7 @@ describe Members::AuthorizedDestroyService, services: true do
it "unassigns issues and merge requests" do
project.team << [member_user, :developer]
- create :issue, project: project, assignee: member_user
+ create :issue, project: project, assignees: [member_user]
create :merge_request, target_project: project, source_project: project, assignee: member_user
member = project.members.find_by(user_id: member_user.id)
diff --git a/spec/services/merge_requests/assign_issues_service_spec.rb b/spec/services/merge_requests/assign_issues_service_spec.rb
index fe75757dd29..d3556020d4d 100644
--- a/spec/services/merge_requests/assign_issues_service_spec.rb
+++ b/spec/services/merge_requests/assign_issues_service_spec.rb
@@ -15,14 +15,14 @@ describe MergeRequests::AssignIssuesService, services: true do
expect(service.assignable_issues.map(&:id)).to include(issue.id)
end
- it 'ignores issues already assigned to any user' do
- issue.update!(assignee: create(:user))
+ it 'ignores issues the user cannot update assignee on' do
+ project.team.truncate
expect(service.assignable_issues).to be_empty
end
- it 'ignores issues the user cannot update assignee on' do
- project.team.truncate
+ it 'ignores issues already assigned to any user' do
+ issue.assignees = [create(:user)]
expect(service.assignable_issues).to be_empty
end
@@ -44,7 +44,7 @@ describe MergeRequests::AssignIssuesService, services: true do
end
it 'assigns these to the merge request owner' do
- expect { service.execute }.to change { issue.reload.assignee }.to(user)
+ expect { service.execute }.to change { issue.assignees.first }.to(user)
end
it 'ignores external issues' do
diff --git a/spec/services/merge_requests/build_service_spec.rb b/spec/services/merge_requests/build_service_spec.rb
index be9f9ea2dec..6f9d1208b1d 100644
--- a/spec/services/merge_requests/build_service_spec.rb
+++ b/spec/services/merge_requests/build_service_spec.rb
@@ -261,6 +261,16 @@ describe MergeRequests::BuildService, services: true do
end
end
+ context 'upstream project has disabled merge requests' do
+ let(:upstream_project) { create(:empty_project, :merge_requests_disabled) }
+ let(:project) { create(:empty_project, forked_from_project: upstream_project) }
+ let(:commits) { Commit.decorate([commit_1], project) }
+
+ it 'sets target project correctly' do
+ expect(merge_request.target_project).to eq(project)
+ end
+ end
+
context 'target_project is set and accessible by current_user' do
let(:target_project) { create(:project, :public, :repository)}
let(:commits) { Commit.decorate([commit_1], project) }
diff --git a/spec/services/merge_requests/conflicts/list_service_spec.rb b/spec/services/merge_requests/conflicts/list_service_spec.rb
new file mode 100644
index 00000000000..e8a305d6130
--- /dev/null
+++ b/spec/services/merge_requests/conflicts/list_service_spec.rb
@@ -0,0 +1,73 @@
+require 'spec_helper'
+
+describe MergeRequests::Conflicts::ListService do
+ describe '#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
+
+ def conflicts_service(merge_request)
+ described_class.new(merge_request)
+ 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(conflicts_service(merge_request).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(conflicts_service(merge_request).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')
+ service = conflicts_service(merge_request)
+ allow(service.conflicts).to receive(:merge_index).and_raise(Rugged::OdbError)
+
+ expect(service.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(conflicts_service(merge_request).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(conflicts_service(merge_request).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(conflicts_service(merge_request).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(conflicts_service(merge_request).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(conflicts_service(merge_request).can_be_resolved_in_ui?).to be_truthy
+ end
+
+ it 'returns a truthy value when the conflicts have to be resolved in an editor' do
+ merge_request = create_merge_request('conflict-contains-conflict-markers')
+
+ expect(conflicts_service(merge_request).can_be_resolved_in_ui?).to be_truthy
+ end
+ end
+end
diff --git a/spec/services/merge_requests/resolve_service_spec.rb b/spec/services/merge_requests/conflicts/resolve_service_spec.rb
index eaf7785e549..19e8d5cc5f1 100644
--- a/spec/services/merge_requests/resolve_service_spec.rb
+++ b/spec/services/merge_requests/conflicts/resolve_service_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe MergeRequests::ResolveService do
+describe MergeRequests::Conflicts::ResolveService do
let(:user) { create(:user) }
let(:project) { create(:project, :repository) }
@@ -24,6 +24,8 @@ describe MergeRequests::ResolveService do
end
describe '#execute' do
+ let(:service) { described_class.new(merge_request) }
+
context 'with section params' do
let(:params) do
{
@@ -50,7 +52,7 @@ describe MergeRequests::ResolveService do
context 'when the source and target project are the same' do
before do
- MergeRequests::ResolveService.new(project, user, params).execute(merge_request)
+ service.execute(user, params)
end
it 'creates a commit with the message' do
@@ -74,15 +76,26 @@ describe MergeRequests::ResolveService do
branch_name: 'conflict-start')
end
- before do
- MergeRequests::ResolveService.new(fork_project, user, params).execute(merge_request_from_fork)
+ def resolve_conflicts
+ described_class.new(merge_request_from_fork).execute(user, params)
+ end
+
+ it 'gets conflicts from the source project' do
+ expect(fork_project.repository.rugged).to receive(:merge_commits).and_call_original
+ expect(project.repository.rugged).not_to receive(:merge_commits)
+
+ resolve_conflicts
end
it 'creates a commit with the message' do
+ resolve_conflicts
+
expect(merge_request_from_fork.source_branch_head.message).to eq(params[:commit_message])
end
it 'creates a commit with the correct parents' do
+ resolve_conflicts
+
expect(merge_request_from_fork.source_branch_head.parents.map(&:id)).
to eq(['404fa3fc7c2c9b5dacff102f353bdf55b1be2813',
target_head])
@@ -115,7 +128,7 @@ describe MergeRequests::ResolveService do
end
before do
- MergeRequests::ResolveService.new(project, user, params).execute(merge_request)
+ service.execute(user, params)
end
it 'creates a commit with the message' do
@@ -154,15 +167,15 @@ describe MergeRequests::ResolveService do
}
end
- let(:service) { MergeRequests::ResolveService.new(project, user, invalid_params) }
-
it 'raises a MissingResolution error' do
- expect { service.execute(merge_request) }.
+ expect { service.execute(user, invalid_params) }.
to raise_error(Gitlab::Conflict::File::MissingResolution)
end
end
context 'when the content of a file is unchanged' do
+ let(:list_service) { MergeRequests::Conflicts::ListService.new(merge_request) }
+
let(:invalid_params) do
{
files: [
@@ -173,17 +186,15 @@ describe MergeRequests::ResolveService do
}, {
old_path: 'files/ruby/regex.rb',
new_path: 'files/ruby/regex.rb',
- content: merge_request.conflicts.file_for_path('files/ruby/regex.rb', 'files/ruby/regex.rb').content
+ content: list_service.conflicts.file_for_path('files/ruby/regex.rb', 'files/ruby/regex.rb').content
}
],
commit_message: 'This is a commit message!'
}
end
- let(:service) { MergeRequests::ResolveService.new(project, user, invalid_params) }
-
it 'raises a MissingResolution error' do
- expect { service.execute(merge_request) }.
+ expect { service.execute(user, invalid_params) }.
to raise_error(Gitlab::Conflict::File::MissingResolution)
end
end
@@ -202,11 +213,9 @@ describe MergeRequests::ResolveService do
}
end
- let(:service) { MergeRequests::ResolveService.new(project, user, invalid_params) }
-
it 'raises a MissingFiles error' do
- expect { service.execute(merge_request) }.
- to raise_error(MergeRequests::ResolveService::MissingFiles)
+ expect { service.execute(user, invalid_params) }.
+ to raise_error(described_class::MissingFiles)
end
end
end
diff --git a/spec/services/merge_requests/create_from_issue_service_spec.rb b/spec/services/merge_requests/create_from_issue_service_spec.rb
new file mode 100644
index 00000000000..1588d30c394
--- /dev/null
+++ b/spec/services/merge_requests/create_from_issue_service_spec.rb
@@ -0,0 +1,74 @@
+require 'spec_helper'
+
+describe MergeRequests::CreateFromIssueService, services: true do
+ let(:project) { create(:project, :repository) }
+ let(:user) { create(:user) }
+ let(:issue) { create(:issue, project: project) }
+
+ subject(:service) { described_class.new(project, user, issue_iid: issue.iid) }
+
+ before do
+ project.add_developer(user)
+ end
+
+ describe '#execute' do
+ it 'returns an error with invalid issue iid' do
+ result = described_class.new(project, user, issue_iid: -1).execute
+
+ expect(result[:status]).to eq :error
+ expect(result[:message]).to eq 'Invalid issue iid'
+ end
+
+ it 'delegates issue search to IssuesFinder' do
+ expect_any_instance_of(IssuesFinder).to receive(:execute).once.and_call_original
+
+ described_class.new(project, user, issue_iid: -1).execute
+ end
+
+ it 'delegates the branch creation to CreateBranchService' do
+ expect_any_instance_of(CreateBranchService).to receive(:execute).once.and_call_original
+
+ service.execute
+ end
+
+ it 'creates a branch based on issue title' do
+ service.execute
+
+ expect(project.repository.branch_exists?(issue.to_branch_name)).to be_truthy
+ end
+
+ it 'creates a system note' do
+ expect(SystemNoteService).to receive(:new_issue_branch).with(issue, project, user, issue.to_branch_name)
+
+ service.execute
+ end
+
+ it 'creates a merge request' do
+ expect { service.execute }.to change(project.merge_requests, :count).by(1)
+ end
+
+ it 'sets the merge request title to: "WIP: Resolves "$issue-title"' do
+ result = service.execute
+
+ expect(result[:merge_request].title).to eq("WIP: Resolve \"#{issue.title}\"")
+ end
+
+ it 'sets the merge request author to current user' do
+ result = service.execute
+
+ expect(result[:merge_request].author).to eq user
+ end
+
+ it 'sets the merge request source branch to the new issue branch' do
+ result = service.execute
+
+ expect(result[:merge_request].source_branch).to eq issue.to_branch_name
+ end
+
+ it 'sets the merge request target branch to the project default branch' do
+ result = service.execute
+
+ expect(result[:merge_request].target_branch).to eq project.default_branch
+ end
+ end
+end
diff --git a/spec/services/merge_requests/create_service_spec.rb b/spec/services/merge_requests/create_service_spec.rb
index 0e16c7cc94b..41752f1a01a 100644
--- a/spec/services/merge_requests/create_service_spec.rb
+++ b/spec/services/merge_requests/create_service_spec.rb
@@ -84,7 +84,107 @@ describe MergeRequests::CreateService, services: true do
end
end
- it_behaves_like 'issuable create service'
+ context 'Slash commands' do
+ context 'with assignee and milestone in params and command' do
+ let(:merge_request) { described_class.new(project, user, opts).execute }
+ let(:milestone) { create(:milestone, project: project) }
+
+ let(:opts) do
+ {
+ assignee_id: create(:user).id,
+ milestone_id: 1,
+ title: 'Title',
+ description: %(/assign @#{assignee.username}\n/milestone %"#{milestone.name}"),
+ source_branch: 'feature',
+ target_branch: 'master'
+ }
+ end
+
+ before do
+ project.team << [user, :master]
+ project.team << [assignee, :master]
+ end
+
+ it 'assigns and sets milestone to issuable from command' do
+ expect(merge_request).to be_persisted
+ expect(merge_request.assignee).to eq(assignee)
+ expect(merge_request.milestone).to eq(milestone)
+ end
+ end
+ end
+
+ context 'merge request create service' do
+ context 'asssignee_id' do
+ let(:assignee) { create(:user) }
+
+ before { project.team << [user, :master] }
+
+ it 'removes assignee_id when user id is invalid' do
+ opts = { title: 'Title', description: 'Description', assignee_id: -1 }
+
+ merge_request = described_class.new(project, user, opts).execute
+
+ expect(merge_request.assignee_id).to be_nil
+ end
+
+ it 'removes assignee_id when user id is 0' do
+ opts = { title: 'Title', description: 'Description', assignee_id: 0 }
+
+ merge_request = described_class.new(project, user, opts).execute
+
+ expect(merge_request.assignee_id).to be_nil
+ end
+
+ it 'saves assignee when user id is valid' do
+ project.team << [assignee, :master]
+ opts = { title: 'Title', description: 'Description', assignee_id: assignee.id }
+
+ merge_request = described_class.new(project, user, opts).execute
+
+ expect(merge_request.assignee).to eq(assignee)
+ end
+
+ context 'when assignee is set' do
+ let(:opts) do
+ {
+ title: 'Title',
+ description: 'Description',
+ assignee_id: assignee.id,
+ source_branch: 'feature',
+ target_branch: 'master'
+ }
+ end
+
+ it 'invalidates open merge request counter for assignees when merge request is assigned' do
+ project.team << [assignee, :master]
+
+ described_class.new(project, user, opts).execute
+
+ expect(assignee.assigned_open_merge_requests_count).to eq 1
+ end
+ end
+
+ context "when issuable feature is private" do
+ before do
+ project.project_feature.update(issues_access_level: ProjectFeature::PRIVATE,
+ merge_requests_access_level: ProjectFeature::PRIVATE)
+ end
+
+ levels = [Gitlab::VisibilityLevel::INTERNAL, Gitlab::VisibilityLevel::PUBLIC]
+
+ levels.each do |level|
+ it "removes not authorized assignee when project is #{Gitlab::VisibilityLevel.level_name(level)}" do
+ project.update(visibility_level: level)
+ opts = { title: 'Title', description: 'Description', assignee_id: assignee.id }
+
+ merge_request = described_class.new(project, user, opts).execute
+
+ expect(merge_request.assignee_id).to be_nil
+ end
+ end
+ end
+ end
+ end
context 'while saving references to issues that the created merge request closes' do
let(:first_issue) { create(:issue, project: project) }
diff --git a/spec/services/merge_requests/get_urls_service_spec.rb b/spec/services/merge_requests/get_urls_service_spec.rb
index 290e00ea1ba..4a7d8ab4c6c 100644
--- a/spec/services/merge_requests/get_urls_service_spec.rb
+++ b/spec/services/merge_requests/get_urls_service_spec.rb
@@ -2,7 +2,7 @@ require "spec_helper"
describe MergeRequests::GetUrlsService do
let(:project) { create(:project, :public, :repository) }
- let(:service) { MergeRequests::GetUrlsService.new(project) }
+ let(:service) { described_class.new(project) }
let(:source_branch) { "my_branch" }
let(:new_merge_request_url) { "http://#{Gitlab.config.gitlab.host}/#{project.namespace.name}/#{project.path}/merge_requests/new?merge_request%5Bsource_branch%5D=#{source_branch}" }
let(:show_merge_request_url) { "http://#{Gitlab.config.gitlab.host}/#{project.namespace.name}/#{project.path}/merge_requests/#{merge_request.iid}" }
@@ -89,7 +89,7 @@ describe MergeRequests::GetUrlsService do
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) }
+ let(:service) { described_class.new(forked_project) }
before do
allow(forked_project).to receive(:empty_repo?).and_return(false)
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
index 35804d41b46..935f4710851 100644
--- a/spec/services/merge_requests/merge_request_diff_cache_service_spec.rb
+++ b/spec/services/merge_requests/merge_request_diff_cache_service_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe MergeRequests::MergeRequestDiffCacheService do
- let(:subject) { MergeRequests::MergeRequestDiffCacheService.new }
+ let(:subject) { described_class.new }
describe '#execute' do
it 'retrieves the diff files to cache the highlighted result' do
diff --git a/spec/services/merge_requests/merge_when_pipeline_succeeds_service_spec.rb b/spec/services/merge_requests/merge_when_pipeline_succeeds_service_spec.rb
index 769b3193275..3ef5135e6a3 100644
--- a/spec/services/merge_requests/merge_when_pipeline_succeeds_service_spec.rb
+++ b/spec/services/merge_requests/merge_when_pipeline_succeeds_service_spec.rb
@@ -82,6 +82,10 @@ describe MergeRequests::MergeWhenPipelineSucceedsService do
sha: merge_request_head, status: 'success')
end
+ before do
+ mr_merge_if_green_enabled.update(head_pipeline: triggering_pipeline)
+ end
+
it "merges all merge requests with merge when the pipeline succeeds enabled" do
expect(MergeWorker).to receive(:perform_async)
service.trigger(triggering_pipeline)
@@ -124,6 +128,8 @@ describe MergeRequests::MergeWhenPipelineSucceedsService do
sha: mr_conflict.diff_head_sha, status: 'success')
end
+ before { mr_conflict.update(head_pipeline: conflict_pipeline) }
+
it 'does not merge the merge request' do
expect(MergeWorker).not_to receive(:perform_async)
diff --git a/spec/services/merge_requests/refresh_service_spec.rb b/spec/services/merge_requests/refresh_service_spec.rb
index 03215a4624a..1f109eab268 100644
--- a/spec/services/merge_requests/refresh_service_spec.rb
+++ b/spec/services/merge_requests/refresh_service_spec.rb
@@ -348,7 +348,7 @@ describe MergeRequests::RefreshService, services: true do
title: 'fixup! Fix issue',
work_in_progress?: true,
to_reference: 'ccccccc'
- ),
+ )
])
refresh_service.execute(@oldrev, @newrev, 'refs/heads/wip')
reload_mrs
diff --git a/spec/services/merge_requests/resolved_discussion_notification_service.rb b/spec/services/merge_requests/resolved_discussion_notification_service_spec.rb
index 7ddd812e513..7ddd812e513 100644
--- a/spec/services/merge_requests/resolved_discussion_notification_service.rb
+++ b/spec/services/merge_requests/resolved_discussion_notification_service_spec.rb
diff --git a/spec/services/merge_requests/update_service_spec.rb b/spec/services/merge_requests/update_service_spec.rb
index f2ca1e6fcbd..2c8fbb46e75 100644
--- a/spec/services/merge_requests/update_service_spec.rb
+++ b/spec/services/merge_requests/update_service_spec.rb
@@ -102,6 +102,13 @@ describe MergeRequests::UpdateService, services: true do
expect(note.note).to eq 'changed title from **{-Old-} title** to **{+New+} title**'
end
+ it 'creates system note about description change' do
+ note = find_note('changed the description')
+
+ expect(note).not_to be_nil
+ expect(note.note).to eq('changed the description')
+ end
+
it 'creates system note about branch change' do
note = find_note('changed target')
@@ -167,11 +174,13 @@ describe MergeRequests::UpdateService, services: true do
context 'with active pipeline' do
before do
service_mock = double
- create(:ci_pipeline_with_one_job,
+ pipeline = create(:ci_pipeline_with_one_job,
project: project,
ref: merge_request.source_branch,
sha: merge_request.diff_head_sha)
+ merge_request.update(head_pipeline: pipeline)
+
expect(MergeRequests::MergeWhenPipelineSucceedsService).to receive(:new).with(project, user).
and_return(service_mock)
expect(service_mock).to receive(:execute).with(merge_request)
@@ -290,6 +299,15 @@ describe MergeRequests::UpdateService, services: true do
end
end
+ context 'when the assignee changes' do
+ it 'updates open merge request counter for assignees when merge request is reassigned' do
+ update_merge_request(assignee_id: user2.id)
+
+ expect(user3.assigned_open_merge_requests_count).to eq 0
+ expect(user2.assigned_open_merge_requests_count).to eq 1
+ end
+ end
+
context 'when the target branch change' do
before do
update_merge_request({ target_branch: 'target' })
@@ -423,6 +441,54 @@ describe MergeRequests::UpdateService, services: true do
end
end
+ context 'updating asssignee_id' do
+ it 'does not update assignee when assignee_id is invalid' do
+ merge_request.update(assignee_id: user.id)
+
+ update_merge_request(assignee_id: -1)
+
+ expect(merge_request.reload.assignee).to eq(user)
+ end
+
+ it 'unassigns assignee when user id is 0' do
+ merge_request.update(assignee_id: user.id)
+
+ update_merge_request(assignee_id: 0)
+
+ expect(merge_request.assignee_id).to be_nil
+ end
+
+ it 'saves assignee when user id is valid' do
+ update_merge_request(assignee_id: user.id)
+
+ expect(merge_request.assignee_id).to eq(user.id)
+ end
+
+ it 'does not update assignee_id when user cannot read issue' do
+ non_member = create(:user)
+ original_assignee = merge_request.assignee
+
+ update_merge_request(assignee_id: non_member.id)
+
+ expect(merge_request.assignee_id).to eq(original_assignee.id)
+ end
+
+ context "when issuable feature is private" do
+ levels = [Gitlab::VisibilityLevel::INTERNAL, Gitlab::VisibilityLevel::PUBLIC]
+
+ levels.each do |level|
+ it "does not update with unauthorized assignee when project is #{Gitlab::VisibilityLevel.level_name(level)}" do
+ assignee = create(:user)
+ project.update(visibility_level: level)
+ feature_visibility_attr = :"#{merge_request.model_name.plural}_access_level"
+ project.project_feature.update_attribute(feature_visibility_attr, ProjectFeature::PRIVATE)
+
+ expect{ update_merge_request(assignee_id: assignee) }.not_to change{ merge_request.assignee }
+ end
+ end
+ end
+ end
+
include_examples 'issuable update service' do
let(:open_issuable) { merge_request }
let(:closed_issuable) { create(:closed_merge_request, source_project: project) }
diff --git a/spec/services/notes/build_service_spec.rb b/spec/services/notes/build_service_spec.rb
index f9dd5541b10..133175769ca 100644
--- a/spec/services/notes/build_service_spec.rb
+++ b/spec/services/notes/build_service_spec.rb
@@ -29,10 +29,82 @@ describe Notes::BuildService, services: true do
expect(new_note.errors[:base]).to include('Discussion to reply to cannot be found')
end
end
+
+ context 'personal snippet note' do
+ def reply(note, user = nil)
+ user ||= create(:user)
+
+ described_class.new(nil,
+ user,
+ note: 'Test',
+ in_reply_to_discussion_id: note.discussion_id).execute
+ end
+
+ let(:snippet_author) { create(:user) }
+
+ context 'when a snippet is public' do
+ it 'creates a reply note' do
+ snippet = create(:personal_snippet, :public)
+ note = create(:discussion_note_on_personal_snippet, noteable: snippet)
+
+ new_note = reply(note)
+
+ expect(new_note).to be_valid
+ expect(new_note.in_reply_to?(note)).to be_truthy
+ end
+ end
+
+ context 'when a snippet is private' do
+ let(:snippet) { create(:personal_snippet, :private, author: snippet_author) }
+ let(:note) { create(:discussion_note_on_personal_snippet, noteable: snippet) }
+
+ it 'creates a reply note when the author replies' do
+ new_note = reply(note, snippet_author)
+
+ expect(new_note).to be_valid
+ expect(new_note.in_reply_to?(note)).to be_truthy
+ end
+
+ it 'sets an error when another user replies' do
+ new_note = reply(note)
+
+ expect(new_note.errors[:base]).to include('Discussion to reply to cannot be found')
+ end
+ end
+
+ context 'when a snippet is internal' do
+ let(:snippet) { create(:personal_snippet, :internal, author: snippet_author) }
+ let(:note) { create(:discussion_note_on_personal_snippet, noteable: snippet) }
+
+ it 'creates a reply note when the author replies' do
+ new_note = reply(note, snippet_author)
+
+ expect(new_note).to be_valid
+ expect(new_note.in_reply_to?(note)).to be_truthy
+ end
+
+ it 'creates a reply note when a regular user replies' do
+ new_note = reply(note)
+
+ expect(new_note).to be_valid
+ expect(new_note.in_reply_to?(note)).to be_truthy
+ end
+
+ it 'sets an error when an external user replies' do
+ new_note = reply(note, create(:user, :external))
+
+ expect(new_note.errors[:base]).to include('Discussion to reply to cannot be found')
+ end
+ end
+ end
end
it 'builds a note without saving it' do
- new_note = described_class.new(project, author, noteable_type: note.noteable_type, noteable_id: note.noteable_id, note: 'Test').execute
+ new_note = described_class.new(project,
+ author,
+ noteable_type: note.noteable_type,
+ noteable_id: note.noteable_id,
+ note: 'Test').execute
expect(new_note).to be_valid
expect(new_note).not_to be_persisted
end
diff --git a/spec/services/notes/slash_commands_service_spec.rb b/spec/services/notes/slash_commands_service_spec.rb
index 1a64c8bbf00..c9954dc3603 100644
--- a/spec/services/notes/slash_commands_service_spec.rb
+++ b/spec/services/notes/slash_commands_service_spec.rb
@@ -66,7 +66,7 @@ describe Notes::SlashCommandsService, services: true do
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.assignees).to eq([assignee])
expect(note.noteable.milestone).to eq(milestone)
end
end
@@ -113,7 +113,7 @@ describe Notes::SlashCommandsService, services: true do
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.assignees).to eq([assignee])
expect(note.noteable.milestone).to eq(milestone)
end
end
@@ -220,4 +220,31 @@ describe Notes::SlashCommandsService, services: true do
let(:note) { build(:note_on_commit, project: project) }
end
end
+
+ context 'CE restriction for issue assignees' do
+ describe '/assign' do
+ let(:project) { create(:empty_project) }
+ let(:master) { create(:user).tap { |u| project.team << [u, :master] } }
+ let(:assignee) { create(:user) }
+ let(:master) { create(:user) }
+ let(:service) { described_class.new(project, master) }
+ let(:note) { create(:note_on_issue, note: note_text, project: project) }
+
+ let(:note_text) do
+ %(/assign @#{assignee.username} @#{master.username}\n")
+ end
+
+ before do
+ project.team << [master, :master]
+ project.team << [assignee, :master]
+ end
+
+ it 'adds only one assignee from the list' do
+ _, command_params = service.extract_commands(note)
+ service.execute(command_params, note)
+
+ expect(note.noteable.assignees.count).to eq(1)
+ end
+ end
+ end
end
diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb
index 989fd90cda9..de3bbc6b6a1 100644
--- a/spec/services/notification_service_spec.rb
+++ b/spec/services/notification_service_spec.rb
@@ -4,6 +4,7 @@ describe NotificationService, services: true do
include EmailHelpers
let(:notification) { NotificationService.new }
+ let(:assignee) { create(:user) }
around(:each) do |example|
perform_enqueued_jobs do
@@ -52,7 +53,11 @@ describe NotificationService, services: true do
shared_examples 'participating by assignee notification' do
it 'emails the participant' do
- issuable.update_attribute(:assignee, participant)
+ if issuable.is_a?(Issue)
+ issuable.assignees << participant
+ else
+ issuable.update_attribute(:assignee, participant)
+ end
notification_trigger
@@ -103,14 +108,14 @@ describe NotificationService, services: true do
describe 'Notes' do
context 'issue note' do
let(:project) { create(:empty_project, :private) }
- let(:issue) { create(:issue, project: project, assignee: create(:user)) }
- let(:mentioned_issue) { create(:issue, assignee: issue.assignee) }
+ let(:issue) { create(:issue, project: project, assignees: [assignee]) }
+ let(:mentioned_issue) { create(:issue, assignees: issue.assignees) }
let(:note) { create(:note_on_issue, noteable: issue, project_id: issue.project_id, note: '@mention referenced, @outsider also') }
before do
build_team(note.project)
project.add_master(issue.author)
- project.add_master(issue.assignee)
+ project.add_master(assignee)
project.add_master(note.author)
create(:note_on_issue, noteable: issue, project_id: issue.project_id, note: '@subscribed_participant cc this guy')
update_custom_notification(:new_note, @u_guest_custom, resource: project)
@@ -130,7 +135,7 @@ describe NotificationService, services: true do
should_email(@u_watcher)
should_email(note.noteable.author)
- should_email(note.noteable.assignee)
+ should_email(note.noteable.assignees.first)
should_email(@u_custom_global)
should_email(@u_mentioned)
should_email(@subscriber)
@@ -196,7 +201,7 @@ describe NotificationService, services: true do
notification.new_note(note)
should_email(note.noteable.author)
- should_email(note.noteable.assignee)
+ should_email(note.noteable.assignees.first)
should_email(@u_mentioned)
should_email(@u_custom_global)
should_not_email(@u_guest_custom)
@@ -218,7 +223,7 @@ describe NotificationService, services: true do
let(:member) { create(:user) }
let(:guest) { create(:user) }
let(:admin) { create(:admin) }
- let(:confidential_issue) { create(:issue, :confidential, project: project, author: author, assignee: assignee) }
+ let(:confidential_issue) { create(:issue, :confidential, project: project, author: author, assignees: [assignee]) }
let(:note) { create(:note_on_issue, noteable: confidential_issue, project: project, note: "#{author.to_reference} #{assignee.to_reference} #{non_member.to_reference} #{member.to_reference} #{admin.to_reference}") }
let(:guest_watcher) { create_user_with_notification(:watch, "guest-watcher-confidential") }
@@ -244,8 +249,8 @@ describe NotificationService, services: true do
context 'issue note mention' do
let(:project) { create(:empty_project, :public) }
- let(:issue) { create(:issue, project: project, assignee: create(:user)) }
- let(:mentioned_issue) { create(:issue, assignee: issue.assignee) }
+ let(:issue) { create(:issue, project: project, assignees: [assignee]) }
+ let(:mentioned_issue) { create(:issue, assignees: issue.assignees) }
let(:note) { create(:note_on_issue, noteable: issue, project_id: issue.project_id, note: '@all mentioned') }
before do
@@ -269,7 +274,7 @@ describe NotificationService, services: true do
should_email(@u_guest_watcher)
should_email(note.noteable.author)
- should_email(note.noteable.assignee)
+ should_email(note.noteable.assignees.first)
should_not_email(note.author)
should_email(@u_mentioned)
should_not_email(@u_disabled)
@@ -345,7 +350,7 @@ describe NotificationService, services: true do
create(:note_on_personal_snippet, noteable: snippet, note: 'note', author: @u_participant),
create(:note_on_personal_snippet, noteable: snippet, note: 'note', author: @u_mentioned),
create(:note_on_personal_snippet, noteable: snippet, note: 'note', author: @u_disabled),
- create(:note_on_personal_snippet, noteable: snippet, note: 'note', author: @u_note_author),
+ create(:note_on_personal_snippet, noteable: snippet, note: 'note', author: @u_note_author)
]
end
@@ -449,7 +454,7 @@ describe NotificationService, services: true do
let(:group) { create(:group) }
let(:project) { create(:empty_project, :public, namespace: group) }
let(:another_project) { create(:empty_project, :public, namespace: group) }
- let(:issue) { create :issue, project: project, assignee: create(:user), description: 'cc @participant' }
+ let(:issue) { create :issue, project: project, assignees: [assignee], description: 'cc @participant' }
before do
build_team(issue.project)
@@ -465,7 +470,7 @@ describe NotificationService, services: true do
it do
notification.new_issue(issue, @u_disabled)
- should_email(issue.assignee)
+ should_email(assignee)
should_email(@u_watcher)
should_email(@u_guest_watcher)
should_email(@u_guest_custom)
@@ -480,10 +485,10 @@ describe NotificationService, services: true do
end
it do
- create_global_setting_for(issue.assignee, :mention)
+ create_global_setting_for(issue.assignees.first, :mention)
notification.new_issue(issue, @u_disabled)
- should_not_email(issue.assignee)
+ should_not_email(issue.assignees.first)
end
it "emails the author if they've opted into notifications about their activity" do
@@ -528,7 +533,7 @@ describe NotificationService, services: true do
let(:member) { create(:user) }
let(:guest) { create(:user) }
let(:admin) { create(:admin) }
- let(:confidential_issue) { create(:issue, :confidential, project: project, title: 'Confidential issue', author: author, assignee: assignee) }
+ let(:confidential_issue) { create(:issue, :confidential, project: project, title: 'Confidential issue', author: author, assignees: [assignee]) }
it "emails subscribers of the issue's labels that can read the issue" do
project.add_developer(member)
@@ -572,9 +577,9 @@ describe NotificationService, services: true do
end
it 'emails new assignee' do
- notification.reassigned_issue(issue, @u_disabled)
+ notification.reassigned_issue(issue, @u_disabled, [assignee])
- should_email(issue.assignee)
+ should_email(issue.assignees.first)
should_email(@u_watcher)
should_email(@u_guest_watcher)
should_email(@u_guest_custom)
@@ -588,9 +593,8 @@ describe NotificationService, services: true do
end
it 'emails previous assignee even if he has the "on mention" notif level' do
- issue.update_attribute(:assignee, @u_mentioned)
- issue.update_attributes(assignee: @u_watcher)
- notification.reassigned_issue(issue, @u_disabled)
+ issue.assignees = [@u_mentioned]
+ notification.reassigned_issue(issue, @u_disabled, [@u_watcher])
should_email(@u_mentioned)
should_email(@u_watcher)
@@ -606,11 +610,11 @@ describe NotificationService, services: true do
end
it 'emails new assignee even if he has the "on mention" notif level' do
- issue.update_attributes(assignee: @u_mentioned)
- notification.reassigned_issue(issue, @u_disabled)
+ issue.assignees = [@u_mentioned]
+ notification.reassigned_issue(issue, @u_disabled, [@u_mentioned])
- expect(issue.assignee).to be @u_mentioned
- should_email(issue.assignee)
+ expect(issue.assignees.first).to be @u_mentioned
+ should_email(issue.assignees.first)
should_email(@u_watcher)
should_email(@u_guest_watcher)
should_email(@u_guest_custom)
@@ -624,11 +628,11 @@ describe NotificationService, services: true do
end
it 'emails new assignee' do
- issue.update_attribute(:assignee, @u_mentioned)
- notification.reassigned_issue(issue, @u_disabled)
+ issue.assignees = [@u_mentioned]
+ notification.reassigned_issue(issue, @u_disabled, [@u_mentioned])
- expect(issue.assignee).to be @u_mentioned
- should_email(issue.assignee)
+ expect(issue.assignees.first).to be @u_mentioned
+ should_email(issue.assignees.first)
should_email(@u_watcher)
should_email(@u_guest_watcher)
should_email(@u_guest_custom)
@@ -642,17 +646,17 @@ describe NotificationService, services: true do
end
it 'does not email new assignee if they are the current user' do
- issue.update_attribute(:assignee, @u_mentioned)
- notification.reassigned_issue(issue, @u_mentioned)
+ issue.assignees = [@u_mentioned]
+ notification.reassigned_issue(issue, @u_mentioned, [@u_mentioned])
- expect(issue.assignee).to be @u_mentioned
+ expect(issue.assignees.first).to be @u_mentioned
should_email(@u_watcher)
should_email(@u_guest_watcher)
should_email(@u_guest_custom)
should_email(@u_participant_mentioned)
should_email(@subscriber)
should_email(@u_custom_global)
- should_not_email(issue.assignee)
+ should_not_email(issue.assignees.first)
should_not_email(@unsubscriber)
should_not_email(@u_participating)
should_not_email(@u_disabled)
@@ -662,7 +666,7 @@ describe NotificationService, services: true do
it_behaves_like 'participating notifications' do
let(:participant) { create(:user, username: 'user-participant') }
let(:issuable) { issue }
- let(:notification_trigger) { notification.reassigned_issue(issue, @u_disabled) }
+ let(:notification_trigger) { notification.reassigned_issue(issue, @u_disabled, [assignee]) }
end
end
@@ -705,7 +709,7 @@ describe NotificationService, services: true do
it "doesn't send email to anyone but subscribers of the given labels" do
notification.relabeled_issue(issue, [group_label_2, label_2], @u_disabled)
- should_not_email(issue.assignee)
+ should_not_email(issue.assignees.first)
should_not_email(issue.author)
should_not_email(@u_watcher)
should_not_email(@u_guest_watcher)
@@ -729,7 +733,7 @@ describe NotificationService, services: true do
let(:member) { create(:user) }
let(:guest) { create(:user) }
let(:admin) { create(:admin) }
- let(:confidential_issue) { create(:issue, :confidential, project: project, title: 'Confidential issue', author: author, assignee: assignee) }
+ let(:confidential_issue) { create(:issue, :confidential, project: project, title: 'Confidential issue', author: author, assignees: [assignee]) }
let!(:label_1) { create(:label, project: project, issues: [confidential_issue]) }
let!(:label_2) { create(:label, project: project) }
@@ -767,7 +771,7 @@ describe NotificationService, services: true do
it 'sends email to issue assignee and issue author' do
notification.close_issue(issue, @u_disabled)
- should_email(issue.assignee)
+ should_email(issue.assignees.first)
should_email(issue.author)
should_email(@u_watcher)
should_email(@u_guest_watcher)
@@ -798,7 +802,7 @@ describe NotificationService, services: true do
it 'sends email to issue notification recipients' do
notification.reopen_issue(issue, @u_disabled)
- should_email(issue.assignee)
+ should_email(issue.assignees.first)
should_email(issue.author)
should_email(@u_watcher)
should_email(@u_guest_watcher)
@@ -826,7 +830,7 @@ describe NotificationService, services: true do
it 'sends email to issue notification recipients' do
notification.issue_moved(issue, new_issue, @u_disabled)
- should_email(issue.assignee)
+ should_email(issue.assignees.first)
should_email(issue.author)
should_email(@u_watcher)
should_email(@u_guest_watcher)
diff --git a/spec/services/preview_markdown_service_spec.rb b/spec/services/preview_markdown_service_spec.rb
new file mode 100644
index 00000000000..b2fb5c91313
--- /dev/null
+++ b/spec/services/preview_markdown_service_spec.rb
@@ -0,0 +1,67 @@
+require 'spec_helper'
+
+describe PreviewMarkdownService do
+ let(:user) { create(:user) }
+ let(:project) { create(:empty_project) }
+
+ before do
+ project.add_developer(user)
+ end
+
+ describe 'user references' do
+ let(:params) { { text: "Take a look #{user.to_reference}" } }
+ let(:service) { described_class.new(project, user, params) }
+
+ it 'returns users referenced in text' do
+ result = service.execute
+
+ expect(result[:users]).to eq [user.username]
+ end
+ end
+
+ context 'new note with slash commands' do
+ let(:issue) { create(:issue, project: project) }
+ let(:params) do
+ {
+ text: "Please do it\n/assign #{user.to_reference}",
+ slash_commands_target_type: 'Issue',
+ slash_commands_target_id: issue.id
+ }
+ end
+ let(:service) { described_class.new(project, user, params) }
+
+ it 'removes slash commands from text' do
+ result = service.execute
+
+ expect(result[:text]).to eq 'Please do it'
+ end
+
+ it 'explains slash commands effect' do
+ result = service.execute
+
+ expect(result[:commands]).to eq "Assigns #{user.to_reference}."
+ end
+ end
+
+ context 'merge request description' do
+ let(:params) do
+ {
+ text: "My work\n/estimate 2y",
+ slash_commands_target_type: 'MergeRequest'
+ }
+ end
+ let(:service) { described_class.new(project, user, params) }
+
+ it 'removes slash commands from text' do
+ result = service.execute
+
+ expect(result[:text]).to eq 'My work'
+ end
+
+ it 'explains slash commands effect' do
+ result = service.execute
+
+ expect(result[:commands]).to eq 'Sets time estimate to 2y.'
+ end
+ end
+end
diff --git a/spec/services/projects/autocomplete_service_spec.rb b/spec/services/projects/autocomplete_service_spec.rb
index 7916c2d957c..c198c3eedfc 100644
--- a/spec/services/projects/autocomplete_service_spec.rb
+++ b/spec/services/projects/autocomplete_service_spec.rb
@@ -11,7 +11,7 @@ describe Projects::AutocompleteService, services: true do
let(:project) { create(:empty_project, :public) }
let!(:issue) { create(:issue, project: project, title: 'Issue 1') }
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) }
+ let!(:security_issue_2) { create(:issue, :confidential, title: 'Security issue 2', project: project, assignees: [assignee]) }
it 'does not list project confidential issues for guests' do
autocomplete = described_class.new(project, nil)
diff --git a/spec/services/projects/create_service_spec.rb b/spec/services/projects/create_service_spec.rb
index 7a07ea618c0..033e6ecd18c 100644
--- a/spec/services/projects/create_service_spec.rb
+++ b/spec/services/projects/create_service_spec.rb
@@ -27,6 +27,22 @@ describe Projects::CreateService, '#execute', services: true do
end
end
+ context "admin creates project with other user's namespace_id" do
+ it 'sets the correct permissions' do
+ admin = create(:admin)
+ opts = {
+ name: 'GitLab',
+ namespace_id: user.namespace.id
+ }
+ project = create_project(admin, opts)
+
+ expect(project).to be_persisted
+ expect(project.owner).to eq(user)
+ expect(project.team.masters).to include(user, admin)
+ expect(project.namespace).to eq(user.namespace)
+ end
+ end
+
context 'group namespace' do
let(:group) do
create(:group).tap do |group|
diff --git a/spec/services/projects/enable_deploy_key_service_spec.rb b/spec/services/projects/enable_deploy_key_service_spec.rb
index a37510cf159..78626fbad4b 100644
--- a/spec/services/projects/enable_deploy_key_service_spec.rb
+++ b/spec/services/projects/enable_deploy_key_service_spec.rb
@@ -21,6 +21,16 @@ describe Projects::EnableDeployKeyService, services: true do
end
end
+ context 'add the same key twice' do
+ before do
+ project.deploy_keys << deploy_key
+ end
+
+ it 'returns existing key' do
+ expect(service.execute).to eq(deploy_key)
+ end
+ end
+
def service
Projects::EnableDeployKeyService.new(project, user, params)
end
diff --git a/spec/services/projects/housekeeping_service_spec.rb b/spec/services/projects/housekeeping_service_spec.rb
index eaf63457b32..fff12beed71 100644
--- a/spec/services/projects/housekeeping_service_spec.rb
+++ b/spec/services/projects/housekeeping_service_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe Projects::HousekeepingService do
- subject { Projects::HousekeepingService.new(project) }
+ subject { described_class.new(project) }
let(:project) { create(:project, :repository) }
before do
diff --git a/spec/services/projects/participants_service_spec.rb b/spec/services/projects/participants_service_spec.rb
index 063b3bd76eb..0657b7e93fe 100644
--- a/spec/services/projects/participants_service_spec.rb
+++ b/spec/services/projects/participants_service_spec.rb
@@ -6,7 +6,6 @@ describe Projects::ParticipantsService, services: true do
let(:project) { create(:empty_project, :public) }
let(:group) { create(:group, avatar: fixture_file_upload(Rails.root + 'spec/fixtures/dk.png')) }
let(:user) { create(:user) }
- let(:base_url) { Settings.send(:build_base_gitlab_url) }
let!(:group_member) { create(:group_member, group: group, user: user) }
it 'should return an url for the avatar' do
@@ -14,7 +13,7 @@ describe Projects::ParticipantsService, services: true do
groups = participants.groups
expect(groups.size).to eq 1
- expect(groups.first[:avatar_url]).to eq "#{base_url}/uploads/group/avatar/#{group.id}/dk.png"
+ expect(groups.first[:avatar_url]).to eq("/uploads/group/avatar/#{group.id}/dk.png")
end
it 'should return an url for the avatar with relative url' do
@@ -25,7 +24,7 @@ describe Projects::ParticipantsService, services: true do
groups = participants.groups
expect(groups.size).to eq 1
- expect(groups.first[:avatar_url]).to eq "#{base_url}/gitlab/uploads/group/avatar/#{group.id}/dk.png"
+ expect(groups.first[:avatar_url]).to eq("/gitlab/uploads/group/avatar/#{group.id}/dk.png")
end
end
end
diff --git a/spec/services/projects/propagate_service_template_spec.rb b/spec/services/projects/propagate_service_template_spec.rb
new file mode 100644
index 00000000000..90eff3bbc1e
--- /dev/null
+++ b/spec/services/projects/propagate_service_template_spec.rb
@@ -0,0 +1,103 @@
+require 'spec_helper'
+
+describe Projects::PropagateServiceTemplate, services: true do
+ describe '.propagate' do
+ let!(:service_template) do
+ PushoverService.create(
+ template: true,
+ active: true,
+ properties: {
+ device: 'MyDevice',
+ sound: 'mic',
+ priority: 4,
+ user_key: 'asdf',
+ api_key: '123456789'
+ })
+ end
+
+ let!(:project) { create(:empty_project) }
+
+ it 'creates services for projects' do
+ expect(project.pushover_service).to be_nil
+
+ described_class.propagate(service_template)
+
+ expect(project.reload.pushover_service).to be_present
+ end
+
+ it 'creates services for a project that has another service' do
+ BambooService.create(
+ template: true,
+ active: true,
+ project: project,
+ properties: {
+ bamboo_url: 'http://gitlab.com',
+ username: 'mic',
+ password: "password",
+ build_key: 'build'
+ }
+ )
+
+ expect(project.pushover_service).to be_nil
+
+ described_class.propagate(service_template)
+
+ expect(project.reload.pushover_service).to be_present
+ end
+
+ it 'does not create the service if it exists already' do
+ other_service = BambooService.create(
+ template: true,
+ active: true,
+ properties: {
+ bamboo_url: 'http://gitlab.com',
+ username: 'mic',
+ password: "password",
+ build_key: 'build'
+ }
+ )
+
+ Service.build_from_template(project.id, service_template).save!
+ Service.build_from_template(project.id, other_service).save!
+
+ expect { described_class.propagate(service_template) }.
+ not_to change { Service.count }
+ end
+
+ it 'creates the service containing the template attributes' do
+ described_class.propagate(service_template)
+
+ expect(project.pushover_service.properties).to eq(service_template.properties)
+ end
+
+ describe 'bulk update' do
+ it 'creates services for all projects' do
+ project_total = 5
+ stub_const 'Projects::PropagateServiceTemplate::BATCH_SIZE', 3
+
+ project_total.times { create(:empty_project) }
+
+ expect { described_class.propagate(service_template) }.
+ to change { Service.count }.by(project_total + 1)
+ end
+ end
+
+ describe 'external tracker' do
+ it 'updates the project external tracker' do
+ service_template.update!(category: 'issue_tracker', default: false)
+
+ expect { described_class.propagate(service_template) }.
+ to change { project.reload.has_external_issue_tracker }.to(true)
+ end
+ end
+
+ describe 'external wiki' do
+ it 'updates the project external tracker' do
+ service_template.update!(type: 'ExternalWikiService')
+
+ expect { described_class.propagate(service_template) }.
+ to change { project.reload.has_external_wiki }.to(true)
+ end
+ end
+ end
+end
diff --git a/spec/services/slash_commands/interpret_service_spec.rb b/spec/services/slash_commands/interpret_service_spec.rb
index a63281f0eab..e5e400ee281 100644
--- a/spec/services/slash_commands/interpret_service_spec.rb
+++ b/spec/services/slash_commands/interpret_service_spec.rb
@@ -3,6 +3,7 @@ require 'spec_helper'
describe SlashCommands::InterpretService, services: true do
let(:project) { create(:empty_project, :public) }
let(:developer) { create(:user) }
+ let(:developer2) { 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') }
@@ -42,23 +43,6 @@ describe SlashCommands::InterpretService, services: true do
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: developer.id)
- end
- end
-
- shared_examples 'unassign command' do
- it 'populates assignee_id: nil if content contains /unassign' do
- issuable.update(assignee_id: developer.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
@@ -70,7 +54,7 @@ describe SlashCommands::InterpretService, services: true do
shared_examples 'remove_milestone command' do
it 'populates milestone_id: nil if content contains /remove_milestone' do
- issuable.update(milestone_id: milestone.id)
+ issuable.update!(milestone_id: milestone.id)
_, updates = service.execute(content, issuable)
expect(updates).to eq(milestone_id: nil)
@@ -108,7 +92,7 @@ describe SlashCommands::InterpretService, services: true do
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
+ issuable.update!(label_ids: [inprogress.id]) # populate the label
_, updates = service.execute(content, issuable)
expect(updates).to eq(remove_label_ids: [inprogress.id])
@@ -117,7 +101,7 @@ describe SlashCommands::InterpretService, services: true do
shared_examples 'multiple unlabel command' do
it 'fetches label ids and populates remove_label_ids if content contains mutiple /unlabel' do
- issuable.update(label_ids: [inprogress.id, bug.id]) # populate the label
+ issuable.update!(label_ids: [inprogress.id, bug.id]) # populate the label
_, updates = service.execute(content, issuable)
expect(updates).to eq(remove_label_ids: [inprogress.id, bug.id])
@@ -126,7 +110,7 @@ describe SlashCommands::InterpretService, services: true do
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
+ issuable.update!(label_ids: [inprogress.id]) # populate the label
_, updates = service.execute(content, issuable)
expect(updates).to eq(label_ids: [])
@@ -135,7 +119,7 @@ describe SlashCommands::InterpretService, services: true do
shared_examples 'relabel command' do
it 'populates label_ids: [] if content contains /relabel' do
- issuable.update(label_ids: [bug.id]) # populate the label
+ issuable.update!(label_ids: [bug.id]) # populate the label
inprogress # populate the label
_, updates = service.execute(content, issuable)
@@ -187,7 +171,7 @@ describe SlashCommands::InterpretService, services: true do
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)
+ issuable.update!(due_date: Date.today)
_, updates = service.execute(content, issuable)
expect(updates).to eq(due_date: nil)
@@ -204,7 +188,7 @@ describe SlashCommands::InterpretService, services: true do
shared_examples 'unwip command' do
it 'returns wip_event: "unwip" if content contains /wip' do
- issuable.update(title: issuable.wip_title)
+ issuable.update!(title: issuable.wip_title)
_, updates = service.execute(content, issuable)
expect(updates).to eq(wip_event: 'unwip')
@@ -371,14 +355,46 @@ describe SlashCommands::InterpretService, services: true do
let(:issuable) { issue }
end
- it_behaves_like 'assign command' do
+ context 'assign command' do
let(:content) { "/assign @#{developer.username}" }
- let(:issuable) { issue }
+
+ context 'Issue' do
+ it 'fetches assignee and populates assignee_id if content contains /assign' do
+ _, updates = service.execute(content, issue)
+
+ expect(updates).to eq(assignee_ids: [developer.id])
+ end
+ end
+
+ context 'Merge Request' do
+ it 'fetches assignee and populates assignee_id if content contains /assign' do
+ _, updates = service.execute(content, merge_request)
+
+ expect(updates).to eq(assignee_id: developer.id)
+ end
+ end
end
- it_behaves_like 'assign command' do
- let(:content) { "/assign @#{developer.username}" }
- let(:issuable) { merge_request }
+ context 'assign command with multiple assignees' do
+ let(:content) { "/assign @#{developer.username} @#{developer2.username}" }
+
+ before{ project.team << [developer2, :developer] }
+
+ context 'Issue' do
+ it 'fetches assignee and populates assignee_id if content contains /assign' do
+ _, updates = service.execute(content, issue)
+
+ expect(updates[:assignee_ids]).to match_array([developer.id, developer2.id])
+ end
+ end
+
+ context 'Merge Request' do
+ it 'fetches assignee and populates assignee_id if content contains /assign' do
+ _, updates = service.execute(content, merge_request)
+
+ expect(updates).to eq(assignee_id: developer.id)
+ end
+ end
end
it_behaves_like 'empty command' do
@@ -391,14 +407,26 @@ describe SlashCommands::InterpretService, services: true do
let(:issuable) { issue }
end
- it_behaves_like 'unassign command' do
+ context 'unassign command' do
let(:content) { '/unassign' }
- let(:issuable) { issue }
- end
- it_behaves_like 'unassign command' do
- let(:content) { '/unassign' }
- let(:issuable) { merge_request }
+ context 'Issue' do
+ it 'populates assignee_ids: [] if content contains /unassign' do
+ issue.update(assignee_ids: [developer.id])
+ _, updates = service.execute(content, issue)
+
+ expect(updates).to eq(assignee_ids: [])
+ end
+ end
+
+ context 'Merge Request' do
+ it 'populates assignee_id: nil if content contains /unassign' do
+ merge_request.update(assignee_id: developer.id)
+ _, updates = service.execute(content, merge_request)
+
+ expect(updates).to eq(assignee_id: nil)
+ end
+ end
end
it_behaves_like 'milestone command' do
@@ -727,5 +755,282 @@ describe SlashCommands::InterpretService, services: true do
end
end
end
+
+ context '/board_move command' do
+ let(:todo) { create(:label, project: project, title: 'To Do') }
+ let(:inreview) { create(:label, project: project, title: 'In Review') }
+ let(:content) { %{/board_move ~"#{inreview.title}"} }
+
+ let!(:board) { create(:board, project: project) }
+ let!(:todo_list) { create(:list, board: board, label: todo) }
+ let!(:inreview_list) { create(:list, board: board, label: inreview) }
+ let!(:inprogress_list) { create(:list, board: board, label: inprogress) }
+
+ it 'populates remove_label_ids for all current board columns' do
+ issue.update!(label_ids: [todo.id, inprogress.id])
+
+ _, updates = service.execute(content, issue)
+
+ expect(updates[:remove_label_ids]).to match_array([todo.id, inprogress.id])
+ end
+
+ it 'populates add_label_ids with the id of the given label' do
+ _, updates = service.execute(content, issue)
+
+ expect(updates[:add_label_ids]).to eq([inreview.id])
+ end
+
+ it 'does not include the given label id in remove_label_ids' do
+ issue.update!(label_ids: [todo.id, inreview.id])
+
+ _, updates = service.execute(content, issue)
+
+ expect(updates[:remove_label_ids]).to match_array([todo.id])
+ end
+
+ it 'does not remove label ids that are not lists on the board' do
+ issue.update!(label_ids: [todo.id, bug.id])
+
+ _, updates = service.execute(content, issue)
+
+ expect(updates[:remove_label_ids]).to match_array([todo.id])
+ end
+
+ context 'if the project has multiple boards' do
+ let(:issuable) { issue }
+ before { create(:board, project: project) }
+ it_behaves_like 'empty command'
+ end
+
+ context 'if the given label does not exist' do
+ let(:issuable) { issue }
+ let(:content) { '/board_move ~"Fake Label"' }
+ it_behaves_like 'empty command'
+ end
+
+ context 'if multiple labels are given' do
+ let(:issuable) { issue }
+ let(:content) { %{/board_move ~"#{inreview.title}" ~"#{todo.title}"} }
+ it_behaves_like 'empty command'
+ end
+
+ context 'if the given label is not a list on the board' do
+ let(:issuable) { issue }
+ let(:content) { %{/board_move ~"#{bug.title}"} }
+ it_behaves_like 'empty command'
+ end
+
+ context 'if issuable is not an Issue' do
+ let(:issuable) { merge_request }
+ it_behaves_like 'empty command'
+ end
+ end
+ end
+
+ describe '#explain' do
+ let(:service) { described_class.new(project, developer) }
+ let(:merge_request) { create(:merge_request, source_project: project) }
+
+ describe 'close command' do
+ let(:content) { '/close' }
+
+ it 'includes issuable name' do
+ _, explanations = service.explain(content, issue)
+
+ expect(explanations).to eq(['Closes this issue.'])
+ end
+ end
+
+ describe 'reopen command' do
+ let(:content) { '/reopen' }
+ let(:merge_request) { create(:merge_request, :closed, source_project: project) }
+
+ it 'includes issuable name' do
+ _, explanations = service.explain(content, merge_request)
+
+ expect(explanations).to eq(['Reopens this merge request.'])
+ end
+ end
+
+ describe 'title command' do
+ let(:content) { '/title This is new title' }
+
+ it 'includes new title' do
+ _, explanations = service.explain(content, issue)
+
+ expect(explanations).to eq(['Changes the title to "This is new title".'])
+ end
+ end
+
+ describe 'assign command' do
+ let(:content) { "/assign @#{developer.username} do it!" }
+
+ it 'includes only the user reference' do
+ _, explanations = service.explain(content, merge_request)
+
+ expect(explanations).to eq(["Assigns @#{developer.username}."])
+ end
+ end
+
+ describe 'unassign command' do
+ let(:content) { '/unassign' }
+ let(:issue) { create(:issue, project: project, assignees: [developer]) }
+
+ it 'includes current assignee reference' do
+ _, explanations = service.explain(content, issue)
+
+ expect(explanations).to eq(["Removes assignee @#{developer.username}."])
+ end
+ end
+
+ describe 'milestone command' do
+ let(:content) { '/milestone %wrong-milestone' }
+ let!(:milestone) { create(:milestone, project: project, title: '9.10') }
+
+ it 'is empty when milestone reference is wrong' do
+ _, explanations = service.explain(content, issue)
+
+ expect(explanations).to eq([])
+ end
+ end
+
+ describe 'remove milestone command' do
+ let(:content) { '/remove_milestone' }
+ let(:merge_request) { create(:merge_request, source_project: project, milestone: milestone) }
+
+ it 'includes current milestone name' do
+ _, explanations = service.explain(content, merge_request)
+
+ expect(explanations).to eq(['Removes %"9.10" milestone.'])
+ end
+ end
+
+ describe 'label command' do
+ let(:content) { '/label ~missing' }
+ let!(:label) { create(:label, project: project) }
+
+ it 'is empty when there are no correct labels' do
+ _, explanations = service.explain(content, issue)
+
+ expect(explanations).to eq([])
+ end
+ end
+
+ describe 'unlabel command' do
+ let(:content) { '/unlabel' }
+
+ it 'says all labels if no parameter provided' do
+ merge_request.update!(label_ids: [bug.id])
+ _, explanations = service.explain(content, merge_request)
+
+ expect(explanations).to eq(['Removes all labels.'])
+ end
+ end
+
+ describe 'relabel command' do
+ let(:content) { '/relabel Bug' }
+ let!(:bug) { create(:label, project: project, title: 'Bug') }
+ let(:feature) { create(:label, project: project, title: 'Feature') }
+
+ it 'includes label name' do
+ issue.update!(label_ids: [feature.id])
+ _, explanations = service.explain(content, issue)
+
+ expect(explanations).to eq(["Replaces all labels with ~#{bug.id} label."])
+ end
+ end
+
+ describe 'subscribe command' do
+ let(:content) { '/subscribe' }
+
+ it 'includes issuable name' do
+ _, explanations = service.explain(content, issue)
+
+ expect(explanations).to eq(['Subscribes to this issue.'])
+ end
+ end
+
+ describe 'unsubscribe command' do
+ let(:content) { '/unsubscribe' }
+
+ it 'includes issuable name' do
+ merge_request.subscribe(developer, project)
+ _, explanations = service.explain(content, merge_request)
+
+ expect(explanations).to eq(['Unsubscribes from this merge request.'])
+ end
+ end
+
+ describe 'due command' do
+ let(:content) { '/due April 1st 2016' }
+
+ it 'includes the date' do
+ _, explanations = service.explain(content, issue)
+
+ expect(explanations).to eq(['Sets the due date to Apr 1, 2016.'])
+ end
+ end
+
+ describe 'wip command' do
+ let(:content) { '/wip' }
+
+ it 'includes the new status' do
+ _, explanations = service.explain(content, merge_request)
+
+ expect(explanations).to eq(['Marks this merge request as Work In Progress.'])
+ end
+ end
+
+ describe 'award command' do
+ let(:content) { '/award :confetti_ball: ' }
+
+ it 'includes the emoji' do
+ _, explanations = service.explain(content, issue)
+
+ expect(explanations).to eq(['Toggles :confetti_ball: emoji award.'])
+ end
+ end
+
+ describe 'estimate command' do
+ let(:content) { '/estimate 79d' }
+
+ it 'includes the formatted duration' do
+ _, explanations = service.explain(content, merge_request)
+
+ expect(explanations).to eq(['Sets time estimate to 3mo 3w 4d.'])
+ end
+ end
+
+ describe 'spend command' do
+ let(:content) { '/spend -120m' }
+
+ it 'includes the formatted duration and proper verb' do
+ _, explanations = service.explain(content, issue)
+
+ expect(explanations).to eq(['Substracts 2h spent time.'])
+ end
+ end
+
+ describe 'target branch command' do
+ let(:content) { '/target_branch my-feature ' }
+
+ it 'includes the branch name' do
+ _, explanations = service.explain(content, merge_request)
+
+ expect(explanations).to eq(['Sets target branch to my-feature.'])
+ end
+ end
+
+ describe 'board move command' do
+ let(:content) { '/board_move ~bug' }
+ let!(:bug) { create(:label, project: project, title: 'bug') }
+ let!(:board) { create(:board, project: project) }
+
+ it 'includes the label name' do
+ _, explanations = service.explain(content, issue)
+
+ expect(explanations).to eq(["Moves issue to ~#{bug.id} column in the board."])
+ end
+ end
end
end
diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb
index 42d63a9f9ba..7a9cd7553b1 100644
--- a/spec/services/system_note_service_spec.rb
+++ b/spec/services/system_note_service_spec.rb
@@ -6,6 +6,7 @@ describe SystemNoteService, services: true do
let(:project) { create(:empty_project) }
let(:author) { create(:user) }
let(:noteable) { create(:issue, project: project) }
+ let(:issue) { noteable }
shared_examples_for 'a system note' do
let(:expected_noteable) { noteable }
@@ -155,6 +156,52 @@ describe SystemNoteService, services: true do
end
end
+ describe '.change_issue_assignees' do
+ subject { described_class.change_issue_assignees(noteable, project, author, [assignee]) }
+
+ let(:assignee) { create(:user) }
+ let(:assignee1) { create(:user) }
+ let(:assignee2) { create(:user) }
+ let(:assignee3) { create(:user) }
+
+ it_behaves_like 'a system note' do
+ let(:action) { 'assignee' }
+ end
+
+ def build_note(old_assignees, new_assignees)
+ issue.assignees = new_assignees
+ described_class.change_issue_assignees(issue, project, author, old_assignees).note
+ end
+
+ it 'builds a correct phrase when an assignee is added to a non-assigned issue' do
+ expect(build_note([], [assignee1])).to eq "assigned to @#{assignee1.username}"
+ end
+
+ it 'builds a correct phrase when assignee removed' do
+ expect(build_note([assignee1], [])).to eq 'removed assignee'
+ end
+
+ it 'builds a correct phrase when assignees changed' do
+ expect(build_note([assignee1], [assignee2])).to eq \
+ "assigned to @#{assignee2.username} and unassigned @#{assignee1.username}"
+ end
+
+ it 'builds a correct phrase when three assignees removed and one added' do
+ expect(build_note([assignee, assignee1, assignee2], [assignee3])).to eq \
+ "assigned to @#{assignee3.username} and unassigned @#{assignee.username}, @#{assignee1.username}, and @#{assignee2.username}"
+ end
+
+ it 'builds a correct phrase when one assignee changed from a set' do
+ expect(build_note([assignee, assignee1], [assignee, assignee2])).to eq \
+ "assigned to @#{assignee2.username} and unassigned @#{assignee1.username}"
+ end
+
+ it 'builds a correct phrase when one assignee removed from a set' do
+ expect(build_note([assignee, assignee1, assignee2], [assignee, assignee1])).to eq \
+ "unassigned @#{assignee2.username}"
+ end
+ end
+
describe '.change_label' do
subject { described_class.change_label(noteable, project, author, added, removed) }
@@ -292,6 +339,20 @@ describe SystemNoteService, services: true do
end
end
+ describe '.change_description' do
+ subject { described_class.change_description(noteable, project, author) }
+
+ context 'when noteable responds to `description`' do
+ it_behaves_like 'a system note' do
+ let(:action) { 'description' }
+ end
+
+ it 'sets the note text' do
+ expect(subject.note).to eq('changed the description')
+ end
+ end
+ end
+
describe '.change_issue_confidentiality' do
subject { described_class.change_issue_confidentiality(noteable, project, author) }
@@ -595,7 +656,7 @@ describe SystemNoteService, services: true do
end
shared_examples 'cross project mentionable' do
- include GitlabMarkdownHelper
+ include MarkupHelper
it 'contains cross reference to new noteable' do
expect(subject.note).to include cross_project_reference(new_project, new_noteable)
diff --git a/spec/services/todo_service_spec.rb b/spec/services/todo_service_spec.rb
index 89b3b6aad10..175a42a32d9 100644
--- a/spec/services/todo_service_spec.rb
+++ b/spec/services/todo_service_spec.rb
@@ -25,11 +25,11 @@ describe TodoService, services: true do
end
describe 'Issues' do
- let(:issue) { create(:issue, project: project, assignee: john_doe, author: author, description: "- [ ] Task 1\n- [ ] Task 2 #{mentions}") }
- let(:addressed_issue) { create(:issue, project: project, assignee: john_doe, author: author, description: "#{directly_addressed}\n- [ ] Task 1\n- [ ] Task 2") }
- let(:unassigned_issue) { create(:issue, project: project, assignee: nil) }
- let(:confidential_issue) { create(:issue, :confidential, project: project, author: author, assignee: assignee, description: mentions) }
- let(:addressed_confident_issue) { create(:issue, :confidential, project: project, author: author, assignee: assignee, description: directly_addressed) }
+ let(:issue) { create(:issue, project: project, assignees: [john_doe], author: author, description: "- [ ] Task 1\n- [ ] Task 2 #{mentions}") }
+ let(:addressed_issue) { create(:issue, project: project, assignees: [john_doe], author: author, description: "#{directly_addressed}\n- [ ] Task 1\n- [ ] Task 2") }
+ let(:unassigned_issue) { create(:issue, project: project, assignees: []) }
+ let(:confidential_issue) { create(:issue, :confidential, project: project, author: author, assignees: [assignee], description: mentions) }
+ let(:addressed_confident_issue) { create(:issue, :confidential, project: project, author: author, assignees: [assignee], description: directly_addressed) }
describe '#new_issue' do
it 'creates a todo if assigned' do
@@ -43,7 +43,7 @@ describe TodoService, services: true do
end
it 'creates a todo if assignee is the current user' do
- unassigned_issue.update_attribute(:assignee, john_doe)
+ unassigned_issue.assignees = [john_doe]
service.new_issue(unassigned_issue, john_doe)
should_create_todo(user: john_doe, target: unassigned_issue, author: john_doe, action: Todo::ASSIGNED)
@@ -258,20 +258,20 @@ describe TodoService, services: true do
describe '#reassigned_issue' do
it 'creates a pending todo for new assignee' do
- unassigned_issue.update_attribute(:assignee, john_doe)
+ unassigned_issue.assignees << john_doe
service.reassigned_issue(unassigned_issue, author)
should_create_todo(user: john_doe, target: unassigned_issue, action: Todo::ASSIGNED)
end
it 'does not create a todo if unassigned' do
- issue.update_attribute(:assignee, nil)
+ issue.assignees.destroy_all
should_not_create_any_todo { service.reassigned_issue(issue, author) }
end
it 'creates a todo if new assignee is the current user' do
- unassigned_issue.update_attribute(:assignee, john_doe)
+ unassigned_issue.assignees << john_doe
service.reassigned_issue(unassigned_issue, john_doe)
should_create_todo(user: john_doe, target: unassigned_issue, author: john_doe, action: Todo::ASSIGNED)
@@ -361,7 +361,7 @@ describe TodoService, services: true do
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) }
- let(:confidential_issue) { create(:issue, :confidential, project: project, author: author, assignee: assignee) }
+ let(:confidential_issue) { create(:issue, :confidential, project: project, author: author, assignees: [assignee]) }
let(:note) { create(:note, project: project, noteable: issue, author: john_doe, note: mentions) }
let(:addressed_note) { create(:note, project: project, noteable: issue, author: john_doe, note: directly_addressed) }
let(:note_on_commit) { create(:note_on_commit, project: project, author: john_doe, note: mentions) }
@@ -854,7 +854,7 @@ describe TodoService, services: true do
end
it 'updates cached counts when a todo is created' do
- issue = create(:issue, project: project, assignee: john_doe, author: author, description: mentions)
+ issue = create(:issue, project: project, assignees: [john_doe], author: author, description: mentions)
expect(john_doe.todos_pending_count).to eq(0)
expect(john_doe).to receive(:update_todos_count_cache).and_call_original
@@ -866,8 +866,8 @@ describe TodoService, services: true do
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) }
+ let(:issue) { create(:issue, project: project, author: author, assignees: [john_doe]) }
+ let(:another_issue) { create(:issue, project: project, author: author, assignees: [john_doe]) }
it 'marks a relation of todos as done' do
create(:todo, :mentioned, user: john_doe, target: issue, project: project)
diff --git a/spec/services/projects/upload_service_spec.rb b/spec/services/upload_service_spec.rb
index d2cefa46bfa..95ba28dbecd 100644
--- a/spec/services/projects/upload_service_spec.rb
+++ b/spec/services/upload_service_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Projects::UploadService, services: true do
+describe UploadService, services: true do
describe 'File service' do
before do
@user = create(:user)
@@ -68,6 +68,6 @@ describe Projects::UploadService, services: true do
end
def upload_file(project, file)
- Projects::UploadService.new(project, file).execute
+ described_class.new(project, file, FileUploader).execute
end
end
diff --git a/spec/services/users/destroy_service_spec.rb b/spec/services/users/destroy_service_spec.rb
index 4bc30018ebd..de37a61e388 100644
--- a/spec/services/users/destroy_service_spec.rb
+++ b/spec/services/users/destroy_service_spec.rb
@@ -47,7 +47,7 @@ describe Users::DestroyService, services: true do
end
context "for an issue the user was assigned to" do
- let!(:issue) { create(:issue, project: project, assignee: user) }
+ let!(:issue) { create(:issue, project: project, assignees: [user]) }
before do
service.execute(user)
@@ -60,7 +60,7 @@ describe Users::DestroyService, services: true do
it 'migrates the issue so that it is "Unassigned"' do
migrated_issue = Issue.find_by_id(issue.id)
- expect(migrated_issue.assignee).to be_nil
+ expect(migrated_issue.assignees).to be_empty
end
end
end
diff --git a/spec/services/users/migrate_to_ghost_user_service_spec.rb b/spec/services/users/migrate_to_ghost_user_service_spec.rb
index 8c5b7e41c15..9e1edf1ac30 100644
--- a/spec/services/users/migrate_to_ghost_user_service_spec.rb
+++ b/spec/services/users/migrate_to_ghost_user_service_spec.rb
@@ -60,5 +60,23 @@ describe Users::MigrateToGhostUserService, services: true do
end
end
end
+
+ context "when record migration fails with a rollback exception" do
+ before do
+ expect_any_instance_of(MergeRequest::ActiveRecord_Associations_CollectionProxy)
+ .to receive(:update_all).and_raise(ActiveRecord::Rollback)
+ end
+
+ context "for records that were already migrated" do
+ let!(:issue) { create(:issue, project: project, author: user) }
+ let!(:merge_request) { create(:merge_request, source_project: project, author: user, target_branch: "first") }
+
+ it "reverses the migration" do
+ service.execute
+
+ expect(issue.reload.author).to eq(user)
+ end
+ end
+ end
end
end
diff --git a/spec/sidekiq/cron/job_gem_dependency_spec.rb b/spec/sidekiq/cron/job_gem_dependency_spec.rb
new file mode 100644
index 00000000000..2e30cf025b0
--- /dev/null
+++ b/spec/sidekiq/cron/job_gem_dependency_spec.rb
@@ -0,0 +1,18 @@
+require 'spec_helper'
+
+describe Sidekiq::Cron::Job do
+ describe 'cron jobs' do
+ context 'when rufus-scheduler depends on ZoTime or EoTime' do
+ before do
+ described_class
+ .create(name: 'TestCronWorker',
+ cron: Settings.cron_jobs[:pipeline_schedule_worker]['cron'],
+ class: Settings.cron_jobs[:pipeline_schedule_worker]['job_class'])
+ end
+
+ it 'does not get "Rufus::Scheduler::ZoTime/EtOrbi::EoTime into an exact number"' do
+ expect { described_class.all.first.should_enque?(Time.now) }.not_to raise_error
+ end
+ end
+ end
+end
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index e67ad8f3455..e2d5928e5b2 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -13,8 +13,9 @@ rspec_profiling_is_configured =
ENV['RSPEC_PROFILING_POSTGRES_URL'] ||
ENV['RSPEC_PROFILING']
branch_can_be_profiled =
- ENV['CI_COMMIT_REF_NAME'] == 'master' ||
- ENV['CI_COMMIT_REF_NAME'] =~ /rspec-profile/
+ ENV['GITLAB_DATABASE'] == 'postgresql' &&
+ (ENV['CI_COMMIT_REF_NAME'] == 'master' ||
+ ENV['CI_COMMIT_REF_NAME'] =~ /rspec-profile/)
if rspec_profiling_is_configured && (!ENV.key?('CI') || branch_can_be_profiled)
require 'rspec_profiling/rspec'
diff --git a/spec/support/cycle_analytics_helpers.rb b/spec/support/cycle_analytics_helpers.rb
index 8ad042f5e3b..66545127a44 100644
--- a/spec/support/cycle_analytics_helpers.rb
+++ b/spec/support/cycle_analytics_helpers.rb
@@ -20,7 +20,7 @@ module CycleAnalyticsHelpers
ref: 'refs/heads/master').execute
end
- def create_merge_request_closing_issue(issue, message: nil, source_branch: nil)
+ def create_merge_request_closing_issue(issue, message: nil, source_branch: nil, commit_message: 'commit message')
if !source_branch || project.repository.commit(source_branch).blank?
source_branch = generate(:branch)
project.repository.add_branch(user, source_branch, 'master')
@@ -30,7 +30,7 @@ module CycleAnalyticsHelpers
user,
generate(:branch),
'content',
- message: 'commit message',
+ message: commit_message,
branch_name: source_branch)
project.repository.commit(sha)
diff --git a/spec/support/fake_migration_classes.rb b/spec/support/fake_migration_classes.rb
new file mode 100644
index 00000000000..3de0460c3ca
--- /dev/null
+++ b/spec/support/fake_migration_classes.rb
@@ -0,0 +1,3 @@
+class FakeRenameReservedPathMigrationV1 < ActiveRecord::Migration
+ include Gitlab::Database::RenameReservedPathsMigration::V1
+end
diff --git a/spec/support/features/issuable_slash_commands_shared_examples.rb b/spec/support/features/issuable_slash_commands_shared_examples.rb
index a4713e53f63..ad46b163cd6 100644
--- a/spec/support/features/issuable_slash_commands_shared_examples.rb
+++ b/spec/support/features/issuable_slash_commands_shared_examples.rb
@@ -3,7 +3,6 @@
shared_examples 'issuable record that supports slash commands in its description and notes' do |issuable_type|
include SlashCommandsHelpers
- include WaitForAjax
let(:master) { create(:user) }
let(:assignee) { create(:user, username: 'bob') }
@@ -26,7 +25,7 @@ shared_examples 'issuable record that supports slash commands in its description
wait_for_ajax
end
- describe "new #{issuable_type}" do
+ describe "new #{issuable_type}", js: true 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)
@@ -45,7 +44,7 @@ shared_examples 'issuable record that supports slash commands in its description
end
end
- describe "note on #{issuable_type}" do
+ describe "note on #{issuable_type}", js: true do
before do
visit public_send("namespace_project_#{issuable_type}_path", project.namespace, project, issuable)
end
@@ -59,11 +58,12 @@ shared_examples 'issuable record that supports slash commands in its description
expect(page).not_to have_content '/label ~bug'
expect(page).not_to have_content '/milestone %"ASAP"'
+ wait_for_ajax
issuable.reload
note = issuable.notes.user.first
expect(note.note).to eq "Awesome!"
- expect(issuable.assignee).to eq assignee
+ expect(issuable.assignees).to eq [assignee]
expect(issuable.labels).to eq [label_bug]
expect(issuable.milestone).to eq milestone
end
@@ -81,7 +81,7 @@ shared_examples 'issuable record that supports slash commands in its description
issuable.reload
expect(issuable.notes.user).to be_empty
- expect(issuable.assignee).to eq assignee
+ expect(issuable.assignees).to eq [assignee]
expect(issuable.labels).to eq [label_bug]
expect(issuable.milestone).to eq milestone
end
@@ -258,4 +258,19 @@ shared_examples 'issuable record that supports slash commands in its description
end
end
end
+
+ describe "preview of note on #{issuable_type}" do
+ it 'removes slash commands from note and explains them' do
+ visit public_send("namespace_project_#{issuable_type}_path", project.namespace, project, issuable)
+
+ page.within('.js-main-target-form') do
+ fill_in 'note[note]', with: "Awesome!\n/assign @bob "
+ click_on 'Preview'
+
+ expect(page).to have_content 'Awesome!'
+ expect(page).not_to have_content '/assign @bob'
+ expect(page).to have_content 'Assigns @bob.'
+ end
+ end
+ end
end
diff --git a/spec/support/helpers/fake_blob_helpers.rb b/spec/support/helpers/fake_blob_helpers.rb
new file mode 100644
index 00000000000..bc9686ed9cf
--- /dev/null
+++ b/spec/support/helpers/fake_blob_helpers.rb
@@ -0,0 +1,40 @@
+module FakeBlobHelpers
+ class FakeBlob
+ include BlobLike
+
+ attr_reader :path, :size, :data, :lfs_oid, :lfs_size
+
+ def initialize(path: 'file.txt', size: 1.kilobyte, data: 'foo', binary: false, lfs: nil)
+ @path = path
+ @size = size
+ @data = data
+ @binary = binary
+
+ @lfs_pointer = lfs.present?
+ if @lfs_pointer
+ @lfs_oid = SecureRandom.hex(20)
+ @lfs_size = 1.megabyte
+ end
+ end
+
+ alias_method :name, :path
+
+ def id
+ 0
+ end
+
+ def binary?
+ @binary
+ end
+
+ def external_storage
+ :lfs if @lfs_pointer
+ end
+
+ alias_method :external_size, :lfs_size
+ end
+
+ def fake_blob(**kwargs)
+ Blob.decorate(FakeBlob.new(**kwargs), project)
+ end
+end
diff --git a/spec/support/import_export/export_file_helper.rb b/spec/support/import_export/export_file_helper.rb
index 944ea30656f..57b6abe12b7 100644
--- a/spec/support/import_export/export_file_helper.rb
+++ b/spec/support/import_export/export_file_helper.rb
@@ -10,7 +10,7 @@ module ExportFileHelper
create(:release, project: project)
- issue = create(:issue, assignee: user, project: project)
+ issue = create(:issue, assignees: [user], project: project)
snippet = create(:project_snippet, project: project)
label = create(:label, project: project)
milestone = create(:milestone, project: project)
diff --git a/spec/support/import_export/import_export.yml b/spec/support/import_export/import_export.yml
index 17136dee000..734d6838f4d 100644
--- a/spec/support/import_export/import_export.yml
+++ b/spec/support/import_export/import_export.yml
@@ -11,9 +11,6 @@ project_tree:
- :user
included_attributes:
- project:
- - :name
- - :path
merge_requests:
- :id
user:
@@ -21,4 +18,7 @@ included_attributes:
excluded_attributes:
merge_requests:
- - :iid \ No newline at end of file
+ - :iid
+ project:
+ - :id
+ - :created_at \ No newline at end of file
diff --git a/spec/support/kubernetes_helpers.rb b/spec/support/kubernetes_helpers.rb
index b5ed71ba3be..d2a1ded57ff 100644
--- a/spec/support/kubernetes_helpers.rb
+++ b/spec/support/kubernetes_helpers.rb
@@ -5,7 +5,7 @@ module KubernetesHelpers
{
"kind" => "APIResourceList",
"resources" => [
- { "name" => "pods", "namespaced" => true, "kind" => "Pod" },
+ { "name" => "pods", "namespaced" => true, "kind" => "Pod" }
]
}
end
@@ -22,13 +22,13 @@ module KubernetesHelpers
"metadata" => {
"name" => "kube-pod",
"creationTimestamp" => "2016-11-25T19:55:19Z",
- "labels" => { "app" => app },
+ "labels" => { "app" => app }
},
"spec" => {
"containers" => [
{ "name" => "container-0" },
- { "name" => "container-1" },
- ],
+ { "name" => "container-1" }
+ ]
},
"status" => { "phase" => "Running" }
}
diff --git a/spec/support/login_helpers.rb b/spec/support/login_helpers.rb
index 9ffb00be0b8..e6da852e728 100644
--- a/spec/support/login_helpers.rb
+++ b/spec/support/login_helpers.rb
@@ -84,8 +84,4 @@ module LoginHelpers
def logout_direct
page.driver.submit :delete, '/users/sign_out', {}
end
-
- def skip_ci_admin_auth
- allow_any_instance_of(Ci::Admin::ApplicationController).to receive_messages(authenticate_admin!: true)
- end
end
diff --git a/spec/support/markdown_feature.rb b/spec/support/markdown_feature.rb
index dea0015f105..21a054af4e1 100644
--- a/spec/support/markdown_feature.rb
+++ b/spec/support/markdown_feature.rb
@@ -23,7 +23,7 @@ class MarkdownFeature
# Direct references ----------------------------------------------------------
def project
- @project ||= create(:project).tap do |project|
+ @project ||= create(:project, :repository).tap do |project|
project.team << [user, :master]
end
end
@@ -80,7 +80,7 @@ class MarkdownFeature
def xproject
@xproject ||= begin
group = create(:group, :nested)
- create(:project, namespace: group) do |project|
+ create(:project, :repository, namespace: group) do |project|
project.team << [user, :developer]
end
end
diff --git a/spec/support/matchers/access_matchers.rb b/spec/support/matchers/access_matchers.rb
index 7d238850520..3e4ca8b7ab0 100644
--- a/spec/support/matchers/access_matchers.rb
+++ b/spec/support/matchers/access_matchers.rb
@@ -51,7 +51,7 @@ module AccessMatchers
emulate_user(user, @membership)
visit(url)
- status_code != 404 && current_path != new_user_session_path
+ status_code == 200 && current_path != new_user_session_path
end
chain :of do |membership|
@@ -66,7 +66,7 @@ module AccessMatchers
emulate_user(user, @membership)
visit(url)
- status_code == 404 || current_path == new_user_session_path
+ [401, 404].include?(status_code) || current_path == new_user_session_path
end
chain :of do |membership|
diff --git a/spec/support/matchers/gitlab_git_matchers.rb b/spec/support/matchers/gitlab_git_matchers.rb
new file mode 100644
index 00000000000..c840cd4bf2d
--- /dev/null
+++ b/spec/support/matchers/gitlab_git_matchers.rb
@@ -0,0 +1,6 @@
+RSpec::Matchers.define :gitlab_git_repository_with do |values|
+ match do |actual|
+ actual.is_a?(Gitlab::Git::Repository) &&
+ values.all? { |k, v| actual.send(k) == v }
+ end
+end
diff --git a/spec/support/milestone_tabs_examples.rb b/spec/support/milestone_tabs_examples.rb
new file mode 100644
index 00000000000..c69f8e11008
--- /dev/null
+++ b/spec/support/milestone_tabs_examples.rb
@@ -0,0 +1,68 @@
+shared_examples 'milestone tabs' do
+ def go(path, extra_params = {})
+ params = if milestone.is_a?(GlobalMilestone)
+ { group_id: group.id, id: milestone.safe_title, title: milestone.title }
+ else
+ { namespace_id: project.namespace.to_param, project_id: project, id: milestone.iid }
+ end
+
+ get path, params.merge(extra_params)
+ end
+
+ describe '#merge_requests' do
+ context 'as html' do
+ before { go(:merge_requests, format: 'html') }
+
+ it 'redirects to milestone#show' do
+ expect(response).to redirect_to(milestone_path)
+ end
+ end
+
+ context 'as json' do
+ before { go(:merge_requests, format: 'json') }
+
+ it 'renders the merge requests tab template to a string' do
+ expect(response).to render_template('shared/milestones/_merge_requests_tab')
+ expect(json_response).to have_key('html')
+ end
+ end
+ end
+
+ describe '#participants' do
+ context 'as html' do
+ before { go(:participants, format: 'html') }
+
+ it 'redirects to milestone#show' do
+ expect(response).to redirect_to(milestone_path)
+ end
+ end
+
+ context 'as json' do
+ before { go(:participants, format: 'json') }
+
+ it 'renders the participants tab template to a string' do
+ expect(response).to render_template('shared/milestones/_participants_tab')
+ expect(json_response).to have_key('html')
+ end
+ end
+ end
+
+ describe '#labels' do
+ context 'as html' do
+ before { go(:labels, format: 'html') }
+
+ it 'redirects to milestone#show' do
+ expect(response).to redirect_to(milestone_path)
+ end
+ end
+
+ context 'as json' do
+ before { go(:labels, format: 'json') }
+
+ it 'renders the labels tab template to a string' do
+ expect(response).to render_template('shared/milestones/_labels_tab')
+ expect(json_response).to have_key('html')
+ end
+ end
+ end
+end
diff --git a/spec/support/prometheus_helpers.rb b/spec/support/prometheus_helpers.rb
index cc79b11616a..6b9ebcf2bb3 100644
--- a/spec/support/prometheus_helpers.rb
+++ b/spec/support/prometheus_helpers.rb
@@ -1,10 +1,16 @@
module PrometheusHelpers
def prometheus_memory_query(environment_slug)
- %{(sum(container_memory_usage_bytes{container_name!="POD",environment="#{environment_slug}"}) / count(container_memory_usage_bytes{container_name!="POD",environment="#{environment_slug}"})) /1024/1024}
+ %{avg(container_memory_usage_bytes{container_name!="POD",environment="#{environment_slug}"}) / 2^20}
end
def prometheus_cpu_query(environment_slug)
- %{sum(rate(container_cpu_usage_seconds_total{container_name!="POD",environment="#{environment_slug}"}[2m])) / count(container_cpu_usage_seconds_total{container_name!="POD",environment="#{environment_slug}"}) * 100}
+ %{avg(rate(container_cpu_usage_seconds_total{container_name!="POD",environment="#{environment_slug}"}[2m])) * 100}
+ end
+
+ def prometheus_ping_url(prometheus_query)
+ query = { query: prometheus_query }.to_query
+
+ "https://prometheus.example.com/api/v1/query?#{query}"
end
def prometheus_query_url(prometheus_query)
@@ -13,11 +19,17 @@ module PrometheusHelpers
"https://prometheus.example.com/api/v1/query?#{query}"
end
- def prometheus_query_range_url(prometheus_query, start: 8.hours.ago)
+ def prometheus_query_with_time_url(prometheus_query, time)
+ query = { query: prometheus_query, time: time.to_f }.to_query
+
+ "https://prometheus.example.com/api/v1/query?#{query}"
+ end
+
+ def prometheus_query_range_url(prometheus_query, start: 8.hours.ago, stop: Time.now.to_f)
query = {
query: prometheus_query,
start: start.to_f,
- end: Time.now.utc.to_f,
+ end: stop,
step: 1.minute.to_i
}.to_query
@@ -33,9 +45,18 @@ module PrometheusHelpers
})
end
+ def stub_prometheus_request_with_exception(url, exception_type)
+ WebMock.stub_request(:get, url).to_raise(exception_type)
+ end
+
def stub_all_prometheus_requests(environment_slug, body: nil, status: 200)
stub_prometheus_request(
- prometheus_query_url(prometheus_memory_query(environment_slug)),
+ prometheus_query_with_time_url(prometheus_memory_query(environment_slug), Time.now.utc),
+ status: status,
+ body: body || prometheus_value_body
+ )
+ stub_prometheus_request(
+ prometheus_query_with_time_url(prometheus_memory_query(environment_slug), 8.hours.ago),
status: status,
body: body || prometheus_value_body
)
@@ -45,7 +66,12 @@ module PrometheusHelpers
body: body || prometheus_values_body
)
stub_prometheus_request(
- prometheus_query_url(prometheus_cpu_query(environment_slug)),
+ prometheus_query_with_time_url(prometheus_cpu_query(environment_slug), Time.now.utc),
+ status: status,
+ body: body || prometheus_value_body
+ )
+ stub_prometheus_request(
+ prometheus_query_with_time_url(prometheus_cpu_query(environment_slug), 8.hours.ago),
status: status,
body: body || prometheus_value_body
)
diff --git a/spec/support/repo_helpers.rb b/spec/support/repo_helpers.rb
index e9d5c7b12ae..3c6956cf5e0 100644
--- a/spec/support/repo_helpers.rb
+++ b/spec/support/repo_helpers.rb
@@ -92,11 +92,11 @@ eos
changes = [
{
line_code: 'a5cc2925ca8258af241be7e5b0381edf30266302_20_20',
- file_path: '.gitignore',
+ file_path: '.gitignore'
},
{
line_code: '7445606fbf8f3683cd42bdc54b05d7a0bc2dfc44_4_6',
- file_path: '.gitmodules',
+ file_path: '.gitmodules'
}
]
diff --git a/spec/support/services/issuable_create_service_shared_examples.rb b/spec/support/services/issuable_create_service_shared_examples.rb
deleted file mode 100644
index 4f0c745b7ee..00000000000
--- a/spec/support/services/issuable_create_service_shared_examples.rb
+++ /dev/null
@@ -1,52 +0,0 @@
-shared_examples 'issuable create service' do
- context 'asssignee_id' do
- let(:assignee) { create(:user) }
-
- before { project.team << [user, :master] }
-
- it 'removes assignee_id when user id is invalid' do
- opts = { title: 'Title', description: 'Description', assignee_id: -1 }
-
- issuable = described_class.new(project, user, opts).execute
-
- expect(issuable.assignee_id).to be_nil
- end
-
- it 'removes assignee_id when user id is 0' do
- opts = { title: 'Title', description: 'Description', assignee_id: 0 }
-
- issuable = described_class.new(project, user, opts).execute
-
- expect(issuable.assignee_id).to be_nil
- end
-
- it 'saves assignee when user id is valid' do
- project.team << [assignee, :master]
- opts = { title: 'Title', description: 'Description', assignee_id: assignee.id }
-
- issuable = described_class.new(project, user, opts).execute
-
- expect(issuable.assignee_id).to eq(assignee.id)
- end
-
- context "when issuable feature is private" do
- before do
- project.project_feature.update(issues_access_level: ProjectFeature::PRIVATE,
- merge_requests_access_level: ProjectFeature::PRIVATE)
- end
-
- levels = [Gitlab::VisibilityLevel::INTERNAL, Gitlab::VisibilityLevel::PUBLIC]
-
- levels.each do |level|
- it "removes not authorized assignee when project is #{Gitlab::VisibilityLevel.level_name(level)}" do
- project.update(visibility_level: level)
- opts = { title: 'Title', description: 'Description', assignee_id: assignee.id }
-
- issuable = described_class.new(project, user, opts).execute
-
- expect(issuable.assignee_id).to be_nil
- end
- end
- end
- end
-end
diff --git a/spec/support/services/issuable_create_service_slash_commands_shared_examples.rb b/spec/support/services/issuable_create_service_slash_commands_shared_examples.rb
index 9e9cdf3e48b..1dd3663b944 100644
--- a/spec/support/services/issuable_create_service_slash_commands_shared_examples.rb
+++ b/spec/support/services/issuable_create_service_slash_commands_shared_examples.rb
@@ -49,23 +49,7 @@ shared_examples 'new issuable record that supports slash commands' do
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: create(:user),
- milestone_id: 1,
- 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.assignees).to eq([assignee])
expect(issuable.milestone).to eq(milestone)
end
end
diff --git a/spec/support/services/issuable_update_service_shared_examples.rb b/spec/support/services/issuable_update_service_shared_examples.rb
index 49cea1e608c..8947f20562f 100644
--- a/spec/support/services/issuable_update_service_shared_examples.rb
+++ b/spec/support/services/issuable_update_service_shared_examples.rb
@@ -18,52 +18,4 @@ shared_examples 'issuable update service' do
end
end
end
-
- context 'asssignee_id' do
- it 'does not update assignee when assignee_id is invalid' do
- open_issuable.update(assignee_id: user.id)
-
- update_issuable(assignee_id: -1)
-
- expect(open_issuable.reload.assignee).to eq(user)
- end
-
- it 'unassigns assignee when user id is 0' do
- open_issuable.update(assignee_id: user.id)
-
- update_issuable(assignee_id: 0)
-
- expect(open_issuable.assignee_id).to be_nil
- end
-
- it 'saves assignee when user id is valid' do
- update_issuable(assignee_id: user.id)
-
- expect(open_issuable.assignee_id).to eq(user.id)
- end
-
- it 'does not update assignee_id when user cannot read issue' do
- non_member = create(:user)
- original_assignee = open_issuable.assignee
-
- update_issuable(assignee_id: non_member.id)
-
- expect(open_issuable.assignee_id).to eq(original_assignee.id)
- end
-
- context "when issuable feature is private" do
- levels = [Gitlab::VisibilityLevel::INTERNAL, Gitlab::VisibilityLevel::PUBLIC]
-
- levels.each do |level|
- it "does not update with unauthorized assignee when project is #{Gitlab::VisibilityLevel.level_name(level)}" do
- assignee = create(:user)
- project.update(visibility_level: level)
- feature_visibility_attr = :"#{open_issuable.model_name.plural}_access_level"
- project.project_feature.update_attribute(feature_visibility_attr, ProjectFeature::PRIVATE)
-
- expect{ update_issuable(assignee_id: assignee) }.not_to change{ open_issuable.assignee }
- end
- end
- end
- end
end
diff --git a/spec/support/services/migrate_to_ghost_user_service_shared_examples.rb b/spec/support/services/migrate_to_ghost_user_service_shared_examples.rb
index 0eac587e973..dcc562c684b 100644
--- a/spec/support/services/migrate_to_ghost_user_service_shared_examples.rb
+++ b/spec/support/services/migrate_to_ghost_user_service_shared_examples.rb
@@ -35,5 +35,57 @@ shared_examples "migrating a deleted user's associated records to the ghost user
expect(user).to be_blocked
end
+
+ context "race conditions" do
+ context "when #{record_class_name} migration fails and is rolled back" do
+ before do
+ expect_any_instance_of(record_class::ActiveRecord_Associations_CollectionProxy)
+ .to receive(:update_all).and_raise(ActiveRecord::Rollback)
+ end
+
+ it 'rolls back the user block' do
+ service.execute
+
+ expect(user.reload).not_to be_blocked
+ end
+
+ it "doesn't unblock an previously-blocked user" do
+ user.block
+
+ service.execute
+
+ expect(user.reload).to be_blocked
+ end
+ end
+
+ context "when #{record_class_name} migration fails with a non-rollback exception" do
+ before do
+ expect_any_instance_of(record_class::ActiveRecord_Associations_CollectionProxy)
+ .to receive(:update_all).and_raise(ArgumentError)
+ end
+
+ it 'rolls back the user block' do
+ service.execute rescue nil
+
+ expect(user.reload).not_to be_blocked
+ end
+
+ it "doesn't unblock an previously-blocked user" do
+ user.block
+
+ service.execute rescue nil
+
+ expect(user.reload).to be_blocked
+ end
+ end
+
+ it "blocks the user before #{record_class_name} migration begins" do
+ expect(service).to receive("migrate_#{record_class_name.parameterize('_')}s".to_sym) do
+ expect(user.reload).to be_blocked
+ end
+
+ service.execute
+ end
+ end
end
end
diff --git a/spec/support/slack_mattermost_notifications_shared_examples.rb b/spec/support/slack_mattermost_notifications_shared_examples.rb
index b902fe90707..7e35ebb6c97 100644
--- a/spec/support/slack_mattermost_notifications_shared_examples.rb
+++ b/spec/support/slack_mattermost_notifications_shared_examples.rb
@@ -328,7 +328,7 @@ RSpec.shared_examples 'slack or mattermost notifications' do
context 'only notify for the default branch' do
context 'when enabled' do
let(:pipeline) do
- create(:ci_pipeline, project: project, status: 'failed', ref: 'not-the-default-branch')
+ create(:ci_pipeline, :failed, project: project, ref: 'not-the-default-branch')
end
before do
@@ -342,6 +342,18 @@ RSpec.shared_examples 'slack or mattermost notifications' do
expect(result).to be_falsy
end
end
+
+ context 'when disabled' do
+ let(:pipeline) do
+ create(:ci_pipeline, :failed, project: project, ref: 'not-the-default-branch')
+ end
+
+ before do
+ chat_service.notify_only_default_branch = false
+ end
+
+ it_behaves_like 'call Slack/Mattermost API'
+ end
end
end
end
diff --git a/spec/support/test_env.rb b/spec/support/test_env.rb
index 60c2096a126..8e31c26591b 100644
--- a/spec/support/test_env.rb
+++ b/spec/support/test_env.rb
@@ -27,6 +27,7 @@ module TestEnv
'expand-collapse-files' => '025db92',
'expand-collapse-lines' => '238e82d',
'video' => '8879059',
+ 'add-balsamiq-file' => 'b89b56d',
'crlf-diff' => '5938907',
'conflict-start' => '824be60',
'conflict-resolvable' => '1450cd6',
@@ -38,7 +39,9 @@ module TestEnv
'deleted-image-test' => '6c17798',
'wip' => 'b9238ee',
'csv' => '3dd0896',
- 'v1.1.0' => 'b83d6e3'
+ 'v1.1.0' => 'b83d6e3',
+ 'add-ipython-files' => '6d85bb69',
+ 'add-pdf-file' => 'e774ebd3'
}.freeze
# gitlab-test-fork is a fork of gitlab-fork, but we don't necessarily
@@ -124,12 +127,13 @@ module TestEnv
raise "Can't clone gitaly"
end
- start_gitaly(gitaly_dir, socket_path)
+ start_gitaly(gitaly_dir)
end
- def start_gitaly(gitaly_dir, socket_path)
+ def start_gitaly(gitaly_dir)
gitaly_exec = File.join(gitaly_dir, 'gitaly')
- @gitaly_pid = spawn({ "GITALY_SOCKET_PATH" => socket_path }, gitaly_exec, [:out, :err] => '/dev/null')
+ gitaly_config = File.join(gitaly_dir, 'config.toml')
+ @gitaly_pid = spawn(gitaly_exec, gitaly_config, [:out, :err] => '/dev/null')
end
def stop_gitaly
diff --git a/spec/support/time_tracking_shared_examples.rb b/spec/support/time_tracking_shared_examples.rb
index 01bc80f957e..84ef46ffa27 100644
--- a/spec/support/time_tracking_shared_examples.rb
+++ b/spec/support/time_tracking_shared_examples.rb
@@ -8,6 +8,7 @@ shared_examples 'issuable time tracker' do
it 'updates the sidebar component when estimate is added' do
submit_time('/estimate 3w 1d 1h')
+ wait_for_ajax
page.within '.time-tracking-estimate-only-pane' do
expect(page).to have_content '3w 1d 1h'
end
@@ -16,6 +17,7 @@ shared_examples 'issuable time tracker' do
it 'updates the sidebar component when spent is added' do
submit_time('/spend 3w 1d 1h')
+ wait_for_ajax
page.within '.time-tracking-spend-only-pane' do
expect(page).to have_content '3w 1d 1h'
end
@@ -25,6 +27,7 @@ shared_examples 'issuable time tracker' do
submit_time('/estimate 3w 1d 1h')
submit_time('/spend 3w 1d 1h')
+ wait_for_ajax
page.within '.time-tracking-comparison-pane' do
expect(page).to have_content '3w 1d 1h'
end
@@ -34,7 +37,7 @@ shared_examples 'issuable time tracker' do
submit_time('/estimate 3w 1d 1h')
submit_time('/remove_estimate')
- page.within '#issuable-time-tracker' do
+ page.within '.time-tracking-component-wrap' do
expect(page).to have_content 'No estimate or time spent'
end
end
@@ -43,13 +46,13 @@ shared_examples 'issuable time tracker' do
submit_time('/spend 3w 1d 1h')
submit_time('/remove_time_spent')
- page.within '#issuable-time-tracker' do
+ page.within '.time-tracking-component-wrap' do
expect(page).to have_content 'No estimate or time spent'
end
end
it 'shows the help state when icon is clicked' do
- page.within '#issuable-time-tracker' do
+ page.within '.time-tracking-component-wrap' do
find('.help-button').click
expect(page).to have_content 'Track time with slash commands'
expect(page).to have_content 'Learn more'
@@ -57,7 +60,7 @@ shared_examples 'issuable time tracker' do
end
it 'hides the help state when close icon is clicked' do
- page.within '#issuable-time-tracker' do
+ page.within '.time-tracking-component-wrap' do
find('.help-button').click
find('.close-help-button').click
@@ -67,7 +70,7 @@ shared_examples 'issuable time tracker' do
end
it 'displays the correct help url' do
- page.within '#issuable-time-tracker' do
+ page.within '.time-tracking-component-wrap' do
find('.help-button').click
expect(find_link('Learn more')[:href]).to have_content('/help/workflow/time_tracking.md')
diff --git a/spec/support/wait_for_ajax.rb b/spec/support/wait_for_ajax.rb
index 0f9dc2dee75..508de2ee8e1 100644
--- a/spec/support/wait_for_ajax.rb
+++ b/spec/support/wait_for_ajax.rb
@@ -6,10 +6,13 @@ module WaitForAjax
end
def finished_all_ajax_requests?
+ return true unless javascript_test?
+ return true if page.evaluate_script('typeof jQuery === "undefined"')
+
page.evaluate_script('jQuery.active').zero?
end
def javascript_test?
- [:selenium, :webkit, :chrome, :poltergeist].include?(Capybara.current_driver)
+ Capybara.current_driver == Capybara.javascript_driver
end
end
diff --git a/spec/support/wait_for_requests.rb b/spec/support/wait_for_requests.rb
index 0bfa7f72ff8..d41e83ae128 100644
--- a/spec/support/wait_for_requests.rb
+++ b/spec/support/wait_for_requests.rb
@@ -1,5 +1,10 @@
+require_relative './wait_for_ajax'
+require_relative './wait_for_vue_resource'
+
module WaitForRequests
extend self
+ include WaitForAjax
+ include WaitForVueResource
# This is inspired by http://www.salsify.com/blog/engineering/tearing-capybara-ajax-tests
def wait_for_requests_complete
diff --git a/spec/support/wait_for_vue_resource.rb b/spec/support/wait_for_vue_resource.rb
index 4a4e2e16ee7..3bb3d9c2e51 100644
--- a/spec/support/wait_for_vue_resource.rb
+++ b/spec/support/wait_for_vue_resource.rb
@@ -1,7 +1,19 @@
module WaitForVueResource
def wait_for_vue_resource(spinner: true)
Timeout.timeout(Capybara.default_max_wait_time) do
- loop until page.evaluate_script('window.activeVueResources').zero?
+ loop until finished_all_vue_resource_requests?
end
end
+
+ private
+
+ def finished_all_vue_resource_requests?
+ return true unless javascript_test?
+
+ page.evaluate_script('window.activeVueResources || 0').zero?
+ end
+
+ def javascript_test?
+ Capybara.current_driver == Capybara.javascript_driver
+ end
end
diff --git a/spec/support/workhorse_helpers.rb b/spec/support/workhorse_helpers.rb
index 47673cd4c3a..ef1f9f68671 100644
--- a/spec/support/workhorse_helpers.rb
+++ b/spec/support/workhorse_helpers.rb
@@ -9,7 +9,7 @@ module WorkhorseHelpers
header = split_header.join(':')
[
type,
- JSON.parse(Base64.urlsafe_decode64(header)),
+ JSON.parse(Base64.urlsafe_decode64(header))
]
end
end
diff --git a/spec/tasks/config_lint_spec.rb b/spec/tasks/config_lint_spec.rb
index c32f9a740b7..ed6c5b09663 100644
--- a/spec/tasks/config_lint_spec.rb
+++ b/spec/tasks/config_lint_spec.rb
@@ -5,11 +5,11 @@ describe ConfigLint do
let(:files){ ['lib/support/fake.sh'] }
it 'errors out if any bash scripts have errors' do
- expect { ConfigLint.run(files){ system('exit 1') } }.to raise_error(SystemExit)
+ expect { described_class.run(files){ system('exit 1') } }.to raise_error(SystemExit)
end
it 'passes if all scripts are fine' do
- expect { ConfigLint.run(files){ system('exit 0') } }.not_to raise_error
+ expect { described_class.run(files){ system('exit 0') } }.not_to raise_error
end
end
diff --git a/spec/tasks/gitlab/backup_rake_spec.rb b/spec/tasks/gitlab/backup_rake_spec.rb
index daea0c6bb37..4def113dd77 100644
--- a/spec/tasks/gitlab/backup_rake_spec.rb
+++ b/spec/tasks/gitlab/backup_rake_spec.rb
@@ -230,11 +230,13 @@ describe 'gitlab:app namespace rake task' do
before do
FileUtils.mkdir('tmp/tests/default_storage')
FileUtils.mkdir('tmp/tests/custom_storage')
+ gitaly_address = Gitlab.config.repositories.storages.default.gitaly_address
storages = {
- 'default' => { 'path' => Settings.absolute('tmp/tests/default_storage') },
- 'custom' => { 'path' => Settings.absolute('tmp/tests/custom_storage') }
+ 'default' => { 'path' => Settings.absolute('tmp/tests/default_storage'), 'gitaly_address' => gitaly_address },
+ 'custom' => { 'path' => Settings.absolute('tmp/tests/custom_storage'), 'gitaly_address' => gitaly_address }
}
allow(Gitlab.config.repositories).to receive(:storages).and_return(storages)
+ Gitlab::GitalyClient.configure_channels
# Create the projects now, after mocking the settings but before doing the backup
project_a
@@ -350,7 +352,7 @@ describe 'gitlab:app namespace rake task' do
end
it 'name has human readable time' do
- expect(@backup_tar).to match(/\d+_\d{4}_\d{2}_\d{2}_gitlab_backup.tar$/)
+ expect(@backup_tar).to match(/\d+_\d{4}_\d{2}_\d{2}_\d+\.\d+\.\d+.*_gitlab_backup.tar$/)
end
end
end # gitlab:app namespace
diff --git a/spec/tasks/gitlab/gitaly_rake_spec.rb b/spec/tasks/gitlab/gitaly_rake_spec.rb
index aaf998a546f..f035504320b 100644
--- a/spec/tasks/gitlab/gitaly_rake_spec.rb
+++ b/spec/tasks/gitlab/gitaly_rake_spec.rb
@@ -80,7 +80,7 @@ describe 'gitlab:gitaly namespace rake task' do
it 'prints storage configuration in a TOML format' do
config = {
'default' => { 'path' => '/path/to/default' },
- 'nfs_01' => { 'path' => '/path/to/nfs_01' },
+ 'nfs_01' => { 'path' => '/path/to/nfs_01' }
}
allow(Gitlab.config.repositories).to receive(:storages).and_return(config)
diff --git a/spec/tasks/gitlab/shell_rake_spec.rb b/spec/tasks/gitlab/shell_rake_spec.rb
index 226d34fe2c9..ee3614c50f6 100644
--- a/spec/tasks/gitlab/shell_rake_spec.rb
+++ b/spec/tasks/gitlab/shell_rake_spec.rb
@@ -11,6 +11,10 @@ describe 'gitlab:shell rake tasks' do
it 'invokes create_hooks task' do
expect(Rake::Task['gitlab:shell:create_hooks']).to receive(:invoke)
+ storages = Gitlab.config.repositories.storages.values.map { |rs| rs['path'] }
+ expect(Kernel).to receive(:system).with('bin/install', *storages).and_call_original
+ expect(Kernel).to receive(:system).with('bin/compile').and_call_original
+
run_rake_task('gitlab:shell:install')
end
end
diff --git a/spec/unicorn/unicorn_spec.rb b/spec/unicorn/unicorn_spec.rb
new file mode 100644
index 00000000000..8518c047a47
--- /dev/null
+++ b/spec/unicorn/unicorn_spec.rb
@@ -0,0 +1,98 @@
+require 'fileutils'
+
+require 'excon'
+
+require 'spec_helper'
+
+describe 'Unicorn' do
+ before(:all) do
+ config_lines = File.read('config/unicorn.rb.example').split("\n")
+
+ # Remove these because they make setup harder.
+ config_lines = config_lines.reject do |line|
+ %w[
+ working_directory
+ worker_processes
+ listen
+ pid
+ stderr_path
+ stdout_path
+ ].any? { |prefix| line.start_with?(prefix) }
+ end
+
+ config_lines << "working_directory '#{Rails.root}'"
+
+ # We want to have exactly 1 worker process because that makes it
+ # predictable which process will handle our requests.
+ config_lines << 'worker_processes 1'
+
+ @socket_path = File.join(Dir.pwd, 'tmp/tests/unicorn.socket')
+ config_lines << "listen '#{@socket_path}'"
+
+ ready_file = 'tmp/tests/unicorn-worker-ready'
+ FileUtils.rm_f(ready_file)
+ after_fork_index = config_lines.index { |l| l.start_with?('after_fork') }
+ config_lines.insert(after_fork_index + 1, "File.write('#{ready_file}', Process.pid)")
+
+ config_path = 'tmp/tests/unicorn.rb'
+ File.write(config_path, config_lines.join("\n") + "\n")
+
+ cmd = %W[unicorn -E test -c #{config_path} #{Rails.root.join('config.ru')}]
+ @unicorn_master_pid = spawn(*cmd)
+ wait_unicorn_boot!(@unicorn_master_pid, ready_file)
+ WebMock.allow_net_connect!
+ end
+
+ %w[SIGQUIT SIGTERM SIGKILL].each do |signal|
+ it "has a worker that self-terminates on signal #{signal}" do
+ response = Excon.get('unix:///unicorn_test/pid', socket: @socket_path)
+ expect(response.status).to eq(200)
+
+ worker_pid = response.body.to_i
+ expect(worker_pid).to be > 0
+
+ begin
+ Excon.post('unix:///unicorn_test/kill', socket: @socket_path, body: "signal=#{signal}")
+ rescue Excon::Error::Socket
+ # The connection may be closed abruptly
+ end
+
+ expect(pid_gone?(worker_pid)).to eq(true)
+ end
+ end
+
+ after(:all) do
+ WebMock.disable_net_connect!(allow_localhost: true)
+ Process.kill('TERM', @unicorn_master_pid)
+ end
+
+ def wait_unicorn_boot!(master_pid, ready_file)
+ # Unicorn should boot in under 60 seconds so 120 seconds seems like a good timeout.
+ timeout = 120
+ timeout.times do
+ return if File.exist?(ready_file)
+ pid = Process.waitpid(master_pid, Process::WNOHANG)
+ raise "unicorn failed to boot: #{$?}" unless pid.nil?
+
+ sleep 1
+ end
+
+ raise "unicorn boot timed out after #{timeout} seconds"
+ end
+
+ def pid_gone?(pid)
+ # Worker termination should take less than a second. That makes 10
+ # seconds a generous timeout.
+ 10.times do
+ begin
+ Process.kill(0, pid)
+ rescue Errno::ESRCH
+ return true
+ end
+
+ sleep 1
+ end
+
+ false
+ end
+end
diff --git a/spec/uploaders/personal_file_uploader_spec.rb b/spec/uploaders/personal_file_uploader_spec.rb
new file mode 100644
index 00000000000..fb92f2ae3ab
--- /dev/null
+++ b/spec/uploaders/personal_file_uploader_spec.rb
@@ -0,0 +1,31 @@
+require 'spec_helper'
+
+describe PersonalFileUploader do
+ let(:uploader) { described_class.new(build_stubbed(:empty_project)) }
+ let(:snippet) { create(:personal_snippet) }
+
+ describe '.absolute_path' do
+ it 'returns the correct absolute path by building it dynamically' do
+ upload = double(model: snippet, path: 'secret/foo.jpg')
+
+ dynamic_segment = "personal_snippet/#{snippet.id}"
+
+ expect(described_class.absolute_path(upload)).to end_with("#{dynamic_segment}/secret/foo.jpg")
+ end
+ end
+
+ describe '#to_h' do
+ it 'returns the hass' do
+ uploader = described_class.new(snippet, 'secret')
+
+ allow(uploader).to receive(:file).and_return(double(extension: 'txt', filename: 'file_name'))
+ expected_url = "/uploads/personal_snippet/#{snippet.id}/secret/file_name"
+
+ expect(uploader.to_h).to eq(
+ alt: 'file_name',
+ url: expected_url,
+ markdown: "[file_name](#{expected_url})"
+ )
+ end
+ end
+end
diff --git a/spec/validators/dynamic_path_validator_spec.rb b/spec/validators/dynamic_path_validator_spec.rb
new file mode 100644
index 00000000000..b114bfc1bca
--- /dev/null
+++ b/spec/validators/dynamic_path_validator_spec.rb
@@ -0,0 +1,266 @@
+require 'spec_helper'
+
+describe DynamicPathValidator do
+ let(:validator) { described_class.new(attributes: [:path]) }
+
+ # Pass in a full path to remove the format segment:
+ # `/ci/lint(.:format)` -> `/ci/lint`
+ def without_format(path)
+ path.split('(', 2)[0]
+ end
+
+ # Pass in a full path and get the last segment before a wildcard
+ # That's not a parameter
+ # `/*namespace_id/:project_id/builds/artifacts/*ref_name_and_path`
+ # -> 'builds/artifacts'
+ def path_before_wildcard(path)
+ path = path.gsub(STARTING_WITH_NAMESPACE, "")
+ path_segments = path.split('/').reject(&:empty?)
+ wildcard_index = path_segments.index { |segment| parameter?(segment) }
+
+ segments_before_wildcard = path_segments[0..wildcard_index - 1]
+
+ segments_before_wildcard.join('/')
+ end
+
+ def parameter?(segment)
+ segment =~ /[*:]/
+ end
+
+ # If the path is reserved. Then no conflicting paths can# be created for any
+ # route using this reserved word.
+ #
+ # Both `builds/artifacts` & `build` are covered by reserving the word
+ # `build`
+ def wildcards_include?(path)
+ described_class::WILDCARD_ROUTES.include?(path) ||
+ described_class::WILDCARD_ROUTES.include?(path.split('/').first)
+ end
+
+ def failure_message(missing_words, constant_name, migration_helper)
+ missing_words = Array(missing_words)
+ <<-MSG
+ Found new routes that could cause conflicts with existing namespaced routes
+ for groups or projects.
+
+ Add <#{missing_words.join(', ')}> to `DynamicPathValidator::#{constant_name}
+ to make sure no projects or namespaces can be created with those paths.
+
+ To rename any existing records with those paths you can use the
+ `Gitlab::Database::RenameReservedpathsMigration::<VERSION>.#{migration_helper}`
+ migration helper.
+
+ Make sure to make a note of the renamed records in the release blog post.
+
+ MSG
+ end
+
+ let(:all_routes) do
+ Rails.application.routes.routes.routes.
+ map { |r| r.path.spec.to_s }
+ end
+
+ let(:routes_without_format) { all_routes.map { |path| without_format(path) } }
+
+ # Routes not starting with `/:` or `/*`
+ # all routes not starting with a param
+ let(:routes_not_starting_in_wildcard) { routes_without_format.select { |p| p !~ %r{^/[:*]} } }
+
+ let(:top_level_words) do
+ routes_not_starting_in_wildcard.map do |route|
+ route.split('/')[1]
+ end.compact.uniq
+ end
+
+ # All routes that start with a namespaced path, that have 1 or more
+ # path-segments before having another wildcard parameter.
+ # - Starting with paths:
+ # - `/*namespace_id/:project_id/`
+ # - `/*namespace_id/:id/`
+ # - Followed by one or more path-parts not starting with `:` or `*`
+ # - Followed by a path-part that includes a wildcard parameter `*`
+ # At the time of writing these routes match: http://rubular.com/r/Rv2pDE5Dvw
+ STARTING_WITH_NAMESPACE = %r{^/\*namespace_id/:(project_)?id}
+ NON_PARAM_PARTS = %r{[^:*][a-z\-_/]*}
+ ANY_OTHER_PATH_PART = %r{[a-z\-_/:]*}
+ WILDCARD_SEGMENT = %r{\*}
+ let(:namespaced_wildcard_routes) do
+ routes_without_format.select do |p|
+ p =~ %r{#{STARTING_WITH_NAMESPACE}/#{NON_PARAM_PARTS}/#{ANY_OTHER_PATH_PART}#{WILDCARD_SEGMENT}}
+ end
+ end
+
+ # This will return all paths that are used in a namespaced route
+ # before another wildcard path:
+ #
+ # /*namespace_id/:project_id/builds/artifacts/*ref_name_and_path
+ # /*namespace_id/:project_id/info/lfs/objects/*oid
+ # /*namespace_id/:project_id/commits/*id
+ # /*namespace_id/:project_id/builds/:build_id/artifacts/file/*path
+ # -> ['builds/artifacts', 'info/lfs/objects', 'commits', 'artifacts/file']
+ let(:all_wildcard_paths) do
+ namespaced_wildcard_routes.map do |route|
+ path_before_wildcard(route)
+ end.uniq
+ end
+
+ STARTING_WITH_GROUP = %r{^/groups/\*(group_)?id/}
+ let(:group_routes) do
+ routes_without_format.select do |path|
+ path =~ STARTING_WITH_GROUP
+ end
+ end
+
+ let(:paths_after_group_id) do
+ group_routes.map do |route|
+ route.gsub(STARTING_WITH_GROUP, '').split('/').first
+ end.uniq
+ end
+
+ describe 'TOP_LEVEL_ROUTES' do
+ it 'includes all the top level namespaces' do
+ failure_block = lambda do
+ missing_words = top_level_words - described_class::TOP_LEVEL_ROUTES
+ failure_message(missing_words, 'TOP_LEVEL_ROUTES', 'rename_root_paths')
+ end
+
+ expect(described_class::TOP_LEVEL_ROUTES)
+ .to include(*top_level_words), failure_block
+ end
+ end
+
+ describe 'GROUP_ROUTES' do
+ it "don't contain a second wildcard" do
+ failure_block = lambda do
+ missing_words = paths_after_group_id - described_class::GROUP_ROUTES
+ failure_message(missing_words, 'GROUP_ROUTES', 'rename_child_paths')
+ end
+
+ expect(described_class::GROUP_ROUTES)
+ .to include(*paths_after_group_id), failure_block
+ end
+ end
+
+ describe 'WILDCARD_ROUTES' do
+ it 'includes all paths that can be used after a namespace/project path' do
+ aggregate_failures do
+ all_wildcard_paths.each do |path|
+ expect(wildcards_include?(path))
+ .to be(true), failure_message(path, 'WILDCARD_ROUTES', 'rename_wildcard_paths')
+ end
+ end
+ end
+ end
+
+ describe '.without_reserved_wildcard_paths_regex' do
+ subject { described_class.without_reserved_wildcard_paths_regex }
+
+ it 'rejects paths starting with a reserved top level' do
+ expect(subject).not_to match('dashboard/hello/world')
+ expect(subject).not_to match('dashboard')
+ end
+
+ it 'matches valid paths with a toplevel word in a different place' do
+ expect(subject).to match('parent/dashboard/project-path')
+ end
+
+ it 'rejects paths containing a wildcard reserved word' do
+ expect(subject).not_to match('hello/edit')
+ expect(subject).not_to match('hello/edit/in-the-middle')
+ expect(subject).not_to match('foo/bar1/refs/master/logs_tree')
+ end
+
+ it 'matches valid paths' do
+ expect(subject).to match('parent/child/project-path')
+ end
+ end
+
+ describe '.regex_excluding_child_paths' do
+ let(:subject) { described_class.without_reserved_child_paths_regex }
+
+ it 'rejects paths containing a child reserved word' do
+ expect(subject).not_to match('hello/group_members')
+ expect(subject).not_to match('hello/activity/in-the-middle')
+ expect(subject).not_to match('foo/bar1/refs/master/logs_tree')
+ end
+
+ it 'allows a child path on the top level' do
+ expect(subject).to match('activity/foo')
+ expect(subject).to match('avatar')
+ end
+ end
+
+ describe ".valid?" do
+ it 'is not case sensitive' do
+ expect(described_class.valid?("Users")).to be_falsey
+ end
+
+ it "isn't valid when the top level is reserved" do
+ test_path = 'u/should-be-a/reserved-word'
+
+ expect(described_class.valid?(test_path)).to be_falsey
+ end
+
+ it "isn't valid if any of the path segments is reserved" do
+ test_path = 'the-wildcard/wikis/is-not-allowed'
+
+ expect(described_class.valid?(test_path)).to be_falsey
+ end
+
+ it "is valid if the path doesn't contain reserved words" do
+ test_path = 'there-are/no-wildcards/in-this-path'
+
+ expect(described_class.valid?(test_path)).to be_truthy
+ end
+
+ it 'allows allows a child path on the last spot' do
+ test_path = 'there/can-be-a/project-called/labels'
+
+ expect(described_class.valid?(test_path)).to be_truthy
+ end
+
+ it 'rejects a child path somewhere else' do
+ test_path = 'there/can-be-no/labels/group'
+
+ expect(described_class.valid?(test_path)).to be_falsey
+ end
+
+ it 'rejects paths that are in an incorrect format' do
+ test_path = 'incorrect/format.git'
+
+ expect(described_class.valid?(test_path)).to be_falsey
+ end
+ end
+
+ describe '#path_reserved_for_record?' do
+ it 'reserves a sub-group named activity' do
+ group = build(:group, :nested, path: 'activity')
+
+ expect(validator.path_reserved_for_record?(group, 'activity')).to be_truthy
+ end
+
+ it "doesn't reserve a project called activity" do
+ project = build(:project, path: 'activity')
+
+ expect(validator.path_reserved_for_record?(project, 'activity')).to be_falsey
+ end
+ end
+
+ describe '#validates_each' do
+ it 'adds a message when the path is not in the correct format' do
+ group = build(:group)
+
+ validator.validate_each(group, :path, "Path with spaces, and comma's!")
+
+ expect(group.errors[:path]).to include(Gitlab::Regex.namespace_regex_message)
+ end
+
+ it 'adds a message when the path is not in the correct format' do
+ group = build(:group, path: 'users')
+
+ validator.validate_each(group, :path, 'users')
+
+ expect(group.errors[:path]).to include('users is a reserved name')
+ end
+ end
+end
diff --git a/spec/views/projects/_last_commit.html.haml_spec.rb b/spec/views/projects/_last_commit.html.haml_spec.rb
new file mode 100644
index 00000000000..eea1695b171
--- /dev/null
+++ b/spec/views/projects/_last_commit.html.haml_spec.rb
@@ -0,0 +1,22 @@
+require 'spec_helper'
+
+describe 'projects/_last_commit', :view do
+ let(:project) { create(:project, :repository) }
+
+ context 'when there is a pipeline present for the commit' do
+ context 'when pipeline is blocked' do
+ let!(:pipeline) do
+ create(:ci_pipeline, :blocked, project: project,
+ sha: project.commit.id)
+ end
+
+ it 'shows correct pipeline badge' do
+ render 'projects/last_commit', commit: project.commit,
+ project: project,
+ ref: :master
+
+ expect(rendered).to have_text "blocked #{project.commit.short_id}"
+ end
+ end
+ end
+end
diff --git a/spec/views/projects/blob/_viewer.html.haml_spec.rb b/spec/views/projects/blob/_viewer.html.haml_spec.rb
new file mode 100644
index 00000000000..08018767624
--- /dev/null
+++ b/spec/views/projects/blob/_viewer.html.haml_spec.rb
@@ -0,0 +1,97 @@
+require 'spec_helper'
+
+describe 'projects/blob/_viewer.html.haml', :view do
+ include FakeBlobHelpers
+
+ let(:project) { build(:empty_project) }
+
+ let(:viewer_class) do
+ Class.new(BlobViewer::Base) do
+ include BlobViewer::Rich
+
+ self.partial_name = 'text'
+ self.max_size = 1.megabyte
+ self.absolute_max_size = 5.megabytes
+ self.client_side = false
+ end
+ end
+
+ let(:viewer) { viewer_class.new(blob) }
+ let(:blob) { fake_blob }
+
+ before do
+ assign(:project, project)
+ assign(:blob, blob)
+ assign(:id, File.join('master', blob.path))
+
+ controller.params[:controller] = 'projects/blob'
+ controller.params[:action] = 'show'
+ controller.params[:namespace_id] = project.namespace.to_param
+ controller.params[:project_id] = project.to_param
+ controller.params[:id] = File.join('master', blob.path)
+ end
+
+ def render_view
+ render partial: 'projects/blob/viewer', locals: { viewer: viewer }
+ end
+
+ context 'when the viewer is server side' do
+ before do
+ viewer_class.client_side = false
+ end
+
+ context 'when there is no render error' do
+ it 'adds a URL to the blob viewer element' do
+ render_view
+
+ expect(rendered).to have_css('.blob-viewer[data-url]')
+ end
+
+ it 'renders the loading indicator' do
+ render_view
+
+ expect(view).to render_template('projects/blob/viewers/_loading')
+ end
+ end
+
+ context 'when there is a render error' do
+ let(:blob) { fake_blob(size: 10.megabytes) }
+
+ it 'renders the error' do
+ render_view
+
+ expect(view).to render_template('projects/blob/_render_error')
+ end
+ end
+ end
+
+ context 'when the viewer is client side' do
+ before do
+ viewer_class.client_side = true
+ end
+
+ context 'when there is no render error' do
+ it 'prepares the viewer' do
+ expect(viewer).to receive(:prepare!)
+
+ render_view
+ end
+
+ it 'renders the viewer' do
+ render_view
+
+ expect(view).to render_template('projects/blob/viewers/_text')
+ end
+ end
+
+ context 'when there is a render error' do
+ let(:blob) { fake_blob(size: 10.megabytes) }
+
+ it 'renders the error' do
+ render_view
+
+ expect(view).to render_template('projects/blob/_render_error')
+ end
+ end
+ end
+end
diff --git a/spec/views/projects/commit/_commit_box.html.haml_spec.rb b/spec/views/projects/commit/_commit_box.html.haml_spec.rb
index cec87dcecc8..ab120929c6c 100644
--- a/spec/views/projects/commit/_commit_box.html.haml_spec.rb
+++ b/spec/views/projects/commit/_commit_box.html.haml_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
-describe 'projects/commit/_commit_box.html.haml' do
- include Devise::Test::ControllerHelpers
-
+describe 'projects/commit/_commit_box.html.haml', :view do
let(:user) { create(:user) }
let(:project) { create(:project, :repository) }
@@ -18,14 +16,32 @@ describe 'projects/commit/_commit_box.html.haml' do
expect(rendered).to have_text("#{Commit.truncate_sha(project.commit.sha)}")
end
- it 'shows the last pipeline that ran for the commit' do
- create(:ci_pipeline, project: project, sha: project.commit.id, status: 'success')
- create(:ci_pipeline, project: project, sha: project.commit.id, status: 'canceled')
- third_pipeline = create(:ci_pipeline, project: project, sha: project.commit.id, status: 'failed')
+ context 'when there is a pipeline present' do
+ context 'when there are multiple pipelines for a commit' do
+ it 'shows the last pipeline' do
+ create(:ci_pipeline, project: project, sha: project.commit.id, status: 'success')
+ create(:ci_pipeline, project: project, sha: project.commit.id, status: 'canceled')
+ third_pipeline = create(:ci_pipeline, project: project, sha: project.commit.id, status: 'failed')
- render
+ render
+
+ expect(rendered).to have_text("Pipeline ##{third_pipeline.id} failed")
+ end
+ end
- expect(rendered).to have_text("Pipeline ##{third_pipeline.id} failed")
+ context 'when pipeline for the commit is blocked' do
+ let!(:pipeline) do
+ create(:ci_pipeline, :blocked, project: project,
+ sha: project.commit.id)
+ end
+
+ it 'shows correct pipeline description' do
+ render
+
+ expect(rendered).to have_text "Pipeline ##{pipeline.id} " \
+ 'waiting for manual action'
+ end
+ end
end
context 'viewing a commit' do
diff --git a/spec/views/projects/commit/show.html.haml_spec.rb b/spec/views/projects/commit/show.html.haml_spec.rb
new file mode 100644
index 00000000000..122075cc10e
--- /dev/null
+++ b/spec/views/projects/commit/show.html.haml_spec.rb
@@ -0,0 +1,44 @@
+require 'spec_helper'
+
+describe 'projects/commit/show.html.haml', :view do
+ let(:project) { create(:project, :repository) }
+
+ before do
+ assign(:project, project)
+ assign(:repository, project.repository)
+ assign(:commit, project.commit)
+ assign(:noteable, project.commit)
+ assign(:notes, [])
+ assign(:diffs, project.commit.diffs)
+
+ allow(view).to receive(:current_user).and_return(nil)
+ allow(view).to receive(:can?).and_return(false)
+ allow(view).to receive(:can_collaborate_with_project?).and_return(false)
+ allow(view).to receive(:current_ref).and_return(project.repository.root_ref)
+ allow(view).to receive(:diff_btn).and_return('')
+ end
+
+ context 'inline diff view' do
+ before do
+ allow(view).to receive(:diff_view).and_return(:inline)
+
+ render
+ end
+
+ it 'keeps container-limited' do
+ expect(rendered).not_to have_selector('.limit-container-width')
+ end
+ end
+
+ context 'parallel diff view' do
+ before do
+ allow(view).to receive(:diff_view).and_return(:parallel)
+
+ render
+ end
+
+ it 'spans full width' do
+ expect(rendered).to have_selector('.limit-container-width')
+ end
+ end
+end
diff --git a/spec/views/projects/environments/terminal.html.haml_spec.rb b/spec/views/projects/environments/terminal.html.haml_spec.rb
new file mode 100644
index 00000000000..d2e47225226
--- /dev/null
+++ b/spec/views/projects/environments/terminal.html.haml_spec.rb
@@ -0,0 +1,32 @@
+require 'spec_helper'
+
+describe 'projects/environments/terminal' do
+ let!(:environment) { create(:environment, :with_review_app) }
+
+ before do
+ assign(:environment, environment)
+ assign(:project, environment.project)
+
+ allow(view).to receive(:can?).and_return(true)
+ end
+
+ context 'when environment has external URL' do
+ it 'shows external URL button' do
+ environment.update_attribute(:external_url, 'https://gitlab.com')
+
+ render
+
+ expect(rendered).to have_link(nil, href: 'https://gitlab.com')
+ end
+ end
+
+ context 'when environment does not have external URL' do
+ it 'shows external URL button' do
+ environment.update_attribute(:external_url, nil)
+
+ render
+
+ expect(rendered).not_to have_link(nil, href: 'https://gitlab.com')
+ end
+ end
+end
diff --git a/spec/views/projects/imports/new.html.haml_spec.rb b/spec/views/projects/imports/new.html.haml_spec.rb
new file mode 100644
index 00000000000..9b293065797
--- /dev/null
+++ b/spec/views/projects/imports/new.html.haml_spec.rb
@@ -0,0 +1,22 @@
+require "spec_helper"
+
+describe "projects/imports/new.html.haml" do
+ let(:user) { create(:user) }
+
+ context 'when import fails' do
+ let(:project) { create(:project_empty_repo, import_status: :failed, import_error: '<a href="http://googl.com">Foo</a>', import_type: :gitlab_project, import_source: '/var/opt/gitlab/gitlab-rails/shared/tmp/project_exports/uploads/t.tar.gz', import_url: nil) }
+
+ before do
+ sign_in(user)
+ project.team << [user, :master]
+ end
+
+ it "escapes HTML in import errors" do
+ assign(:project, project)
+
+ render
+
+ expect(rendered).not_to have_link('Foo', href: "http://googl.com")
+ end
+ end
+end
diff --git a/spec/views/projects/pipelines/_stage.html.haml_spec.rb b/spec/views/projects/pipelines/_stage.html.haml_spec.rb
index 10095ad7694..9c91c4e0fbd 100644
--- a/spec/views/projects/pipelines/_stage.html.haml_spec.rb
+++ b/spec/views/projects/pipelines/_stage.html.haml_spec.rb
@@ -39,9 +39,8 @@ describe 'projects/pipelines/_stage', :view do
context 'when there are retried builds present' do
before do
- create_list(:ci_build, 2, name: 'test:build',
- stage: stage.name,
- pipeline: pipeline)
+ create(:ci_build, name: 'test:build', stage: stage.name, pipeline: pipeline, retried: true)
+ create(:ci_build, name: 'test:build', stage: stage.name, pipeline: pipeline)
end
it 'shows only latest builds' do
diff --git a/spec/views/projects/pipelines/show.html.haml_spec.rb b/spec/views/projects/pipelines/show.html.haml_spec.rb
deleted file mode 100644
index bb39ec8efbf..00000000000
--- a/spec/views/projects/pipelines/show.html.haml_spec.rb
+++ /dev/null
@@ -1,67 +0,0 @@
-require 'spec_helper'
-
-describe 'projects/pipelines/show' do
- include Devise::Test::ControllerHelpers
-
- let(:user) { create(:user) }
- let(:project) { create(:project, :repository) }
-
- let(:pipeline) do
- create(:ci_empty_pipeline,
- project: project,
- sha: project.commit.id,
- user: user)
- end
-
- before do
- controller.prepend_view_path('app/views/projects')
-
- create_build('build', 0, 'build', :success)
- create_build('test', 1, 'rspec 0:2', :pending)
- create_build('test', 1, 'rspec 1:2', :running)
- create_build('test', 1, 'spinach 0:2', :created)
- create_build('test', 1, 'spinach 1:2', :created)
- create_build('test', 1, 'audit', :created)
- create_build('deploy', 2, 'production', :created)
-
- create(:generic_commit_status, pipeline: pipeline, stage: 'external', name: 'jenkins', stage_idx: 3)
-
- assign(:project, project)
- assign(:pipeline, pipeline.present(current_user: user))
- assign(:commit, project.commit)
-
- allow(view).to receive(:can?).and_return(true)
- end
-
- it 'shows a graph with grouped stages' do
- render
-
- expect(rendered).to have_css('.js-pipeline-graph')
- expect(rendered).to have_css('.js-grouped-pipeline-dropdown')
-
- # header
- expect(rendered).to have_text("##{pipeline.id}")
- expect(rendered).to have_css('time', text: pipeline.created_at.strftime("%b %d, %Y"))
- expect(rendered).to have_selector(%Q(img[alt$="#{pipeline.user.name}'s avatar"]))
- expect(rendered).to have_link(pipeline.user.name, href: user_path(pipeline.user))
-
- # stages
- expect(rendered).to have_text('Build')
- expect(rendered).to have_text('Test')
- expect(rendered).to have_text('Deploy')
- expect(rendered).to have_text('External')
-
- # builds
- expect(rendered).to have_text('rspec')
- expect(rendered).to have_text('spinach')
- expect(rendered).to have_text('rspec 0:2')
- expect(rendered).to have_text('production')
- expect(rendered).to have_text('jenkins')
- end
-
- private
-
- def create_build(stage, stage_idx, name, status)
- create(:ci_build, pipeline: pipeline, stage: stage, stage_idx: stage_idx, name: name, status: status)
- end
-end
diff --git a/spec/views/projects/tags/index.html.haml_spec.rb b/spec/views/projects/tags/index.html.haml_spec.rb
new file mode 100644
index 00000000000..33122365e9a
--- /dev/null
+++ b/spec/views/projects/tags/index.html.haml_spec.rb
@@ -0,0 +1,20 @@
+require 'spec_helper'
+
+describe 'projects/tags/index', :view do
+ let(:project) { create(:project) }
+
+ before do
+ assign(:project, project)
+ assign(:repository, project.repository)
+ assign(:tags, [])
+
+ allow(view).to receive(:current_ref).and_return('master')
+ allow(view).to receive(:can?).and_return(false)
+ end
+
+ it 'defaults sort dropdown toggle to last updated' do
+ render
+
+ expect(rendered).to have_button('Last updated')
+ end
+end
diff --git a/spec/views/projects/tree/show.html.haml_spec.rb b/spec/views/projects/tree/show.html.haml_spec.rb
index 900f8d4732f..835a93e620e 100644
--- a/spec/views/projects/tree/show.html.haml_spec.rb
+++ b/spec/views/projects/tree/show.html.haml_spec.rb
@@ -31,7 +31,7 @@ describe 'projects/tree/show' do
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)
+ expect(rendered).to have_css('.readme-holder')
end
end
end
diff --git a/spec/views/projects/notes/_form.html.haml_spec.rb b/spec/views/shared/notes/_form.html.haml_spec.rb
index a364f9bce92..d7d0a5bf56a 100644
--- a/spec/views/projects/notes/_form.html.haml_spec.rb
+++ b/spec/views/shared/notes/_form.html.haml_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe 'projects/notes/_form' do
+describe 'shared/notes/_form' do
include Devise::Test::ControllerHelpers
let(:user) { create(:user) }
diff --git a/spec/workers/delete_user_worker_spec.rb b/spec/workers/delete_user_worker_spec.rb
index 0765573408c..5912dd76262 100644
--- a/spec/workers/delete_user_worker_spec.rb
+++ b/spec/workers/delete_user_worker_spec.rb
@@ -8,13 +8,13 @@ describe DeleteUserWorker do
expect_any_instance_of(Users::DestroyService).to receive(:execute).
with(user, {})
- DeleteUserWorker.new.perform(current_user.id, user.id)
+ described_class.new.perform(current_user.id, user.id)
end
it "uses symbolized keys" do
expect_any_instance_of(Users::DestroyService).to receive(:execute).
with(user, test: "test")
- DeleteUserWorker.new.perform(current_user.id, user.id, "test" => "test")
+ described_class.new.perform(current_user.id, user.id, "test" => "test")
end
end
diff --git a/spec/workers/emails_on_push_worker_spec.rb b/spec/workers/emails_on_push_worker_spec.rb
index 8cf2b888f9a..a0ed85cc0b3 100644
--- a/spec/workers/emails_on_push_worker_spec.rb
+++ b/spec/workers/emails_on_push_worker_spec.rb
@@ -12,7 +12,7 @@ describe EmailsOnPushWorker do
let(:perform) { subject.perform(project.id, recipients, data.stringify_keys) }
let(:email) { ActionMailer::Base.deliveries.last }
- subject { EmailsOnPushWorker.new }
+ subject { described_class.new }
describe "#perform" do
context "when push is a new branch" do
diff --git a/spec/workers/expire_build_instance_artifacts_worker_spec.rb b/spec/workers/expire_build_instance_artifacts_worker_spec.rb
index d202b3de77e..1d8da68883b 100644
--- a/spec/workers/expire_build_instance_artifacts_worker_spec.rb
+++ b/spec/workers/expire_build_instance_artifacts_worker_spec.rb
@@ -34,12 +34,14 @@ describe ExpireBuildInstanceArtifactsWorker do
context 'when associated project was removed' do
let(:build) do
create(:ci_build, :artifacts, artifacts_expiry) do |build|
- build.project.delete
+ build.project.pending_delete = true
end
end
it 'does not remove artifacts' do
- expect(build.reload.artifacts_file.exists?).to be_truthy
+ expect do
+ build.reload.artifacts_file
+ end.not_to raise_error
end
end
end
diff --git a/spec/workers/expire_pipeline_cache_worker_spec.rb b/spec/workers/expire_pipeline_cache_worker_spec.rb
new file mode 100644
index 00000000000..ceba604dea2
--- /dev/null
+++ b/spec/workers/expire_pipeline_cache_worker_spec.rb
@@ -0,0 +1,44 @@
+require 'spec_helper'
+
+describe ExpirePipelineCacheWorker do
+ let(:user) { create(:user) }
+ let(:project) { create(:empty_project) }
+ let(:pipeline) { create(:ci_pipeline, project: project) }
+ subject { described_class.new }
+
+ describe '#perform' do
+ it 'invalidates Etag caching for project pipelines path' do
+ pipelines_path = "/#{project.full_path}/pipelines.json"
+ new_mr_pipelines_path = "/#{project.full_path}/merge_requests/new.json"
+
+ expect_any_instance_of(Gitlab::EtagCaching::Store).to receive(:touch).with(pipelines_path)
+ expect_any_instance_of(Gitlab::EtagCaching::Store).to receive(:touch).with(new_mr_pipelines_path)
+
+ subject.perform(pipeline.id)
+ end
+
+ it 'invalidates Etag caching for merge request pipelines if pipeline runs on any commit of that source branch' do
+ pipeline = create(:ci_empty_pipeline, status: 'created', project: project, ref: 'master')
+ merge_request = create(:merge_request, source_project: project, source_branch: pipeline.ref)
+ merge_request_pipelines_path = "/#{project.full_path}/merge_requests/#{merge_request.iid}/pipelines.json"
+
+ allow_any_instance_of(Gitlab::EtagCaching::Store).to receive(:touch)
+ expect_any_instance_of(Gitlab::EtagCaching::Store).to receive(:touch).with(merge_request_pipelines_path)
+
+ subject.perform(pipeline.id)
+ end
+
+ it "doesn't do anything if the pipeline not exist" do
+ expect_any_instance_of(Gitlab::EtagCaching::Store).not_to receive(:touch)
+
+ subject.perform(617748)
+ end
+
+ it 'updates the cached status for a project' do
+ expect(Gitlab::Cache::Ci::ProjectPipelineStatus).to receive(:update_for_pipeline).
+ with(pipeline)
+
+ subject.perform(pipeline.id)
+ end
+ end
+end
diff --git a/spec/workers/git_garbage_collect_worker_spec.rb b/spec/workers/git_garbage_collect_worker_spec.rb
index 029f35512e0..8c5303b61cc 100644
--- a/spec/workers/git_garbage_collect_worker_spec.rb
+++ b/spec/workers/git_garbage_collect_worker_spec.rb
@@ -6,7 +6,7 @@ describe GitGarbageCollectWorker do
let(:project) { create(:project, :repository) }
let(:shell) { Gitlab::Shell.new }
- subject { GitGarbageCollectWorker.new }
+ subject { described_class.new }
describe "#perform" do
it "flushes ref caches when the task is 'gc'" do
@@ -105,7 +105,7 @@ describe GitGarbageCollectWorker do
author: Gitlab::Git.committer_hash(email: 'foo@bar', name: 'baz'),
committer: Gitlab::Git.committer_hash(email: 'foo@bar', name: 'baz'),
tree: old_commit.tree,
- parents: [old_commit],
+ parents: [old_commit]
)
GitOperationService.new(nil, project.repository).send(
:update_ref,
diff --git a/spec/workers/gitlab_usage_ping_worker_spec.rb b/spec/workers/gitlab_usage_ping_worker_spec.rb
index b6c080f36f4..26241044533 100644
--- a/spec/workers/gitlab_usage_ping_worker_spec.rb
+++ b/spec/workers/gitlab_usage_ping_worker_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe GitlabUsagePingWorker do
- subject { GitlabUsagePingWorker.new }
+ subject { described_class.new }
it "sends POST request" do
stub_application_setting(usage_ping_enabled: true)
diff --git a/spec/workers/group_destroy_worker_spec.rb b/spec/workers/group_destroy_worker_spec.rb
index 1ff5a3b9034..c78efc67076 100644
--- a/spec/workers/group_destroy_worker_spec.rb
+++ b/spec/workers/group_destroy_worker_spec.rb
@@ -5,7 +5,7 @@ describe GroupDestroyWorker do
let(:user) { create(:admin) }
let!(:project) { create(:empty_project, namespace: group) }
- subject { GroupDestroyWorker.new }
+ subject { described_class.new }
describe "#perform" do
it "deletes the project" do
diff --git a/spec/workers/merge_worker_spec.rb b/spec/workers/merge_worker_spec.rb
index b5e1fdb8ded..303193bab9b 100644
--- a/spec/workers/merge_worker_spec.rb
+++ b/spec/workers/merge_worker_spec.rb
@@ -15,7 +15,7 @@ describe MergeWorker do
it 'clears cache of source repo after removing source branch' do
expect(source_project.repository.branch_names).to include('markdown')
- MergeWorker.new.perform(
+ described_class.new.perform(
merge_request.id, merge_request.author_id,
commit_message: 'wow such merge',
should_remove_source_branch: true)
diff --git a/spec/workers/namespaceless_project_destroy_worker_spec.rb b/spec/workers/namespaceless_project_destroy_worker_spec.rb
new file mode 100644
index 00000000000..8533b7b85e9
--- /dev/null
+++ b/spec/workers/namespaceless_project_destroy_worker_spec.rb
@@ -0,0 +1,79 @@
+require 'spec_helper'
+
+describe NamespacelessProjectDestroyWorker do
+ subject { described_class.new }
+
+ before do
+ # Stub after_save callbacks that will fail when Project has no namespace
+ allow_any_instance_of(Project).to receive(:ensure_dir_exist).and_return(nil)
+ allow_any_instance_of(Project).to receive(:update_project_statistics).and_return(nil)
+ end
+
+ describe '#perform' do
+ context 'project has namespace' do
+ it 'does not do anything' do
+ project = create(:empty_project)
+
+ subject.perform(project.id)
+
+ expect(Project.unscoped.all).to include(project)
+ end
+ end
+
+ context 'project has no namespace' do
+ let!(:project) do
+ project = build(:empty_project, namespace_id: nil)
+ project.save(validate: false)
+ project
+ end
+
+ context 'project not a fork of another project' do
+ it "truncates the project's team" do
+ expect_any_instance_of(ProjectTeam).to receive(:truncate)
+
+ subject.perform(project.id)
+ end
+
+ it 'deletes the project' do
+ subject.perform(project.id)
+
+ expect(Project.unscoped.all).not_to include(project)
+ end
+
+ it 'does not call unlink_fork' do
+ is_expected.not_to receive(:unlink_fork)
+
+ subject.perform(project.id)
+ end
+
+ it 'does not do anything in Project#remove_pages method' do
+ expect(Gitlab::PagesTransfer).not_to receive(:new)
+
+ subject.perform(project.id)
+ end
+ end
+
+ context 'project forked from another' do
+ let!(:parent_project) { create(:empty_project) }
+
+ before do
+ create(:forked_project_link, forked_to_project: project, forked_from_project: parent_project)
+ end
+
+ it 'closes open merge requests' do
+ merge_request = create(:merge_request, source_project: project, target_project: parent_project)
+
+ subject.perform(project.id)
+
+ expect(merge_request.reload).to be_closed
+ end
+
+ it 'destroys the link' do
+ subject.perform(project.id)
+
+ expect(parent_project.forked_project_links).to be_empty
+ end
+ end
+ end
+ end
+end
diff --git a/spec/workers/pipeline_metrics_worker_spec.rb b/spec/workers/pipeline_metrics_worker_spec.rb
index 5dbc0da95c2..ef71125c0b6 100644
--- a/spec/workers/pipeline_metrics_worker_spec.rb
+++ b/spec/workers/pipeline_metrics_worker_spec.rb
@@ -2,7 +2,7 @@ require 'spec_helper'
describe PipelineMetricsWorker do
let(:project) { create(:project, :repository) }
- let!(:merge_request) { create(:merge_request, source_project: project, source_branch: pipeline.ref) }
+ let!(:merge_request) { create(:merge_request, source_project: project, source_branch: pipeline.ref, head_pipeline: pipeline) }
let(:pipeline) do
create(:ci_empty_pipeline,
diff --git a/spec/workers/pipeline_proccess_worker_spec.rb b/spec/workers/pipeline_process_worker_spec.rb
index 86e9d7f6684..86e9d7f6684 100644
--- a/spec/workers/pipeline_proccess_worker_spec.rb
+++ b/spec/workers/pipeline_process_worker_spec.rb
diff --git a/spec/workers/pipeline_schedule_worker_spec.rb b/spec/workers/pipeline_schedule_worker_spec.rb
new file mode 100644
index 00000000000..91d5a16993f
--- /dev/null
+++ b/spec/workers/pipeline_schedule_worker_spec.rb
@@ -0,0 +1,51 @@
+require 'spec_helper'
+
+describe PipelineScheduleWorker do
+ subject { described_class.new.perform }
+
+ set(:project) { create(:project, :repository) }
+ set(:user) { create(:user) }
+
+ let!(:pipeline_schedule) do
+ create(:ci_pipeline_schedule, :nightly, project: project, owner: user)
+ end
+
+ before do
+ project.add_master(user)
+
+ stub_ci_pipeline_to_return_yaml_file
+ end
+
+ context 'when there is a scheduled pipeline within next_run_at' do
+ let(:next_run_at) { 2.days.ago }
+
+ before do
+ pipeline_schedule.update_column(:next_run_at, next_run_at)
+ end
+
+ it 'creates a new pipeline' do
+ expect { subject }.to change { project.pipelines.count }.by(1)
+ end
+
+ it 'updates the next_run_at field' do
+ subject
+
+ expect(pipeline_schedule.reload.next_run_at).to be > Time.now
+ end
+
+ it 'sets the schedule on the pipeline' do
+ subject
+ expect(project.pipelines.last.pipeline_schedule).to eq(pipeline_schedule)
+ end
+ end
+
+ context 'inactive schedule' do
+ before do
+ pipeline_schedule.update(active: false)
+ end
+
+ it 'does not creates a new pipeline' do
+ expect { subject }.not_to change { project.pipelines.count }
+ end
+ end
+end
diff --git a/spec/workers/post_receive_spec.rb b/spec/workers/post_receive_spec.rb
index a2a559a2369..3289c2df1fb 100644
--- a/spec/workers/post_receive_spec.rb
+++ b/spec/workers/post_receive_spec.rb
@@ -5,12 +5,33 @@ describe PostReceive do
let(:wrongly_encoded_changes) { changes.encode("ISO-8859-1").force_encoding("UTF-8") }
let(:base64_changes) { Base64.encode64(wrongly_encoded_changes) }
let(:project) { create(:project, :repository) }
+ let(:project_identifier) { "project-#{project.id}" }
let(:key) { create(:key, user: project.owner) }
let(:key_id) { key.shell_id }
- context "as a resque worker" do
+ context "as a sidekiq worker" do
it "reponds to #perform" do
- expect(PostReceive.new).to respond_to(:perform)
+ expect(described_class.new).to respond_to(:perform)
+ end
+ end
+
+ context 'with a non-existing project' do
+ let(:project_identifier) { "project-123456789" }
+ let(:error_message) do
+ "Triggered hook for non-existing project with identifier \"#{project_identifier}\""
+ end
+
+ it "returns false and logs an error" do
+ expect(Gitlab::GitLogger).to receive(:error).with("POST-RECEIVE: #{error_message}")
+ expect(described_class.new.perform(project_identifier, key_id, base64_changes)).to be(false)
+ end
+ end
+
+ context "with an absolute path as the project identifier" do
+ it "searches the project by full path" do
+ expect(Project).to receive(:find_by_full_path).with(project.full_path).and_call_original
+
+ described_class.new.perform(pwd(project), key_id, base64_changes)
end
end
@@ -25,7 +46,7 @@ describe PostReceive 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)
+ described_class.new.perform(project_identifier, key_id, base64_changes)
end
end
@@ -35,7 +56,7 @@ describe PostReceive 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)
+ described_class.new.perform(project_identifier, key_id, base64_changes)
end
end
@@ -45,12 +66,12 @@ describe PostReceive 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)
+ described_class.new.perform(project_identifier, key_id, base64_changes)
end
end
context "gitlab-ci.yml" do
- subject { PostReceive.new.perform(pwd(project), key_id, base64_changes) }
+ subject { described_class.new.perform(project_identifier, key_id, base64_changes) }
context "creates a Ci::Pipeline for every change" do
before do
@@ -72,10 +93,31 @@ describe PostReceive do
end
end
+ describe '#process_repository_update' do
+ let(:changes) {'123456 789012 refs/heads/tést'}
+ let(:fake_hook_data) do
+ { event_name: 'repository_update' }
+ end
+
+ before do
+ allow_any_instance_of(Gitlab::GitPostReceive).to receive(:identify).and_return(project.owner)
+ allow_any_instance_of(Gitlab::DataBuilder::Repository).to receive(:update).and_return(fake_hook_data)
+ # silence hooks so we can isolate
+ allow_any_instance_of(Key).to receive(:post_create_hook).and_return(true)
+ allow(subject).to receive(:process_project_changes).and_return(true)
+ end
+
+ it 'calls SystemHooksService' do
+ expect_any_instance_of(SystemHooksService).to receive(:execute_hooks).with(fake_hook_data, :repository_update_hooks).and_return(true)
+
+ subject.perform(pwd(project), key_id, base64_changes)
+ end
+ end
+
context "webhook" do
it "fetches the correct project" do
- expect(Project).to receive(:find_by_full_path).with(project.path_with_namespace).and_return(project)
- PostReceive.new.perform(pwd(project), key_id, base64_changes)
+ expect(Project).to receive(:find_by).with(id: project.id.to_s)
+ described_class.new.perform(project_identifier, key_id, base64_changes)
end
it "does not run if the author is not in the project" do
@@ -85,22 +127,22 @@ describe PostReceive do
expect(project).not_to receive(:execute_hooks)
- expect(PostReceive.new.perform(pwd(project), key_id, base64_changes)).to be_falsey
+ expect(described_class.new.perform(project_identifier, key_id, base64_changes)).to be_falsey
end
it "asks the project to trigger all hooks" do
- allow(Project).to receive(:find_by_full_path).and_return(project)
+ allow(Project).to receive(:find_by).and_return(project)
expect(project).to receive(:execute_hooks).twice
expect(project).to receive(:execute_services).twice
- PostReceive.new.perform(pwd(project), key_id, base64_changes)
+ described_class.new.perform(project_identifier, key_id, base64_changes)
end
it "enqueues a UpdateMergeRequestsWorker job" do
- allow(Project).to receive(:find_by_full_path).and_return(project)
+ allow(Project).to receive(:find_by).and_return(project)
expect(UpdateMergeRequestsWorker).to receive(:perform_async).with(project.id, project.owner.id, any_args)
- PostReceive.new.perform(pwd(project), key_id, base64_changes)
+ described_class.new.perform(project_identifier, key_id, base64_changes)
end
end
diff --git a/spec/workers/process_commit_worker_spec.rb b/spec/workers/process_commit_worker_spec.rb
index 9afe2e610b9..6295856b461 100644
--- a/spec/workers/process_commit_worker_spec.rb
+++ b/spec/workers/process_commit_worker_spec.rb
@@ -20,6 +20,14 @@ describe ProcessCommitWorker do
worker.perform(project.id, -1, commit.to_hash)
end
+ it 'does not process the commit when no issues are referenced' do
+ allow(worker).to receive(:build_commit).and_return(double(matches_cross_reference_regex?: false))
+
+ expect(worker).not_to receive(:process_commit_message)
+
+ worker.perform(project.id, user.id, commit.to_hash)
+ end
+
it 'processes the commit message' do
expect(worker).to receive(:process_commit_message).and_call_original
diff --git a/spec/workers/project_destroy_worker_spec.rb b/spec/workers/project_destroy_worker_spec.rb
index 0ab42f99510..3d135f40c1f 100644
--- a/spec/workers/project_destroy_worker_spec.rb
+++ b/spec/workers/project_destroy_worker_spec.rb
@@ -4,7 +4,7 @@ describe ProjectDestroyWorker do
let(:project) { create(:project, :repository) }
let(:path) { project.repository.path_to_repo }
- subject { ProjectDestroyWorker.new }
+ subject { described_class.new }
describe "#perform" do
it "deletes the project" do
diff --git a/spec/workers/propagate_service_template_worker_spec.rb b/spec/workers/propagate_service_template_worker_spec.rb
new file mode 100644
index 00000000000..7040d5ef81c
--- /dev/null
+++ b/spec/workers/propagate_service_template_worker_spec.rb
@@ -0,0 +1,29 @@
+require 'spec_helper'
+
+describe PropagateServiceTemplateWorker do
+ let!(:service_template) do
+ PushoverService.create(
+ template: true,
+ active: true,
+ properties: {
+ device: 'MyDevice',
+ sound: 'mic',
+ priority: 4,
+ user_key: 'asdf',
+ api_key: '123456789'
+ })
+ end
+
+ before do
+ allow_any_instance_of(Gitlab::ExclusiveLease).to receive(:try_obtain).
+ and_return(true)
+ end
+
+ describe '#perform' do
+ it 'calls the propagate service with the template' do
+ expect(Projects::PropagateServiceTemplate).to receive(:propagate).with(service_template)
+
+ subject.perform(service_template.id)
+ end
+ end
+end
diff --git a/spec/workers/remove_expired_members_worker_spec.rb b/spec/workers/remove_expired_members_worker_spec.rb
index 402aa1e714e..058fdf4c009 100644
--- a/spec/workers/remove_expired_members_worker_spec.rb
+++ b/spec/workers/remove_expired_members_worker_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe RemoveExpiredMembersWorker do
- let(:worker) { RemoveExpiredMembersWorker.new }
+ let(:worker) { described_class.new }
describe '#perform' do
context 'project members' do
diff --git a/spec/workers/remove_unreferenced_lfs_objects_worker_spec.rb b/spec/workers/remove_unreferenced_lfs_objects_worker_spec.rb
index 6d42946de38..1c183ce54f4 100644
--- a/spec/workers/remove_unreferenced_lfs_objects_worker_spec.rb
+++ b/spec/workers/remove_unreferenced_lfs_objects_worker_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe RemoveUnreferencedLfsObjectsWorker do
- let(:worker) { RemoveUnreferencedLfsObjectsWorker.new }
+ let(:worker) { described_class.new }
describe '#perform' do
let!(:unreferenced_lfs_object1) { create(:lfs_object, oid: '1') }
diff --git a/spec/workers/repository_check/clear_worker_spec.rb b/spec/workers/repository_check/clear_worker_spec.rb
index a3b70c74787..3b1a64c5057 100644
--- a/spec/workers/repository_check/clear_worker_spec.rb
+++ b/spec/workers/repository_check/clear_worker_spec.rb
@@ -5,7 +5,7 @@ describe RepositoryCheck::ClearWorker do
project = create(:empty_project)
project.update_columns(
last_repository_check_failed: true,
- last_repository_check_at: Time.now,
+ last_repository_check_at: Time.now
)
described_class.new.perform
diff --git a/spec/workers/repository_fork_worker_spec.rb b/spec/workers/repository_fork_worker_spec.rb
index 7d6a2db2972..5e1cb74c7fc 100644
--- a/spec/workers/repository_fork_worker_spec.rb
+++ b/spec/workers/repository_fork_worker_spec.rb
@@ -5,7 +5,7 @@ describe RepositoryForkWorker do
let(:fork_project) { create(:project, :repository, forked_from_project: project) }
let(:shell) { Gitlab::Shell.new }
- subject { RepositoryForkWorker.new }
+ subject { described_class.new }
before do
allow(subject).to receive(:gitlab_shell).and_return(shell)
diff --git a/spec/workers/trigger_schedule_worker_spec.rb b/spec/workers/trigger_schedule_worker_spec.rb
deleted file mode 100644
index 861bed4442e..00000000000
--- a/spec/workers/trigger_schedule_worker_spec.rb
+++ /dev/null
@@ -1,73 +0,0 @@
-require 'spec_helper'
-
-describe TriggerScheduleWorker do
- let(:worker) { described_class.new }
-
- before do
- stub_ci_pipeline_to_return_yaml_file
- end
-
- context 'when there is a scheduled trigger within next_run_at' do
- let(:next_run_at) { 2.days.ago }
-
- let!(:trigger_schedule) do
- create(:ci_trigger_schedule, :nightly)
- end
-
- before do
- trigger_schedule.update_column(:next_run_at, next_run_at)
- end
-
- it 'creates a new trigger request' do
- expect { worker.perform }.to change { Ci::TriggerRequest.count }
- end
-
- it 'creates a new pipeline' do
- expect { worker.perform }.to change { Ci::Pipeline.count }
- expect(Ci::Pipeline.last).to be_pending
- end
-
- it 'updates next_run_at' do
- worker.perform
-
- expect(trigger_schedule.reload.next_run_at).not_to eq(next_run_at)
- end
-
- context 'inactive schedule' do
- before do
- trigger_schedule.update(active: false)
- end
-
- it 'does not create a new trigger' do
- expect { worker.perform }.not_to change { Ci::TriggerRequest.count }
- end
- end
- end
-
- context 'when there are no scheduled triggers within next_run_at' do
- before { create(:ci_trigger_schedule, :nightly) }
-
- it 'does not create a new pipeline' do
- expect { worker.perform }.not_to change { Ci::Pipeline.count }
- end
-
- it 'does not update next_run_at' do
- expect { worker.perform }.not_to change { Ci::TriggerSchedule.last.next_run_at }
- end
- end
-
- context 'when next_run_at is nil' do
- before do
- schedule = create(:ci_trigger_schedule, :nightly)
- schedule.update_column(:next_run_at, nil)
- end
-
- it 'does not create a new pipeline' do
- expect { worker.perform }.not_to change { Ci::Pipeline.count }
- end
-
- it 'does not update next_run_at' do
- expect { worker.perform }.not_to change { Ci::TriggerSchedule.last.next_run_at }
- end
- end
-end