summaryrefslogtreecommitdiff
path: root/spec
diff options
context:
space:
mode:
Diffstat (limited to 'spec')
-rw-r--r--spec/config/smime_signature_settings_spec.rb2
-rw-r--r--spec/controllers/admin/application_settings_controller_spec.rb20
-rw-r--r--spec/controllers/admin/clusters_controller_spec.rb4
-rw-r--r--spec/controllers/admin/requests_profiles_controller_spec.rb2
-rw-r--r--spec/controllers/admin/sessions_controller_spec.rb98
-rw-r--r--spec/controllers/admin/users_controller_spec.rb92
-rw-r--r--spec/controllers/application_controller_spec.rb129
-rw-r--r--spec/controllers/boards/lists_controller_spec.rb25
-rw-r--r--spec/controllers/concerns/enforces_admin_authentication_spec.rb82
-rw-r--r--spec/controllers/concerns/metrics_dashboard_spec.rb58
-rw-r--r--spec/controllers/dashboard/todos_controller_spec.rb4
-rw-r--r--spec/controllers/groups/clusters_controller_spec.rb6
-rw-r--r--spec/controllers/groups/group_members_controller_spec.rb2
-rw-r--r--spec/controllers/groups/labels_controller_spec.rb14
-rw-r--r--spec/controllers/groups/registry/repositories_controller_spec.rb90
-rw-r--r--spec/controllers/groups/settings/ci_cd_controller_spec.rb54
-rw-r--r--spec/controllers/groups/shared_projects_controller_spec.rb6
-rw-r--r--spec/controllers/groups_controller_spec.rb47
-rw-r--r--spec/controllers/health_controller_spec.rb30
-rw-r--r--spec/controllers/import/bitbucket_controller_spec.rb15
-rw-r--r--spec/controllers/import/bitbucket_server_controller_spec.rb2
-rw-r--r--spec/controllers/import/gitlab_projects_controller_spec.rb4
-rw-r--r--spec/controllers/omniauth_callbacks_controller_spec.rb22
-rw-r--r--spec/controllers/profiles/notifications_controller_spec.rb32
-rw-r--r--spec/controllers/profiles_controller_spec.rb4
-rw-r--r--spec/controllers/projects/artifacts_controller_spec.rb132
-rw-r--r--spec/controllers/projects/autocomplete_sources_controller_spec.rb8
-rw-r--r--spec/controllers/projects/branches_controller_spec.rb50
-rw-r--r--spec/controllers/projects/clusters_controller_spec.rb6
-rw-r--r--spec/controllers/projects/commit_controller_spec.rb7
-rw-r--r--spec/controllers/projects/deploy_keys_controller_spec.rb83
-rw-r--r--spec/controllers/projects/deployments_controller_spec.rb17
-rw-r--r--spec/controllers/projects/discussions_controller_spec.rb2
-rw-r--r--spec/controllers/projects/environments/prometheus_api_controller_spec.rb6
-rw-r--r--spec/controllers/projects/environments_controller_spec.rb24
-rw-r--r--spec/controllers/projects/error_tracking_controller_spec.rb4
-rw-r--r--spec/controllers/projects/git_http_controller_spec.rb25
-rw-r--r--spec/controllers/projects/grafana_api_controller_spec.rb97
-rw-r--r--spec/controllers/projects/issues_controller_spec.rb189
-rw-r--r--spec/controllers/projects/jobs_controller_spec.rb125
-rw-r--r--spec/controllers/projects/merge_requests/diffs_controller_spec.rb294
-rw-r--r--spec/controllers/projects/mirrors_controller_spec.rb2
-rw-r--r--spec/controllers/projects/notes_controller_spec.rb1
-rw-r--r--spec/controllers/projects/pipeline_schedules_controller_spec.rb14
-rw-r--r--spec/controllers/projects/pipelines_controller_spec.rb259
-rw-r--r--spec/controllers/projects/pipelines_settings_controller_spec.rb4
-rw-r--r--spec/controllers/projects/project_members_controller_spec.rb2
-rw-r--r--spec/controllers/projects/registry/repositories_controller_spec.rb14
-rw-r--r--spec/controllers/projects/registry/tags_controller_spec.rb40
-rw-r--r--spec/controllers/projects/serverless/functions_controller_spec.rb134
-rw-r--r--spec/controllers/projects/settings/ci_cd_controller_spec.rb29
-rw-r--r--spec/controllers/projects/settings/operations_controller_spec.rb19
-rw-r--r--spec/controllers/projects/snippets_controller_spec.rb6
-rw-r--r--spec/controllers/projects/templates_controller_spec.rb40
-rw-r--r--spec/controllers/projects/wikis_controller_spec.rb4
-rw-r--r--spec/controllers/projects_controller_spec.rb2
-rw-r--r--spec/controllers/registrations_controller_spec.rb5
-rw-r--r--spec/controllers/search_controller_spec.rb2
-rw-r--r--spec/controllers/sessions_controller_spec.rb19
-rw-r--r--spec/controllers/snippets_controller_spec.rb8
-rw-r--r--spec/controllers/uploads_controller_spec.rb27
-rw-r--r--spec/db/production/settings_spec.rb2
-rw-r--r--spec/db/schema_spec.rb2
-rw-r--r--spec/dependencies/omniauth_saml_spec.rb2
-rw-r--r--spec/factories/abuse_reports.rb2
-rw-r--r--spec/factories/analytics/cycle_analytics/project_stages.rb15
-rw-r--r--spec/factories/appearances.rb6
-rw-r--r--spec/factories/application_settings.rb2
-rw-r--r--spec/factories/award_emoji.rb4
-rw-r--r--spec/factories/aws/roles.rb10
-rw-r--r--spec/factories/boards.rb16
-rw-r--r--spec/factories/broadcast_messages.rb2
-rw-r--r--spec/factories/chat_names.rb4
-rw-r--r--spec/factories/ci/bridge.rb24
-rw-r--r--spec/factories/ci/build_trace_chunks.rb22
-rw-r--r--spec/factories/ci/builds.rb181
-rw-r--r--spec/factories/ci/group_variables.rb6
-rw-r--r--spec/factories/ci/job_artifacts.rb46
-rw-r--r--spec/factories/ci/job_variables.rb2
-rw-r--r--spec/factories/ci/pipeline_schedule.rb32
-rw-r--r--spec/factories/ci/pipeline_schedule_variables.rb4
-rw-r--r--spec/factories/ci/pipeline_variables.rb2
-rw-r--r--spec/factories/ci/pipelines.rb39
-rw-r--r--spec/factories/ci/runners.rb24
-rw-r--r--spec/factories/ci/sources/pipelines.rb15
-rw-r--r--spec/factories/ci/stages.rb16
-rw-r--r--spec/factories/ci/variables.rb6
-rw-r--r--spec/factories/clusters/applications/helm.rb32
-rw-r--r--spec/factories/clusters/clusters.rb42
-rw-r--r--spec/factories/clusters/kubernetes_namespaces.rb2
-rw-r--r--spec/factories/clusters/platforms/kubernetes.rb12
-rw-r--r--spec/factories/clusters/providers/aws.rb38
-rw-r--r--spec/factories/clusters/providers/gcp.rb14
-rw-r--r--spec/factories/commit_statuses.rb38
-rw-r--r--spec/factories/commits.rb2
-rw-r--r--spec/factories/container_repositories.rb15
-rw-r--r--spec/factories/conversational_development_index_metrics.rb60
-rw-r--r--spec/factories/deploy_keys_projects.rb2
-rw-r--r--spec/factories/deploy_tokens.rb14
-rw-r--r--spec/factories/deployments.rb20
-rw-r--r--spec/factories/environments.rb6
-rw-r--r--spec/factories/error_tracking/error.rb28
-rw-r--r--spec/factories/error_tracking/project.rb14
-rw-r--r--spec/factories/events.rb36
-rw-r--r--spec/factories/evidences.rb7
-rw-r--r--spec/factories/external_pull_requests.rb16
-rw-r--r--spec/factories/file_uploaders.rb2
-rw-r--r--spec/factories/gitaly/commit.rb1
-rw-r--r--spec/factories/gpg_signature.rb2
-rw-r--r--spec/factories/grafana_integrations.rb9
-rw-r--r--spec/factories/group_members.rb16
-rw-r--r--spec/factories/groups.rb22
-rw-r--r--spec/factories/identities.rb4
-rw-r--r--spec/factories/import_states.rb14
-rw-r--r--spec/factories/internal_ids.rb2
-rw-r--r--spec/factories/issues.rb14
-rw-r--r--spec/factories/labels.rb4
-rw-r--r--spec/factories/lfs_file_locks.rb2
-rw-r--r--spec/factories/lfs_objects.rb6
-rw-r--r--spec/factories/lfs_objects_projects.rb2
-rw-r--r--spec/factories/lists.rb14
-rw-r--r--spec/factories/merge_request_diff_files.rb66
-rw-r--r--spec/factories/merge_request_diffs.rb4
-rw-r--r--spec/factories/merge_requests.rb58
-rw-r--r--spec/factories/milestones.rb20
-rw-r--r--spec/factories/notes.rb36
-rw-r--r--spec/factories/notification_settings.rb2
-rw-r--r--spec/factories/oauth_applications.rb2
-rw-r--r--spec/factories/pages_domains.rb50
-rw-r--r--spec/factories/personal_access_tokens.rb12
-rw-r--r--spec/factories/pool_repositories.rb10
-rw-r--r--spec/factories/programming_languages.rb4
-rw-r--r--spec/factories/project_auto_devops.rb12
-rw-r--r--spec/factories/project_daily_statistics.rb2
-rw-r--r--spec/factories/project_error_tracking_settings.rb10
-rw-r--r--spec/factories/project_group_links.rb2
-rw-r--r--spec/factories/project_hooks.rb22
-rw-r--r--spec/factories/project_members.rb14
-rw-r--r--spec/factories/project_metrics_settings.rb2
-rw-r--r--spec/factories/projects.rb108
-rw-r--r--spec/factories/prometheus_metrics.rb16
-rw-r--r--spec/factories/protected_branches.rb14
-rw-r--r--spec/factories/protected_tags.rb8
-rw-r--r--spec/factories/releases.rb10
-rw-r--r--spec/factories/remote_mirrors.rb2
-rw-r--r--spec/factories/repository_languages.rb2
-rw-r--r--spec/factories/resource_label_events.rb2
-rw-r--r--spec/factories/services.rb90
-rw-r--r--spec/factories/shards.rb2
-rw-r--r--spec/factories/snippets.rb6
-rw-r--r--spec/factories/spam_logs.rb2
-rw-r--r--spec/factories/suggestions.rb12
-rw-r--r--spec/factories/system_note_metadata.rb2
-rw-r--r--spec/factories/term_agreements.rb4
-rw-r--r--spec/factories/terms.rb2
-rw-r--r--spec/factories/timelogs.rb2
-rw-r--r--spec/factories/todos.rb6
-rw-r--r--spec/factories/u2f_registrations.rb2
-rw-r--r--spec/factories/uploads.rb45
-rw-r--r--spec/factories/user_agent_details.rb4
-rw-r--r--spec/factories/user_callouts.rb2
-rw-r--r--spec/factories/user_statuses.rb4
-rw-r--r--spec/factories/users.rb27
-rw-r--r--spec/factories/web_hook_log.rb22
-rw-r--r--spec/factories/wiki_directories.rb2
-rw-r--r--spec/factories_spec.rb2
-rw-r--r--spec/fast_spec_helper.rb2
-rw-r--r--spec/features/admin/admin_settings_spec.rb707
-rw-r--r--spec/features/admin/admin_users_spec.rb6
-rw-r--r--spec/features/boards/boards_spec.rb14
-rw-r--r--spec/features/boards/multi_select_spec.rb110
-rw-r--r--spec/features/boards/sidebar_spec.rb9
-rw-r--r--spec/features/clusters/cluster_detail_page_spec.rb28
-rw-r--r--spec/features/container_registry_spec.rb11
-rw-r--r--spec/features/dashboard/active_tab_spec.rb16
-rw-r--r--spec/features/groups/issues_spec.rb1
-rw-r--r--spec/features/groups/members/master_manages_access_requests_spec.rb2
-rw-r--r--spec/features/groups/members/request_access_spec.rb2
-rw-r--r--spec/features/groups/user_sees_package_sidebar_spec.rb47
-rw-r--r--spec/features/invites_spec.rb1
-rw-r--r--spec/features/issues/filtered_search/dropdown_milestone_spec.rb38
-rw-r--r--spec/features/issues/gfm_autocomplete_spec.rb2
-rw-r--r--spec/features/issues/spam_issues_spec.rb48
-rw-r--r--spec/features/issues_spec.rb3
-rw-r--r--spec/features/markdown/copy_as_gfm_spec.rb6
-rw-r--r--spec/features/markdown/markdown_spec.rb4
-rw-r--r--spec/features/markdown/mermaid_spec.rb12
-rw-r--r--spec/features/merge_request/user_edits_assignees_sidebar_spec.rb68
-rw-r--r--spec/features/merge_request/user_sees_merge_widget_spec.rb14
-rw-r--r--spec/features/merge_request/user_suggests_changes_on_diff_spec.rb58
-rw-r--r--spec/features/merge_request/user_views_merge_request_from_deleted_fork_spec.rb31
-rw-r--r--spec/features/milestones/user_deletes_milestone_spec.rb6
-rw-r--r--spec/features/projects/artifacts/user_browses_artifacts_spec.rb19
-rw-r--r--spec/features/projects/branches_spec.rb3
-rw-r--r--spec/features/projects/clusters/eks_spec.rb34
-rw-r--r--spec/features/projects/clusters/gcp_spec.rb2
-rw-r--r--spec/features/projects/clusters_spec.rb2
-rw-r--r--spec/features/projects/commits/user_browses_commits_spec.rb4
-rw-r--r--spec/features/projects/compare_spec.rb34
-rw-r--r--spec/features/projects/environments/environment_spec.rb18
-rw-r--r--spec/features/projects/features_visibility_spec.rb2
-rw-r--r--spec/features/projects/files/dockerfile_dropdown_spec.rb2
-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/template_type_dropdown_spec.rb20
-rw-r--r--spec/features/projects/files/undo_template_spec.rb11
-rw-r--r--spec/features/projects/import_export/export_file_spec.rb3
-rw-r--r--spec/features/projects/import_export/import_file_spec.rb3
-rw-r--r--spec/features/projects/issuable_templates_spec.rb37
-rw-r--r--spec/features/projects/jobs/permissions_spec.rb3
-rw-r--r--spec/features/projects/jobs/user_browses_job_spec.rb70
-rw-r--r--spec/features/projects/jobs_spec.rb15
-rw-r--r--spec/features/projects/members/group_members_spec.rb4
-rw-r--r--spec/features/projects/members/group_requester_cannot_request_access_to_project_spec.rb4
-rw-r--r--spec/features/projects/members/master_manages_access_requests_spec.rb2
-rw-r--r--spec/features/projects/members/user_requests_access_spec.rb2
-rw-r--r--spec/features/projects/pipelines/pipeline_spec.rb10
-rw-r--r--spec/features/projects/settings/repository_settings_spec.rb13
-rw-r--r--spec/features/protected_branches_spec.rb8
-rw-r--r--spec/features/runners_spec.rb6
-rw-r--r--spec/features/search/user_searches_for_code_spec.rb7
-rw-r--r--spec/features/security/group/internal_access_spec.rb1
-rw-r--r--spec/features/security/group/private_access_spec.rb1
-rw-r--r--spec/features/security/group/public_access_spec.rb1
-rw-r--r--spec/features/security/project/internal_access_spec.rb1
-rw-r--r--spec/features/security/project/private_access_spec.rb1
-rw-r--r--spec/features/security/project/public_access_spec.rb1
-rw-r--r--spec/features/snippets/private_snippets_spec.rb22
-rw-r--r--spec/features/snippets/public_snippets_spec.rb2
-rw-r--r--spec/features/snippets/spam_snippets_spec.rb73
-rw-r--r--spec/features/snippets/user_creates_snippet_spec.rb8
-rw-r--r--spec/features/users/signup_spec.rb257
-rw-r--r--spec/finders/access_requests_finder_spec.rb6
-rw-r--r--spec/finders/admin/projects_finder_spec.rb2
-rw-r--r--spec/finders/artifacts_finder_spec.rb31
-rw-r--r--spec/finders/autocomplete/move_to_project_finder_spec.rb2
-rw-r--r--spec/finders/autocomplete/users_finder_spec.rb2
-rw-r--r--spec/finders/boards/visits_finder_spec.rb8
-rw-r--r--spec/finders/branches_finder_spec.rb2
-rw-r--r--spec/finders/clusters/knative_services_finder_spec.rb1
-rw-r--r--spec/finders/clusters/kubernetes_namespace_finder_spec.rb6
-rw-r--r--spec/finders/clusters_finder_spec.rb2
-rw-r--r--spec/finders/concerns/finder_methods_spec.rb2
-rw-r--r--spec/finders/concerns/finder_with_cross_project_access_spec.rb3
-rw-r--r--spec/finders/contributed_projects_finder_spec.rb2
-rw-r--r--spec/finders/environments_finder_spec.rb2
-rw-r--r--spec/finders/events_finder_spec.rb2
-rw-r--r--spec/finders/fork_projects_finder_spec.rb2
-rw-r--r--spec/finders/group_descendants_finder_spec.rb2
-rw-r--r--spec/finders/group_members_finder_spec.rb4
-rw-r--r--spec/finders/group_projects_finder_spec.rb2
-rw-r--r--spec/finders/groups_finder_spec.rb2
-rw-r--r--spec/finders/issues_finder_spec.rb2
-rw-r--r--spec/finders/joined_groups_finder_spec.rb2
-rw-r--r--spec/finders/labels_finder_spec.rb2
-rw-r--r--spec/finders/license_template_finder_spec.rb2
-rw-r--r--spec/finders/members_finder_spec.rb12
-rw-r--r--spec/finders/merge_request_target_project_finder_spec.rb2
-rw-r--r--spec/finders/merge_requests_finder_spec.rb2
-rw-r--r--spec/finders/milestones_finder_spec.rb2
-rw-r--r--spec/finders/notes_finder_spec.rb2
-rw-r--r--spec/finders/personal_access_tokens_finder_spec.rb2
-rw-r--r--spec/finders/personal_projects_finder_spec.rb2
-rw-r--r--spec/finders/pipeline_schedules_finder_spec.rb2
-rw-r--r--spec/finders/pipelines_finder_spec.rb2
-rw-r--r--spec/finders/projects_finder_spec.rb2
-rw-r--r--spec/finders/runner_jobs_finder_spec.rb2
-rw-r--r--spec/finders/snippets_finder_spec.rb81
-rw-r--r--spec/finders/tags_finder_spec.rb2
-rw-r--r--spec/finders/template_finder_spec.rb2
-rw-r--r--spec/finders/todos_finder_spec.rb102
-rw-r--r--spec/finders/user_finder_spec.rb32
-rw-r--r--spec/finders/user_recent_events_finder_spec.rb2
-rw-r--r--spec/finders/users_finder_spec.rb2
-rw-r--r--spec/fixtures/api/schemas/deployment.json3
-rw-r--r--spec/fixtures/api/schemas/evidences/evidence.json11
-rw-r--r--spec/fixtures/api/schemas/evidences/issue.json25
-rw-r--r--spec/fixtures/api/schemas/evidences/milestone.json27
-rw-r--r--spec/fixtures/api/schemas/evidences/project.json16
-rw-r--r--spec/fixtures/api/schemas/evidences/release.json25
-rw-r--r--spec/fixtures/api/schemas/job/build_trace.json31
-rw-r--r--spec/fixtures/api/schemas/job/build_trace_line.json18
-rw-r--r--spec/fixtures/api/schemas/job/build_trace_line_content.json11
-rw-r--r--spec/fixtures/api/schemas/list.json3
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/board.json3
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/deployment.json5
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/environment.json3
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/release.json10
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/release/release_for_guest.json13
-rw-r--r--spec/fixtures/audio_sample.wavbin0 -> 127020 bytes
-rw-r--r--spec/fixtures/emails/auto_reply.eml2
-rw-r--r--spec/fixtures/emails/auto_submitted.eml21
-rw-r--r--spec/fixtures/lib/gitlab/import_export/project.group.json (renamed from spec/lib/gitlab/import_export/project.group.json)2
-rw-r--r--spec/fixtures/lib/gitlab/import_export/project.json (renamed from spec/lib/gitlab/import_export/project.json)37
-rw-r--r--spec/fixtures/lib/gitlab/import_export/project.light.json (renamed from spec/lib/gitlab/import_export/project.light.json)0
-rw-r--r--spec/fixtures/lib/gitlab/import_export/project.milestone-iid.json (renamed from spec/lib/gitlab/import_export/project.milestone-iid.json)0
-rw-r--r--spec/fixtures/markdown.md.erb4
-rw-r--r--spec/fixtures/spdx.json1
-rw-r--r--spec/frontend/clusters/stores/clusters_store_spec.js3
-rw-r--r--spec/frontend/commit/commit_pipeline_status_component_spec.js179
-rw-r--r--spec/frontend/create_cluster/eks_cluster/components/cluster_form_dropdown_spec.js25
-rw-r--r--spec/frontend/create_cluster/eks_cluster/components/eks_cluster_configuration_form_spec.js457
-rw-r--r--spec/frontend/create_cluster/eks_cluster/components/region_dropdown_spec.js55
-rw-r--r--spec/frontend/create_cluster/eks_cluster/components/role_name_dropdown_spec.js43
-rw-r--r--spec/frontend/create_cluster/eks_cluster/store/actions_spec.js60
-rw-r--r--spec/frontend/create_cluster/eks_cluster/store/cluster_dropdown/actions_spec.js95
-rw-r--r--spec/frontend/create_cluster/eks_cluster/store/cluster_dropdown/mutations_spec.js36
-rw-r--r--spec/frontend/create_cluster/eks_cluster/store/mutations_spec.js62
-rw-r--r--spec/frontend/error_tracking/utils_spec.js27
-rw-r--r--spec/frontend/fixtures/abuse_reports.rb2
-rw-r--r--spec/frontend/fixtures/admin_users.rb2
-rw-r--r--spec/frontend/fixtures/application_settings.rb2
-rw-r--r--spec/frontend/fixtures/autocomplete_sources.rb4
-rw-r--r--spec/frontend/fixtures/blob.rb2
-rw-r--r--spec/frontend/fixtures/boards.rb2
-rw-r--r--spec/frontend/fixtures/branches.rb2
-rw-r--r--spec/frontend/fixtures/clusters.rb2
-rw-r--r--spec/frontend/fixtures/commit.rb2
-rw-r--r--spec/frontend/fixtures/deploy_keys.rb2
-rw-r--r--spec/frontend/fixtures/groups.rb2
-rw-r--r--spec/frontend/fixtures/issues.rb2
-rw-r--r--spec/frontend/fixtures/jobs.rb2
-rw-r--r--spec/frontend/fixtures/labels.rb2
-rw-r--r--spec/frontend/fixtures/merge_requests.rb2
-rw-r--r--spec/frontend/fixtures/merge_requests_diffs.rb2
-rw-r--r--spec/frontend/fixtures/pipeline_schedules.rb2
-rw-r--r--spec/frontend/fixtures/pipelines.rb2
-rw-r--r--spec/frontend/fixtures/projects.rb2
-rw-r--r--spec/frontend/fixtures/prometheus_service.rb2
-rw-r--r--spec/frontend/fixtures/raw.rb2
-rw-r--r--spec/frontend/fixtures/search.rb2
-rw-r--r--spec/frontend/fixtures/services.rb2
-rw-r--r--spec/frontend/fixtures/sessions.rb2
-rw-r--r--spec/frontend/fixtures/snippet.rb2
-rw-r--r--spec/frontend/fixtures/static/environments_logs.html109
-rw-r--r--spec/frontend/fixtures/todos.rb2
-rw-r--r--spec/frontend/fixtures/u2f.rb2
-rw-r--r--spec/frontend/helpers/dom_shims/README.md12
-rw-r--r--spec/frontend/helpers/dom_shims/get_client_rects.js50
-rw-r--r--spec/frontend/helpers/dom_shims/get_client_rects_spec.js71
-rw-r--r--spec/frontend/helpers/dom_shims/index.js1
-rw-r--r--spec/frontend/helpers/test_constants.js7
-rw-r--r--spec/frontend/helpers/tracking_helper.js25
-rw-r--r--spec/frontend/helpers/vue_resource_helper.js11
-rw-r--r--spec/frontend/ide/components/branches/search_list_spec.js81
-rw-r--r--spec/frontend/ide/components/error_message_spec.js122
-rw-r--r--spec/frontend/ide/components/file_templates/dropdown_spec.js175
-rw-r--r--spec/frontend/ide/components/jobs/list_spec.js115
-rw-r--r--spec/frontend/ide/components/merge_requests/list_spec.js214
-rw-r--r--spec/frontend/ide/components/pipelines/__snapshots__/list_spec.js.snap15
-rw-r--r--spec/frontend/ide/components/pipelines/list_spec.js193
-rw-r--r--spec/frontend/ide/lib/files_spec.js4
-rw-r--r--spec/frontend/ide/mock_data.js228
-rw-r--r--spec/frontend/ide/stores/integration_spec.js100
-rw-r--r--spec/frontend/import_projects/components/import_projects_table_spec.js8
-rw-r--r--spec/frontend/import_projects/store/actions_spec.js64
-rw-r--r--spec/frontend/issue_show/store_spec.js39
-rw-r--r--spec/frontend/issue_show/utils/update_description_spec.js24
-rw-r--r--spec/frontend/jobs/components/log/collapsible_section_spec.js73
-rw-r--r--spec/frontend/jobs/components/log/mock_data.js64
-rw-r--r--spec/frontend/jobs/store/mutations_spec.js76
-rw-r--r--spec/frontend/jobs/store/utils_spec.js350
-rw-r--r--spec/frontend/lib/utils/datetime_utility_spec.js29
-rw-r--r--spec/frontend/lib/utils/number_utility_spec.js13
-rw-r--r--spec/frontend/lib/utils/set_spec.js19
-rw-r--r--spec/frontend/lib/utils/suppress_ajax_errors_during_navigation_spec.js37
-rw-r--r--spec/frontend/lib/utils/url_utility_spec.js18
-rw-r--r--spec/frontend/monitoring/components/date_time_picker/date_time_picker_input_spec.js66
-rw-r--r--spec/frontend/monitoring/components/date_time_picker/date_time_picker_spec.js157
-rw-r--r--spec/frontend/monitoring/embed/embed_spec.js4
-rw-r--r--spec/frontend/monitoring/utils_spec.js54
-rw-r--r--spec/frontend/notes/components/__snapshots__/discussion_jump_to_next_button_spec.js.snap1
-rw-r--r--spec/frontend/pages/admin/users/components/__snapshots__/delete_user_modal_spec.js.snap63
-rw-r--r--spec/frontend/pages/admin/users/components/__snapshots__/user_operation_confirmation_modal_spec.js.snap33
-rw-r--r--spec/frontend/pages/admin/users/components/delete_user_modal_spec.js85
-rw-r--r--spec/frontend/pages/admin/users/components/stubs/modal_stub.js23
-rw-r--r--spec/frontend/pages/admin/users/components/user_modal_manager_spec.js148
-rw-r--r--spec/frontend/pages/admin/users/components/user_operation_confirmation_modal_spec.js47
-rw-r--r--spec/frontend/performance_bar/components/detailed_metric_spec.js111
-rw-r--r--spec/frontend/performance_bar/components/performance_bar_app_spec.js20
-rw-r--r--spec/frontend/performance_bar/components/request_selector_spec.js64
-rw-r--r--spec/frontend/performance_bar/components/request_warning_spec.js33
-rw-r--r--spec/frontend/registry/components/__snapshots__/group_empty_state_spec.js.snap61
-rw-r--r--spec/frontend/registry/components/__snapshots__/project_empty_state_spec.js.snap186
-rw-r--r--spec/frontend/registry/components/app_spec.js160
-rw-r--r--spec/frontend/registry/components/collapsible_container_spec.js127
-rw-r--r--spec/frontend/registry/components/group_empty_state_spec.js23
-rw-r--r--spec/frontend/registry/components/project_empty_state_spec.js27
-rw-r--r--spec/frontend/registry/components/table_registry_spec.js268
-rw-r--r--spec/frontend/registry/mock_data.js (renamed from spec/javascripts/registry/mock_data.js)0
-rw-r--r--spec/frontend/registry/stores/actions_spec.js203
-rw-r--r--spec/frontend/registry/stores/getters_spec.js (renamed from spec/frontend/registry/getters_spec.js)6
-rw-r--r--spec/frontend/registry/stores/mutations_spec.js (renamed from spec/javascripts/registry/stores/mutations_spec.js)11
-rw-r--r--spec/frontend/releases/components/milestone_list_spec.js56
-rw-r--r--spec/frontend/releases/components/release_block_spec.js120
-rw-r--r--spec/frontend/releases/detail/components/app_spec.js70
-rw-r--r--spec/frontend/releases/detail/store/actions_spec.js217
-rw-r--r--spec/frontend/releases/detail/store/mutations_spec.js119
-rw-r--r--spec/frontend/releases/list/components/__snapshots__/release_block_spec.js.snap332
-rw-r--r--spec/frontend/releases/list/components/release_block_spec.js266
-rw-r--r--spec/frontend/releases/mock_data.js7
-rw-r--r--spec/frontend/reports/store/utils_spec.js14
-rw-r--r--spec/frontend/repository/components/__snapshots__/last_commit_spec.js.snap107
-rw-r--r--spec/frontend/repository/components/last_commit_spec.js6
-rw-r--r--spec/frontend/sidebar/__snapshots__/confidential_issue_sidebar_spec.js.snap229
-rw-r--r--spec/frontend/sidebar/__snapshots__/todo_spec.js.snap37
-rw-r--r--spec/frontend/sidebar/confidential_issue_sidebar_spec.js167
-rw-r--r--spec/frontend/sidebar/todo_spec.js93
-rw-r--r--spec/frontend/test_setup.js2
-rw-r--r--spec/frontend/tracking_spec.js81
-rw-r--r--spec/frontend/vue_mr_widget/components/artifacts_list_app_spec.js121
-rw-r--r--spec/frontend/vue_mr_widget/components/artifacts_list_spec.js61
-rw-r--r--spec/frontend/vue_mr_widget/components/mock_data.js15
-rw-r--r--spec/frontend/vue_mr_widget/components/mr_collapsible_extension_spec.js99
-rw-r--r--spec/frontend/vue_mr_widget/stores/artifacts_list/getters_spec.js32
-rw-r--r--spec/frontend/vue_mr_widget/stores/artifacts_list/mutations_spec.js78
-rw-r--r--spec/frontend/vue_shared/components/changed_file_icon_spec.js5
-rw-r--r--spec/frontend/vue_shared/components/file_icon_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/modal_copy_button_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/recaptcha_eventhub_spec.js21
-rw-r--r--spec/frontend/vue_shared/components/recaptcha_modal_spec.js36
-rw-r--r--spec/frontend/vue_shared/directives/track_event_spec.js49
-rw-r--r--spec/frontend/vue_shared/gl_feature_flags_plugin_spec.js42
-rw-r--r--spec/frontend/vue_shared/mixins/gl_feature_flags_mixin_spec.js36
-rw-r--r--spec/frontend/vue_shared/plugins/global_toast_spec.js10
-rw-r--r--spec/graphql/mutations/concerns/mutations/resolves_group_spec.rb21
-rw-r--r--spec/graphql/mutations/concerns/mutations/resolves_project_spec.rb2
-rw-r--r--spec/graphql/mutations/merge_requests/set_wip_spec.rb2
-rw-r--r--spec/graphql/resolvers/concerns/resolves_pipelines_spec.rb2
-rw-r--r--spec/graphql/resolvers/issues_resolver_spec.rb2
-rw-r--r--spec/graphql/resolvers/last_commit_resolver_spec.rb41
-rw-r--r--spec/graphql/resolvers/merge_request_pipelines_resolver_spec.rb2
-rw-r--r--spec/graphql/resolvers/merge_requests_resolver_spec.rb2
-rw-r--r--spec/graphql/resolvers/metadata_resolver_spec.rb2
-rw-r--r--spec/graphql/resolvers/project_pipelines_resolver_spec.rb2
-rw-r--r--spec/graphql/resolvers/project_resolver_spec.rb2
-rw-r--r--spec/graphql/resolvers/todo_resolver_spec.rb113
-rw-r--r--spec/graphql/resolvers/tree_resolver_spec.rb2
-rw-r--r--spec/graphql/types/ci/detailed_status_type_spec.rb2
-rw-r--r--spec/graphql/types/ci/pipeline_type_spec.rb2
-rw-r--r--spec/graphql/types/commit_type_spec.rb7
-rw-r--r--spec/graphql/types/extended_issue_type_spec.rb21
-rw-r--r--spec/graphql/types/issue_type_spec.rb7
-rw-r--r--spec/graphql/types/merge_request_type_spec.rb6
-rw-r--r--spec/graphql/types/metadata_type_spec.rb2
-rw-r--r--spec/graphql/types/notes/diff_position_type_spec.rb2
-rw-r--r--spec/graphql/types/permission_types/base_permission_type_spec.rb2
-rw-r--r--spec/graphql/types/permission_types/issue_spec.rb2
-rw-r--r--spec/graphql/types/permission_types/merge_request_spec.rb2
-rw-r--r--spec/graphql/types/permission_types/merge_request_type_spec.rb2
-rw-r--r--spec/graphql/types/permission_types/note_spec.rb2
-rw-r--r--spec/graphql/types/permission_types/project_spec.rb2
-rw-r--r--spec/graphql/types/project_type_spec.rb38
-rw-r--r--spec/graphql/types/query_type_spec.rb4
-rw-r--r--spec/graphql/types/repository_type_spec.rb2
-rw-r--r--spec/graphql/types/time_type_spec.rb2
-rw-r--r--spec/graphql/types/todo_type_spec.rb13
-rw-r--r--spec/helpers/application_helper_spec.rb2
-rw-r--r--spec/helpers/auto_devops_helper_spec.rb2
-rw-r--r--spec/helpers/avatars_helper_spec.rb2
-rw-r--r--spec/helpers/award_emoji_helper_spec.rb2
-rw-r--r--spec/helpers/blame_helper_spec.rb2
-rw-r--r--spec/helpers/blob_helper_spec.rb30
-rw-r--r--spec/helpers/boards_helper_spec.rb2
-rw-r--r--spec/helpers/broadcast_messages_helper_spec.rb2
-rw-r--r--spec/helpers/button_helper_spec.rb4
-rw-r--r--spec/helpers/calendar_helper_spec.rb2
-rw-r--r--spec/helpers/ci_status_helper_spec.rb2
-rw-r--r--spec/helpers/commits_helper_spec.rb2
-rw-r--r--spec/helpers/components_helper_spec.rb2
-rw-r--r--spec/helpers/dashboard_helper_spec.rb2
-rw-r--r--spec/helpers/defer_script_tag_helper_spec.rb2
-rw-r--r--spec/helpers/diff_helper_spec.rb2
-rw-r--r--spec/helpers/emails_helper_spec.rb2
-rw-r--r--spec/helpers/environment_helper_spec.rb25
-rw-r--r--spec/helpers/events_helper_spec.rb2
-rw-r--r--spec/helpers/explore_helper_spec.rb2
-rw-r--r--spec/helpers/form_helper_spec.rb2
-rw-r--r--spec/helpers/git_helper_spec.rb2
-rw-r--r--spec/helpers/gitlab_routing_helper_spec.rb2
-rw-r--r--spec/helpers/graph_helper_spec.rb2
-rw-r--r--spec/helpers/groups_helper_spec.rb35
-rw-r--r--spec/helpers/hooks_helper_spec.rb2
-rw-r--r--spec/helpers/icons_helper_spec.rb2
-rw-r--r--spec/helpers/import_helper_spec.rb2
-rw-r--r--spec/helpers/instance_configuration_helper_spec.rb2
-rw-r--r--spec/helpers/issuables_helper_spec.rb29
-rw-r--r--spec/helpers/issues_helper_spec.rb2
-rw-r--r--spec/helpers/labels_helper_spec.rb2
-rw-r--r--spec/helpers/markup_helper_spec.rb8
-rw-r--r--spec/helpers/members_helper_spec.rb10
-rw-r--r--spec/helpers/merge_requests_helper_spec.rb1
-rw-r--r--spec/helpers/milestones_helper_spec.rb2
-rw-r--r--spec/helpers/milestones_routing_helper_spec.rb2
-rw-r--r--spec/helpers/namespaces_helper_spec.rb2
-rw-r--r--spec/helpers/nav_helper_spec.rb52
-rw-r--r--spec/helpers/notes_helper_spec.rb2
-rw-r--r--spec/helpers/notifications_helper_spec.rb2
-rw-r--r--spec/helpers/page_layout_helper_spec.rb2
-rw-r--r--spec/helpers/pagination_helper_spec.rb2
-rw-r--r--spec/helpers/preferences_helper_spec.rb2
-rw-r--r--spec/helpers/profiles_helper_spec.rb2
-rw-r--r--spec/helpers/projects_helper_spec.rb36
-rw-r--r--spec/helpers/releases_helper_spec.rb16
-rw-r--r--spec/helpers/rss_helper_spec.rb2
-rw-r--r--spec/helpers/runners_helper_spec.rb2
-rw-r--r--spec/helpers/search_helper_spec.rb21
-rw-r--r--spec/helpers/sidekiq_helper_spec.rb2
-rw-r--r--spec/helpers/snippets_helper_spec.rb2
-rw-r--r--spec/helpers/storage_helper_spec.rb2
-rw-r--r--spec/helpers/submodule_helper_spec.rb2
-rw-r--r--spec/helpers/tab_helper_spec.rb2
-rw-r--r--spec/helpers/time_helper_spec.rb2
-rw-r--r--spec/helpers/todos_helper_spec.rb2
-rw-r--r--spec/helpers/tree_helper_spec.rb2
-rw-r--r--spec/helpers/user_callouts_helper_spec.rb2
-rw-r--r--spec/helpers/users_helper_spec.rb2
-rw-r--r--spec/helpers/version_check_helper_spec.rb2
-rw-r--r--spec/helpers/visibility_level_helper_spec.rb2
-rw-r--r--spec/helpers/wiki_helper_spec.rb2
-rw-r--r--spec/initializers/google_api_client_spec.rb17
-rw-r--r--spec/initializers/lograge_spec.rb33
-rw-r--r--spec/javascripts/blob/balsamiq/balsamiq_viewer_spec.js63
-rw-r--r--spec/javascripts/blob/viewer/index_spec.js4
-rw-r--r--spec/javascripts/boards/board_card_spec.js14
-rw-r--r--spec/javascripts/boards/boards_store_spec.js132
-rw-r--r--spec/javascripts/boards/issue_card_spec.js18
-rw-r--r--spec/javascripts/boards/mock_data.js4
-rw-r--r--spec/javascripts/commit/commit_pipeline_status_component_spec.js106
-rw-r--r--spec/javascripts/create_cluster/.eslintrc.yml3
-rw-r--r--spec/javascripts/create_cluster/gke_cluster/components/gke_project_id_dropdown_spec.js11
-rw-r--r--spec/javascripts/create_cluster/gke_cluster/stores/actions_spec.js10
-rw-r--r--spec/javascripts/flash_spec.js12
-rw-r--r--spec/javascripts/frequent_items/components/app_spec.js7
-rw-r--r--spec/javascripts/header_spec.js8
-rw-r--r--spec/javascripts/helpers/tracking_helper.js25
-rw-r--r--spec/javascripts/helpers/vue_resource_helper.js11
-rw-r--r--spec/javascripts/ide/components/branches/search_list_spec.js80
-rw-r--r--spec/javascripts/ide/components/commit_sidebar/list_item_spec.js34
-rw-r--r--spec/javascripts/ide/components/error_message_spec.js106
-rw-r--r--spec/javascripts/ide/components/file_row_extra_spec.js21
-rw-r--r--spec/javascripts/ide/components/file_templates/dropdown_spec.js201
-rw-r--r--spec/javascripts/ide/components/ide_tree_list_spec.js14
-rw-r--r--spec/javascripts/ide/components/jobs/list_spec.js67
-rw-r--r--spec/javascripts/ide/components/merge_requests/list_spec.js159
-rw-r--r--spec/javascripts/ide/components/pipelines/list_spec.js137
-rw-r--r--spec/javascripts/ide/components/repo_editor_spec.js83
-rw-r--r--spec/javascripts/ide/mock_data.js227
-rw-r--r--spec/javascripts/ide/stores/actions/project_spec.js197
-rw-r--r--spec/javascripts/ide/stores/actions_spec.js351
-rw-r--r--spec/javascripts/ide/stores/modules/commit/actions_spec.js106
-rw-r--r--spec/javascripts/ide/stores/mutations/file_spec.js19
-rw-r--r--spec/javascripts/ide/stores/mutations_spec.js389
-rw-r--r--spec/javascripts/ide/stores/utils_spec.js225
-rw-r--r--spec/javascripts/integrations/integration_settings_form_spec.js1
-rw-r--r--spec/javascripts/issue_show/components/app_spec.js68
-rw-r--r--spec/javascripts/jobs/components/environments_block_spec.js212
-rw-r--r--spec/javascripts/jobs/components/job_log_spec.js57
-rw-r--r--spec/javascripts/labels_issue_sidebar_spec.js88
-rw-r--r--spec/javascripts/lazy_loader_spec.js18
-rw-r--r--spec/javascripts/lib/utils/common_utils_spec.js10
-rw-r--r--spec/javascripts/line_highlighter_spec.js28
-rw-r--r--spec/javascripts/monitoring/charts/time_series_spec.js22
-rw-r--r--spec/javascripts/monitoring/components/dashboard_spec.js139
-rw-r--r--spec/javascripts/monitoring/store/actions_spec.js2
-rw-r--r--spec/javascripts/monitoring/store/mutations_spec.js7
-rw-r--r--spec/javascripts/monitoring/store/utils_spec.js41
-rw-r--r--spec/javascripts/monitoring/utils_spec.js249
-rw-r--r--spec/javascripts/notes/components/discussion_filter_spec.js23
-rw-r--r--spec/javascripts/notes/stores/mutation_spec.js17
-rw-r--r--spec/javascripts/pager_spec.js20
-rw-r--r--spec/javascripts/performance_bar/components/detailed_metric_spec.js109
-rw-r--r--spec/javascripts/performance_bar/components/performance_bar_app_spec.js29
-rw-r--r--spec/javascripts/performance_bar/components/request_selector_spec.js46
-rw-r--r--spec/javascripts/pipelines/graph/graph_component_spec.js203
-rw-r--r--spec/javascripts/pipelines/graph/linked_pipeline_spec.js116
-rw-r--r--spec/javascripts/pipelines/graph/linked_pipelines_column_spec.js38
-rw-r--r--spec/javascripts/pipelines/graph/linked_pipelines_mock_data.js407
-rw-r--r--spec/javascripts/pipelines/linked_pipelines_mock.json3532
-rw-r--r--spec/javascripts/pipelines/stores/pipeline.json167
-rw-r--r--spec/javascripts/pipelines/stores/pipeline_store.js165
-rw-r--r--spec/javascripts/pipelines/stores/pipeline_with_triggered.json381
-rw-r--r--spec/javascripts/pipelines/stores/pipeline_with_triggered_by.json379
-rw-r--r--spec/javascripts/pipelines/stores/pipeline_with_triggered_triggered_by.json452
-rw-r--r--spec/javascripts/registry/components/app_spec.js129
-rw-r--r--spec/javascripts/registry/components/collapsible_container_spec.js87
-rw-r--r--spec/javascripts/registry/components/table_registry_spec.js189
-rw-r--r--spec/javascripts/registry/stores/actions_spec.js132
-rw-r--r--spec/javascripts/releases/components/release_block_spec.js170
-rw-r--r--spec/javascripts/releases/list/components/app_spec.js (renamed from spec/javascripts/releases/components/app_spec.js)6
-rw-r--r--spec/javascripts/releases/list/store/actions_spec.js (renamed from spec/javascripts/releases/store/actions_spec.js)8
-rw-r--r--spec/javascripts/releases/list/store/helpers.js (renamed from spec/javascripts/releases/store/helpers.js)2
-rw-r--r--spec/javascripts/releases/list/store/mutations_spec.js (renamed from spec/javascripts/releases/store/mutations_spec.js)8
-rw-r--r--spec/javascripts/reports/components/grouped_test_reports_app_spec.js10
-rw-r--r--spec/javascripts/sidebar/assignee_title_spec.js14
-rw-r--r--spec/javascripts/sidebar/components/time_tracking/time_tracker_spec.js14
-rw-r--r--spec/javascripts/sidebar/confidential_issue_sidebar_spec.js78
-rw-r--r--spec/javascripts/sidebar/lock/lock_issue_sidebar_spec.js13
-rw-r--r--spec/javascripts/sidebar/subscriptions_spec.js9
-rw-r--r--spec/javascripts/sidebar/todo_spec.js171
-rw-r--r--spec/javascripts/test_bundle.js15
-rw-r--r--spec/javascripts/test_constants.js8
-rw-r--r--spec/javascripts/todos_spec.js51
-rw-r--r--spec/javascripts/vue_mr_widget/components/deployment_spec.js6
-rw-r--r--spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_container_spec.js7
-rw-r--r--spec/javascripts/vue_mr_widget/components/states/mr_widget_wip_spec.js7
-rw-r--r--spec/javascripts/vue_mr_widget/mock_data.js1
-rw-r--r--spec/javascripts/vue_mr_widget/stores/artifacts_list/actions_spec.js165
-rw-r--r--spec/javascripts/vue_shared/components/clipboard_button_spec.js6
-rw-r--r--spec/javascripts/vue_shared/components/deprecated_modal_2_spec.js (renamed from spec/javascripts/vue_shared/components/gl_modal_spec.js)14
-rw-r--r--spec/javascripts/vue_shared/components/file_row_spec.js13
-rw-r--r--spec/javascripts/vue_shared/components/icon_spec.js8
-rw-r--r--spec/javascripts/vue_shared/components/project_selector/project_selector_spec.js5
-rw-r--r--spec/javascripts/zen_mode_spec.js2
-rw-r--r--spec/lib/api/helpers/graphql_helpers_spec.rb44
-rw-r--r--spec/lib/backup/files_spec.rb1
-rw-r--r--spec/lib/backup/manager_spec.rb45
-rw-r--r--spec/lib/backup/repository_spec.rb25
-rw-r--r--spec/lib/banzai/filter/audio_link_filter_spec.rb120
-rw-r--r--spec/lib/banzai/filter/project_reference_filter_spec.rb1
-rw-r--r--spec/lib/banzai/filter/relative_link_filter_spec.rb51
-rw-r--r--spec/lib/banzai/filter/table_of_contents_filter_spec.rb4
-rw-r--r--spec/lib/banzai/filter/user_reference_filter_spec.rb1
-rw-r--r--spec/lib/banzai/filter/video_link_filter_spec.rb95
-rw-r--r--spec/lib/banzai/filter/wiki_link_filter_spec.rb8
-rw-r--r--spec/lib/banzai/pipeline/wiki_pipeline_spec.rb24
-rw-r--r--spec/lib/banzai/reference_parser/commit_parser_spec.rb1
-rw-r--r--spec/lib/banzai/reference_parser/commit_range_parser_spec.rb1
-rw-r--r--spec/lib/banzai/reference_parser/external_issue_parser_spec.rb1
-rw-r--r--spec/lib/banzai/reference_parser/label_parser_spec.rb1
-rw-r--r--spec/lib/banzai/reference_parser/mentioned_user_parser_spec.rb51
-rw-r--r--spec/lib/banzai/reference_parser/mentioned_users_by_group_parser_spec.rb46
-rw-r--r--spec/lib/banzai/reference_parser/mentioned_users_by_project_parser_spec.rb46
-rw-r--r--spec/lib/banzai/reference_parser/merge_request_parser_spec.rb1
-rw-r--r--spec/lib/banzai/reference_parser/milestone_parser_spec.rb1
-rw-r--r--spec/lib/banzai/reference_parser/project_parser_spec.rb1
-rw-r--r--spec/lib/banzai/reference_parser/snippet_parser_spec.rb1
-rw-r--r--spec/lib/banzai/reference_parser/user_parser_spec.rb1
-rw-r--r--spec/lib/container_registry/client_spec.rb65
-rw-r--r--spec/lib/container_registry/tag_spec.rb4
-rw-r--r--spec/lib/event_filter_spec.rb6
-rw-r--r--spec/lib/gitlab/analytics/cycle_analytics/base_query_builder_spec.rb62
-rw-r--r--spec/lib/gitlab/analytics/cycle_analytics/records_fetcher_spec.rb131
-rw-r--r--spec/lib/gitlab/analytics/cycle_analytics/stage_events/code_stage_start_spec.rb22
-rw-r--r--spec/lib/gitlab/analytics/cycle_analytics/stage_events/issue_created_spec.rb7
-rw-r--r--spec/lib/gitlab/analytics/cycle_analytics/stage_events/issue_first_mentioned_in_commit_spec.rb7
-rw-r--r--spec/lib/gitlab/analytics/cycle_analytics/stage_events/issue_stage_end_spec.rb7
-rw-r--r--spec/lib/gitlab/analytics/cycle_analytics/stage_events/merge_request_created_spec.rb7
-rw-r--r--spec/lib/gitlab/analytics/cycle_analytics/stage_events/merge_request_first_deployed_to_production_spec.rb7
-rw-r--r--spec/lib/gitlab/analytics/cycle_analytics/stage_events/merge_request_last_build_finished_spec.rb7
-rw-r--r--spec/lib/gitlab/analytics/cycle_analytics/stage_events/merge_request_last_build_started_spec.rb7
-rw-r--r--spec/lib/gitlab/analytics/cycle_analytics/stage_events/merge_request_merged_spec.rb7
-rw-r--r--spec/lib/gitlab/analytics/cycle_analytics/stage_events/plan_stage_start_spec.rb24
-rw-r--r--spec/lib/gitlab/analytics/cycle_analytics/stage_events/stage_event_spec.rb7
-rw-r--r--spec/lib/gitlab/auth/current_user_mode_spec.rb159
-rw-r--r--spec/lib/gitlab/auth/user_access_denied_reason_spec.rb8
-rw-r--r--spec/lib/gitlab/auth_spec.rb74
-rw-r--r--spec/lib/gitlab/background_migration/legacy_upload_mover_spec.rb2
-rw-r--r--spec/lib/gitlab/background_migration/legacy_uploads_migrator_spec.rb2
-rw-r--r--spec/lib/gitlab/background_migration/migrate_pages_metadata_spec.rb44
-rw-r--r--spec/lib/gitlab/badge/pipeline/template_spec.rb2
-rw-r--r--spec/lib/gitlab/bare_repository_import/importer_spec.rb8
-rw-r--r--spec/lib/gitlab/bitbucket_import/importer_spec.rb4
-rw-r--r--spec/lib/gitlab/ci/ansi2json/line_spec.rb168
-rw-r--r--spec/lib/gitlab/ci/ansi2json/parser_spec.rb30
-rw-r--r--spec/lib/gitlab/ci/ansi2json/style_spec.rb166
-rw-r--r--spec/lib/gitlab/ci/ansi2json_spec.rb544
-rw-r--r--spec/lib/gitlab/ci/build/artifacts/metadata/entry_spec.rb15
-rw-r--r--spec/lib/gitlab/ci/build/artifacts/metadata_spec.rb4
-rw-r--r--spec/lib/gitlab/ci/build/rules/rule/clause/changes_spec.rb17
-rw-r--r--spec/lib/gitlab/ci/build/rules/rule/clause/exists_spec.rb27
-rw-r--r--spec/lib/gitlab/ci/config/edge_stages_injector_spec.rb112
-rw-r--r--spec/lib/gitlab/ci/config/entry/cache_spec.rb1
-rw-r--r--spec/lib/gitlab/ci/config/entry/coverage_spec.rb7
-rw-r--r--spec/lib/gitlab/ci/config/entry/root_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/config/entry/rules/rule_spec.rb52
-rw-r--r--spec/lib/gitlab/ci/config/entry/stages_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/config/external/context_spec.rb128
-rw-r--r--spec/lib/gitlab/ci/config/external/file/base_spec.rb10
-rw-r--r--spec/lib/gitlab/ci/config/external/file/local_spec.rb11
-rw-r--r--spec/lib/gitlab/ci/config/external/file/project_spec.rb8
-rw-r--r--spec/lib/gitlab/ci/config/external/file/remote_spec.rb12
-rw-r--r--spec/lib/gitlab/ci/config/external/file/template_spec.rb12
-rw-r--r--spec/lib/gitlab/ci/config/external/mapper_spec.rb8
-rw-r--r--spec/lib/gitlab/ci/config/external/processor_spec.rb8
-rw-r--r--spec/lib/gitlab/ci/config_spec.rb96
-rw-r--r--spec/lib/gitlab/ci/parsers/test/junit_spec.rb68
-rw-r--r--spec/lib/gitlab/ci/pipeline/seed/build_spec.rb18
-rw-r--r--spec/lib/gitlab/ci/pipeline/seed/deployment_spec.rb76
-rw-r--r--spec/lib/gitlab/ci/pipeline/seed/environment_spec.rb43
-rw-r--r--spec/lib/gitlab/ci/status/composite_spec.rb61
-rw-r--r--spec/lib/gitlab/ci/status/external/factory_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/status/factory_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/status/pipeline/factory_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/status/preparing_spec.rb4
-rw-r--r--spec/lib/gitlab/ci/status/stage/factory_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/trace/stream_spec.rb54
-rw-r--r--spec/lib/gitlab/ci/yaml_processor_spec.rb46
-rw-r--r--spec/lib/gitlab/cleanup/project_uploads_spec.rb1
-rw-r--r--spec/lib/gitlab/cluster/mixins/puma_cluster_spec.rb96
-rw-r--r--spec/lib/gitlab/cluster/mixins/unicorn_http_server_spec.rb112
-rw-r--r--spec/lib/gitlab/config/entry/simplifiable_spec.rb6
-rw-r--r--spec/lib/gitlab/cycle_analytics/code_stage_spec.rb12
-rw-r--r--spec/lib/gitlab/cycle_analytics/issue_stage_spec.rb12
-rw-r--r--spec/lib/gitlab/cycle_analytics/plan_stage_spec.rb12
-rw-r--r--spec/lib/gitlab/cycle_analytics/shared_stage_spec.rb38
-rw-r--r--spec/lib/gitlab/cycle_analytics/stage_summary_spec.rb84
-rw-r--r--spec/lib/gitlab/cycle_analytics/staging_stage_spec.rb5
-rw-r--r--spec/lib/gitlab/cycle_analytics/test_stage_spec.rb30
-rw-r--r--spec/lib/gitlab/daemon_spec.rb26
-rw-r--r--spec/lib/gitlab/danger/helper_spec.rb24
-rw-r--r--spec/lib/gitlab/danger/roulette_spec.rb16
-rw-r--r--spec/lib/gitlab/danger/teammate_spec.rb75
-rw-r--r--spec/lib/gitlab/data_builder/push_spec.rb8
-rw-r--r--spec/lib/gitlab/database_spec.rb14
-rw-r--r--spec/lib/gitlab/diff/file_collection/merge_request_diff_batch_spec.rb126
-rw-r--r--spec/lib/gitlab/diff/position_collection_spec.rb86
-rw-r--r--spec/lib/gitlab/diff/position_spec.rb20
-rw-r--r--spec/lib/gitlab/discussions_diff/file_collection_spec.rb8
-rw-r--r--spec/lib/gitlab/downtime_check_spec.rb1
-rw-r--r--spec/lib/gitlab/email/message/repository_push_spec.rb18
-rw-r--r--spec/lib/gitlab/email/receiver_spec.rb10
-rw-r--r--spec/lib/gitlab/experimentation_spec.rb157
-rw-r--r--spec/lib/gitlab/favicon_spec.rb1
-rw-r--r--spec/lib/gitlab/file_markdown_link_builder_spec.rb56
-rw-r--r--spec/lib/gitlab/file_type_detection_spec.rb562
-rw-r--r--spec/lib/gitlab/gfm/uploads_rewriter_spec.rb2
-rw-r--r--spec/lib/gitlab/git/branch_spec.rb1
-rw-r--r--spec/lib/gitlab/git/changes_spec.rb81
-rw-r--r--spec/lib/gitlab/git/commit_spec.rb9
-rw-r--r--spec/lib/gitlab/git/diff_collection_spec.rb115
-rw-r--r--spec/lib/gitlab/git/repository_spec.rb44
-rw-r--r--spec/lib/gitlab/git_access_spec.rb13
-rw-r--r--spec/lib/gitlab/gitaly_client/commit_service_spec.rb25
-rw-r--r--spec/lib/gitlab/gitaly_client/conflict_files_stitcher_spec.rb2
-rw-r--r--spec/lib/gitlab/gitaly_client/repository_service_spec.rb22
-rw-r--r--spec/lib/gitlab/gitaly_client/storage_service_spec.rb13
-rw-r--r--spec/lib/gitlab/gitaly_client/storage_settings_spec.rb32
-rw-r--r--spec/lib/gitlab/gitaly_client_spec.rb10
-rw-r--r--spec/lib/gitlab/github_import/importer/pull_request_importer_spec.rb3
-rw-r--r--spec/lib/gitlab/github_import/importer/releases_importer_spec.rb56
-rw-r--r--spec/lib/gitlab/gitlab_import/client_spec.rb3
-rw-r--r--spec/lib/gitlab/gl_repository/repo_type_spec.rb30
-rw-r--r--spec/lib/gitlab/gpg/commit_spec.rb28
-rw-r--r--spec/lib/gitlab/graphs/commits_spec.rb3
-rw-r--r--spec/lib/gitlab/health_checks/gitaly_check_spec.rb5
-rw-r--r--spec/lib/gitlab/health_checks/probes/collection_spec.rb62
-rw-r--r--spec/lib/gitlab/health_checks/prometheus_text_format_spec.rb41
-rw-r--r--spec/lib/gitlab/health_checks/puma_check_spec.rb65
-rw-r--r--spec/lib/gitlab/health_checks/simple_check_shared.rb13
-rw-r--r--spec/lib/gitlab/health_checks/unicorn_check_spec.rb63
-rw-r--r--spec/lib/gitlab/hook_data/issue_builder_spec.rb3
-rw-r--r--spec/lib/gitlab/import/merge_request_creator_spec.rb7
-rw-r--r--spec/lib/gitlab/import_export/after_export_strategies/base_after_export_strategy_spec.rb26
-rw-r--r--spec/lib/gitlab/import_export/all_models.yml34
-rw-r--r--spec/lib/gitlab/import_export/fast_hash_serializer_spec.rb7
-rw-r--r--spec/lib/gitlab/import_export/model_configuration_spec.rb4
-rw-r--r--spec/lib/gitlab/import_export/project_tree_restorer_spec.rb94
-rw-r--r--spec/lib/gitlab/import_export/relation_factory_spec.rb4
-rw-r--r--spec/lib/gitlab/import_export/relation_rename_service_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/repo_saver_spec.rb12
-rw-r--r--spec/lib/gitlab/import_export/safe_model_attributes.yml23
-rw-r--r--spec/lib/gitlab/import_export/shared_spec.rb40
-rw-r--r--spec/lib/gitlab/import_export/uploads_manager_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/uploads_restorer_spec.rb4
-rw-r--r--spec/lib/gitlab/import_export/wiki_repo_saver_spec.rb12
-rw-r--r--spec/lib/gitlab/kubernetes/helm/delete_command_spec.rb4
-rw-r--r--spec/lib/gitlab/kubernetes/helm/install_command_spec.rb14
-rw-r--r--spec/lib/gitlab/kubernetes/helm/reset_command_spec.rb2
-rw-r--r--spec/lib/gitlab/kubernetes/kube_client_spec.rb2
-rw-r--r--spec/lib/gitlab/legacy_github_import/release_formatter_spec.rb8
-rw-r--r--spec/lib/gitlab/lets_encrypt_spec.rb12
-rw-r--r--spec/lib/gitlab/lfs_token_spec.rb40
-rw-r--r--spec/lib/gitlab/metrics/exporter/base_exporter_spec.rb180
-rw-r--r--spec/lib/gitlab/metrics/exporter/sidekiq_exporter_spec.rb65
-rw-r--r--spec/lib/gitlab/metrics/exporter/web_exporter_spec.rb64
-rw-r--r--spec/lib/gitlab/metrics/requests_rack_middleware_spec.rb14
-rw-r--r--spec/lib/gitlab/metrics/samplers/puma_sampler_spec.rb1
-rw-r--r--spec/lib/gitlab/metrics/sidekiq_metrics_exporter_spec.rb105
-rw-r--r--spec/lib/gitlab/metrics/system_spec.rb40
-rw-r--r--spec/lib/gitlab/metrics/transaction_spec.rb8
-rw-r--r--spec/lib/gitlab/middleware/read_only_spec.rb7
-rw-r--r--spec/lib/gitlab/pages_client_spec.rb174
-rw-r--r--spec/lib/gitlab/patch/prependable_spec.rb10
-rw-r--r--spec/lib/gitlab/path_regex_spec.rb2
-rw-r--r--spec/lib/gitlab/phabricator_import/worker_state_spec.rb1
-rw-r--r--spec/lib/gitlab/reference_extractor_spec.rb4
-rw-r--r--spec/lib/gitlab/regex_spec.rb11
-rw-r--r--spec/lib/gitlab/request_context_spec.rb1
-rw-r--r--spec/lib/gitlab/sanitizers/exif_spec.rb2
-rw-r--r--spec/lib/gitlab/search_results_spec.rb4
-rw-r--r--spec/lib/gitlab/shell_spec.rb25
-rw-r--r--spec/lib/gitlab/sidekiq_daemon/memory_killer_spec.rb501
-rw-r--r--spec/lib/gitlab/sidekiq_daemon/monitor_spec.rb50
-rw-r--r--spec/lib/gitlab/sidekiq_logging/exception_handler_spec.rb44
-rw-r--r--spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb52
-rw-r--r--spec/lib/gitlab/sidekiq_middleware/memory_killer_spec.rb1
-rw-r--r--spec/lib/gitlab/sidekiq_middleware/metrics_spec.rb13
-rw-r--r--spec/lib/gitlab/sidekiq_middleware/monitor_spec.rb2
-rw-r--r--spec/lib/gitlab/slash_commands/presenters/access_spec.rb22
-rw-r--r--spec/lib/gitlab/snippet_search_results_spec.rb15
-rw-r--r--spec/lib/gitlab/submodule_links_spec.rb18
-rw-r--r--spec/lib/gitlab/tracking/incident_management_spec.rb78
-rw-r--r--spec/lib/gitlab/tracking_spec.rb105
-rw-r--r--spec/lib/gitlab/url_blocker_spec.rb8
-rw-r--r--spec/lib/gitlab/usage_data_spec.rb73
-rw-r--r--spec/lib/gitlab/utils/inline_hash_spec.rb70
-rw-r--r--spec/lib/gitlab/utils/override_spec.rb6
-rw-r--r--spec/lib/gitlab/utils/safe_inline_hash_spec.rb35
-rw-r--r--spec/lib/gitlab/utils/sanitize_node_link_spec.rb5
-rw-r--r--spec/lib/gitlab_spec.rb77
-rw-r--r--spec/lib/google_api/cloud_platform/client_spec.rb103
-rw-r--r--spec/lib/grafana/client_spec.rb107
-rw-r--r--spec/lib/json_web_token/token_spec.rb1
-rw-r--r--spec/lib/omni_auth/strategies/jwt_spec.rb1
-rw-r--r--spec/lib/quality/test_level_spec.rb28
-rw-r--r--spec/lib/uploaded_file_spec.rb56
-rw-r--r--spec/mailers/emails/pipelines_spec.rb69
-rw-r--r--spec/mailers/emails/releases_spec.rb52
-rw-r--r--spec/mailers/notify_spec.rb8
-rw-r--r--spec/migrations/README.md138
-rw-r--r--spec/migrations/backfill_releases_table_updated_at_and_add_not_null_constraints_to_timestamps_spec.rb57
-rw-r--r--spec/migrations/migrate_code_owner_approval_status_to_protected_branches_in_batches_spec.rb63
-rw-r--r--spec/migrations/schedule_pages_metadata_migration_spec.rb29
-rw-r--r--spec/migrations/sync_issuables_state_id_spec.rb37
-rw-r--r--spec/models/application_setting_spec.rb12
-rw-r--r--spec/models/aws/role_spec.rb34
-rw-r--r--spec/models/blob_spec.rb16
-rw-r--r--spec/models/ci/build_metadata_spec.rb2
-rw-r--r--spec/models/ci/build_spec.rb50
-rw-r--r--spec/models/ci/build_trace_spec.rb53
-rw-r--r--spec/models/ci/group_spec.rb26
-rw-r--r--spec/models/ci/legacy_stage_spec.rb13
-rw-r--r--spec/models/ci/persistent_ref_spec.rb96
-rw-r--r--spec/models/ci/pipeline_spec.rb227
-rw-r--r--spec/models/ci/runner_spec.rb2
-rw-r--r--spec/models/ci/sources/pipeline_spec.rb19
-rw-r--r--spec/models/ci/stage_spec.rb2
-rw-r--r--spec/models/clusters/applications/cert_manager_spec.rb2
-rw-r--r--spec/models/clusters/applications/helm_spec.rb10
-rw-r--r--spec/models/clusters/applications/ingress_spec.rb4
-rw-r--r--spec/models/clusters/applications/jupyter_spec.rb2
-rw-r--r--spec/models/clusters/applications/knative_spec.rb9
-rw-r--r--spec/models/clusters/applications/prometheus_spec.rb12
-rw-r--r--spec/models/clusters/cluster_spec.rb83
-rw-r--r--spec/models/clusters/clusters_hierarchy_spec.rb36
-rw-r--r--spec/models/clusters/kubernetes_namespace_spec.rb14
-rw-r--r--spec/models/clusters/platforms/kubernetes_spec.rb34
-rw-r--r--spec/models/clusters/providers/aws_spec.rb76
-rw-r--r--spec/models/clusters/providers/gcp_spec.rb109
-rw-r--r--spec/models/commit_collection_spec.rb48
-rw-r--r--spec/models/commit_spec.rb74
-rw-r--r--spec/models/commit_status_spec.rb6
-rw-r--r--spec/models/commit_with_pipeline_spec.rb123
-rw-r--r--spec/models/concerns/access_requestable_spec.rb8
-rw-r--r--spec/models/concerns/atomic_internal_id_spec.rb63
-rw-r--r--spec/models/concerns/checksummable_spec.rb19
-rw-r--r--spec/models/concerns/deployable_spec.rb82
-rw-r--r--spec/models/concerns/deployment_platform_spec.rb49
-rw-r--r--spec/models/concerns/has_status_spec.rb35
-rw-r--r--spec/models/concerns/issuable_spec.rb7
-rw-r--r--spec/models/concerns/noteable_spec.rb1
-rw-r--r--spec/models/concerns/stepable_spec.rb34
-rw-r--r--spec/models/concerns/token_authenticatable_spec.rb1
-rw-r--r--spec/models/container_repository_spec.rb4
-rw-r--r--spec/models/deploy_keys_project_spec.rb1
-rw-r--r--spec/models/deployment_spec.rb13
-rw-r--r--spec/models/description_version_spec.rb29
-rw-r--r--spec/models/environment_spec.rb15
-rw-r--r--spec/models/environment_status_spec.rb2
-rw-r--r--spec/models/event_collection_spec.rb83
-rw-r--r--spec/models/event_spec.rb5
-rw-r--r--spec/models/evidence_spec.rb87
-rw-r--r--spec/models/gpg_signature_spec.rb13
-rw-r--r--spec/models/grafana_integration_spec.rb38
-rw-r--r--spec/models/group_spec.rb37
-rw-r--r--spec/models/hooks/web_hook_spec.rb11
-rw-r--r--spec/models/issue_spec.rb10
-rw-r--r--spec/models/lfs_object_spec.rb51
-rw-r--r--spec/models/list_spec.rb12
-rw-r--r--spec/models/member_spec.rb6
-rw-r--r--spec/models/merge_request_spec.rb102
-rw-r--r--spec/models/namespace_spec.rb123
-rw-r--r--spec/models/note_spec.rb96
-rw-r--r--spec/models/notification_setting_spec.rb1
-rw-r--r--spec/models/pages/lookup_path_spec.rb12
-rw-r--r--spec/models/pages/virtual_domain_spec.rb26
-rw-r--r--spec/models/pages_domain_spec.rb36
-rw-r--r--spec/models/project_services/bugzilla_service_spec.rb2
-rw-r--r--spec/models/project_services/custom_issue_tracker_service_spec.rb2
-rw-r--r--spec/models/project_services/gitlab_issue_tracker_service_spec.rb2
-rw-r--r--spec/models/project_services/jira_service_spec.rb67
-rw-r--r--spec/models/project_services/redmine_service_spec.rb2
-rw-r--r--spec/models/project_services/youtrack_service_spec.rb2
-rw-r--r--spec/models/project_spec.rb362
-rw-r--r--spec/models/project_team_spec.rb8
-rw-r--r--spec/models/project_wiki_spec.rb24
-rw-r--r--spec/models/release_spec.rb41
-rw-r--r--spec/models/repository_spec.rb102
-rw-r--r--spec/models/resource_label_event_spec.rb1
-rw-r--r--spec/models/service_spec.rb62
-rw-r--r--spec/models/snippet_spec.rb30
-rw-r--r--spec/models/suggestion_spec.rb10
-rw-r--r--spec/models/system_note_metadata_spec.rb1
-rw-r--r--spec/models/timelog_spec.rb1
-rw-r--r--spec/models/todo_spec.rb4
-rw-r--r--spec/models/upload_spec.rb48
-rw-r--r--spec/models/uploads/fog_spec.rb2
-rw-r--r--spec/models/user_interacted_project_spec.rb1
-rw-r--r--spec/models/user_spec.rb251
-rw-r--r--spec/policies/deploy_keys_project_policy_spec.rb73
-rw-r--r--spec/policies/global_policy_spec.rb50
-rw-r--r--spec/policies/group_policy_spec.rb87
-rw-r--r--spec/policies/identity_provider_policy_spec.rb1
-rw-r--r--spec/policies/merge_request_policy_spec.rb6
-rw-r--r--spec/policies/project_policy_spec.rb28
-rw-r--r--spec/policies/todo_policy_spec.rb47
-rw-r--r--spec/presenters/ci/build_runner_presenter_spec.rb17
-rw-r--r--spec/presenters/ci/pipeline_presenter_spec.rb38
-rw-r--r--spec/presenters/commit_presenter_spec.rb25
-rw-r--r--spec/presenters/conversational_development_index/metric_presenter_spec.rb1
-rw-r--r--spec/presenters/issue_presenter_spec.rb18
-rw-r--r--spec/presenters/project_presenter_spec.rb22
-rw-r--r--spec/rake_helper.rb2
-rw-r--r--spec/requests/api/access_requests_spec.rb4
-rw-r--r--spec/requests/api/badges_spec.rb4
-rw-r--r--spec/requests/api/commit_statuses_spec.rb60
-rw-r--r--spec/requests/api/commits_spec.rb12
-rw-r--r--spec/requests/api/deploy_keys_spec.rb115
-rw-r--r--spec/requests/api/deployments_spec.rb162
-rw-r--r--spec/requests/api/doorkeeper_access_spec.rb28
-rw-r--r--spec/requests/api/events_spec.rb1
-rw-r--r--spec/requests/api/graphql/namespace/projects_spec.rb1
-rw-r--r--spec/requests/api/graphql/read_only_spec.rb57
-rw-r--r--spec/requests/api/group_labels_spec.rb174
-rw-r--r--spec/requests/api/groups_spec.rb60
-rw-r--r--spec/requests/api/helpers_spec.rb2
-rw-r--r--spec/requests/api/internal/base_spec.rb137
-rw-r--r--spec/requests/api/internal/pages_spec.rb108
-rw-r--r--spec/requests/api/issues/get_project_issues_spec.rb2
-rw-r--r--spec/requests/api/issues/post_projects_issues_spec.rb60
-rw-r--r--spec/requests/api/issues/put_projects_issues_spec.rb61
-rw-r--r--spec/requests/api/labels_spec.rb247
-rw-r--r--spec/requests/api/members_spec.rb33
-rw-r--r--spec/requests/api/pages/internal_access_spec.rb1
-rw-r--r--spec/requests/api/pages/private_access_spec.rb1
-rw-r--r--spec/requests/api/pages/public_access_spec.rb1
-rw-r--r--spec/requests/api/pipelines_spec.rb2
-rw-r--r--spec/requests/api/project_container_repositories_spec.rb36
-rw-r--r--spec/requests/api/project_export_spec.rb2
-rw-r--r--spec/requests/api/project_import_spec.rb47
-rw-r--r--spec/requests/api/project_snippets_spec.rb6
-rw-r--r--spec/requests/api/releases_spec.rb37
-rw-r--r--spec/requests/api/runner_spec.rb79
-rw-r--r--spec/requests/api/settings_spec.rb6
-rw-r--r--spec/requests/api/snippets_spec.rb7
-rw-r--r--spec/requests/api/users_spec.rb211
-rw-r--r--spec/requests/boards/lists_controller_spec.rb25
-rw-r--r--spec/requests/lfs_http_spec.rb879
-rw-r--r--spec/requests/openid_connect_spec.rb25
-rw-r--r--spec/requests/rack_attack_global_spec.rb183
-rw-r--r--spec/routing/project_routing_spec.rb5
-rw-r--r--spec/rubocop/cop/gitlab/const_get_inherit_false_spec.rb80
-rw-r--r--spec/rubocop/cop/line_break_around_conditional_block_spec.rb13
-rw-r--r--spec/rubocop/cop/migration/add_timestamps_spec.rb1
-rw-r--r--spec/rubocop/cop/migration/timestamps_spec.rb1
-rw-r--r--spec/rubocop/cop/scalability/file_uploads_spec.rb1
-rw-r--r--spec/serializers/analytics_build_entity_spec.rb2
-rw-r--r--spec/serializers/analytics_build_serializer_spec.rb2
-rw-r--r--spec/serializers/analytics_issue_entity_spec.rb2
-rw-r--r--spec/serializers/analytics_issue_serializer_spec.rb2
-rw-r--r--spec/serializers/analytics_merge_request_serializer_spec.rb2
-rw-r--r--spec/serializers/analytics_stage_serializer_spec.rb2
-rw-r--r--spec/serializers/analytics_summary_serializer_spec.rb2
-rw-r--r--spec/serializers/blob_entity_spec.rb2
-rw-r--r--spec/serializers/build_action_entity_spec.rb9
-rw-r--r--spec/serializers/build_artifact_entity_spec.rb2
-rw-r--r--spec/serializers/build_details_entity_spec.rb21
-rw-r--r--spec/serializers/build_serializer_spec.rb2
-rw-r--r--spec/serializers/build_trace_entity_spec.rb63
-rw-r--r--spec/serializers/cluster_application_entity_spec.rb2
-rw-r--r--spec/serializers/cluster_basic_entity_spec.rb3
-rw-r--r--spec/serializers/cluster_entity_spec.rb2
-rw-r--r--spec/serializers/cluster_serializer_spec.rb2
-rw-r--r--spec/serializers/commit_entity_spec.rb2
-rw-r--r--spec/serializers/container_repository_entity_spec.rb14
-rw-r--r--spec/serializers/container_tag_entity_spec.rb2
-rw-r--r--spec/serializers/deploy_key_entity_spec.rb36
-rw-r--r--spec/serializers/deployment_entity_spec.rb2
-rw-r--r--spec/serializers/detailed_status_entity_spec.rb2
-rw-r--r--spec/serializers/diff_file_entity_spec.rb2
-rw-r--r--spec/serializers/diff_line_serializer_spec.rb2
-rw-r--r--spec/serializers/diff_viewer_entity_spec.rb2
-rw-r--r--spec/serializers/diffs_entity_spec.rb2
-rw-r--r--spec/serializers/diffs_metadata_entity_spec.rb46
-rw-r--r--spec/serializers/discussion_entity_spec.rb2
-rw-r--r--spec/serializers/entity_date_helper_spec.rb2
-rw-r--r--spec/serializers/entity_request_spec.rb2
-rw-r--r--spec/serializers/environment_entity_spec.rb2
-rw-r--r--spec/serializers/environment_serializer_spec.rb2
-rw-r--r--spec/serializers/environment_status_entity_spec.rb2
-rw-r--r--spec/serializers/evidences/evidence_entity_spec.rb14
-rw-r--r--spec/serializers/evidences/evidence_serializer_spec.rb9
-rw-r--r--spec/serializers/evidences/issue_entity_spec.rb13
-rw-r--r--spec/serializers/evidences/milestone_entity_spec.rb35
-rw-r--r--spec/serializers/evidences/project_entity_spec.rb13
-rw-r--r--spec/serializers/evidences/release_entity_spec.rb36
-rw-r--r--spec/serializers/evidences/release_serializer_spec.rb9
-rw-r--r--spec/serializers/group_child_entity_spec.rb2
-rw-r--r--spec/serializers/group_child_serializer_spec.rb2
-rw-r--r--spec/serializers/group_variable_entity_spec.rb2
-rw-r--r--spec/serializers/issue_entity_spec.rb2
-rw-r--r--spec/serializers/issue_serializer_spec.rb2
-rw-r--r--spec/serializers/job_entity_spec.rb2
-rw-r--r--spec/serializers/label_serializer_spec.rb2
-rw-r--r--spec/serializers/lfs_file_lock_entity_spec.rb2
-rw-r--r--spec/serializers/merge_request_diff_entity_spec.rb2
-rw-r--r--spec/serializers/merge_request_for_pipeline_entity_spec.rb2
-rw-r--r--spec/serializers/merge_request_serializer_spec.rb2
-rw-r--r--spec/serializers/merge_request_user_entity_spec.rb2
-rw-r--r--spec/serializers/merge_request_widget_entity_spec.rb2
-rw-r--r--spec/serializers/note_entity_spec.rb2
-rw-r--r--spec/serializers/paginated_diff_entity_spec.rb33
-rw-r--r--spec/serializers/pipeline_details_entity_spec.rb37
-rw-r--r--spec/serializers/pipeline_entity_spec.rb2
-rw-r--r--spec/serializers/pipeline_serializer_spec.rb8
-rw-r--r--spec/serializers/project_mirror_entity_spec.rb2
-rw-r--r--spec/serializers/project_mirror_serializer_spec.rb2
-rw-r--r--spec/serializers/project_note_entity_spec.rb2
-rw-r--r--spec/serializers/remote_mirror_entity_spec.rb2
-rw-r--r--spec/serializers/request_aware_entity_spec.rb2
-rw-r--r--spec/serializers/runner_entity_spec.rb2
-rw-r--r--spec/serializers/stage_entity_spec.rb2
-rw-r--r--spec/serializers/test_case_entity_spec.rb2
-rw-r--r--spec/serializers/test_report_entity_spec.rb25
-rw-r--r--spec/serializers/test_reports_comparer_entity_spec.rb2
-rw-r--r--spec/serializers/test_reports_comparer_serializer_spec.rb2
-rw-r--r--spec/serializers/test_suite_comparer_entity_spec.rb2
-rw-r--r--spec/serializers/test_suite_entity_spec.rb29
-rw-r--r--spec/serializers/trigger_variable_entity_spec.rb2
-rw-r--r--spec/serializers/user_entity_spec.rb2
-rw-r--r--spec/serializers/variable_entity_spec.rb2
-rw-r--r--spec/services/application_settings/update_service_spec.rb87
-rw-r--r--spec/services/boards/issues/create_service_spec.rb2
-rw-r--r--spec/services/boards/lists/update_service_spec.rb8
-rw-r--r--spec/services/boards/visits/create_service_spec.rb4
-rw-r--r--spec/services/bulk_push_event_payload_service_spec.rb27
-rw-r--r--spec/services/ci/create_pipeline_service/rules_spec.rb94
-rw-r--r--spec/services/ci/create_pipeline_service_spec.rb23
-rw-r--r--spec/services/ci/pipeline_trigger_service_spec.rb162
-rw-r--r--spec/services/ci/process_pipeline_service_spec.rb16
-rw-r--r--spec/services/ci/register_job_service_spec.rb16
-rw-r--r--spec/services/ci/retry_build_service_spec.rb10
-rw-r--r--spec/services/ci/stop_environments_service_spec.rb4
-rw-r--r--spec/services/clusters/gcp/finalize_creation_service_spec.rb24
-rw-r--r--spec/services/create_snippet_service_spec.rb72
-rw-r--r--spec/services/deployments/after_create_service_spec.rb (renamed from spec/services/update_deployment_service_spec.rb)9
-rw-r--r--spec/services/deployments/create_service_spec.rb92
-rw-r--r--spec/services/deployments/update_service_spec.rb15
-rw-r--r--spec/services/event_create_service_spec.rb78
-rw-r--r--spec/services/git/base_hooks_service_spec.rb120
-rw-r--r--spec/services/git/branch_hooks_service_spec.rb10
-rw-r--r--spec/services/git/branch_push_service_spec.rb96
-rw-r--r--spec/services/git/process_ref_changes_service_spec.rb193
-rw-r--r--spec/services/git/tag_hooks_service_spec.rb9
-rw-r--r--spec/services/git/tag_push_service_spec.rb2
-rw-r--r--spec/services/grafana/proxy_service_spec.rb139
-rw-r--r--spec/services/groups/destroy_service_spec.rb6
-rw-r--r--spec/services/groups/transfer_service_spec.rb17
-rw-r--r--spec/services/groups/update_service_spec.rb24
-rw-r--r--spec/services/issues/close_service_spec.rb11
-rw-r--r--spec/services/issues/create_service_spec.rb62
-rw-r--r--spec/services/issues/update_service_spec.rb4
-rw-r--r--spec/services/issues/zoom_link_service_spec.rb37
-rw-r--r--spec/services/members/approve_access_request_service_spec.rb4
-rw-r--r--spec/services/members/request_access_service_spec.rb4
-rw-r--r--spec/services/merge_requests/create_from_issue_service_spec.rb1
-rw-r--r--spec/services/merge_requests/post_merge_service_spec.rb6
-rw-r--r--spec/services/merge_requests/refresh_service_spec.rb124
-rw-r--r--spec/services/merge_requests/update_service_spec.rb4
-rw-r--r--spec/services/note_summary_spec.rb12
-rw-r--r--spec/services/notes/update_service_spec.rb94
-rw-r--r--spec/services/notification_service_spec.rb39
-rw-r--r--spec/services/projects/after_import_service_spec.rb2
-rw-r--r--spec/services/projects/container_repository/cleanup_tags_service_spec.rb2
-rw-r--r--spec/services/projects/container_repository/delete_tags_service_spec.rb135
-rw-r--r--spec/services/projects/destroy_service_spec.rb4
-rw-r--r--spec/services/projects/fork_service_spec.rb21
-rw-r--r--spec/services/projects/hashed_storage/migrate_repository_service_spec.rb9
-rw-r--r--spec/services/projects/hashed_storage/rollback_repository_service_spec.rb9
-rw-r--r--spec/services/projects/housekeeping_service_spec.rb1
-rw-r--r--spec/services/projects/import_export/export_service_spec.rb21
-rw-r--r--spec/services/projects/operations/update_service_spec.rb56
-rw-r--r--spec/services/projects/transfer_service_spec.rb2
-rw-r--r--spec/services/projects/update_pages_service_spec.rb15
-rw-r--r--spec/services/quick_actions/interpret_service_spec.rb28
-rw-r--r--spec/services/spam_service_spec.rb48
-rw-r--r--spec/services/system_note_service_spec.rb729
-rw-r--r--spec/services/system_notes/base_service_spec.rb44
-rw-r--r--spec/services/system_notes/commit_service_spec.rb117
-rw-r--r--spec/services/system_notes/issuables_service_spec.rb628
-rw-r--r--spec/services/system_notes/zoom_service_spec.rb36
-rw-r--r--spec/services/todos/destroy/private_features_service_spec.rb4
-rw-r--r--spec/services/users/destroy_service_spec.rb4
-rw-r--r--spec/simplecov_env.rb2
-rw-r--r--spec/spec_helper.rb43
-rw-r--r--spec/support/api/boards_shared_examples.rb2
-rw-r--r--spec/support/api/milestones_shared_examples.rb2
-rw-r--r--spec/support/controllers/githubish_import_controller_shared_examples.rb32
-rw-r--r--spec/support/features/rss_shared_examples.rb8
-rw-r--r--spec/support/google_api/cloud_platform_helpers.rb2
-rw-r--r--spec/support/helpers/admin_mode_helpers.rb16
-rw-r--r--spec/support/helpers/cycle_analytics_helpers.rb15
-rw-r--r--spec/support/helpers/expect_next_instance_of.rb15
-rw-r--r--spec/support/helpers/gpg_helpers.rb2
-rw-r--r--spec/support/helpers/group_api_helpers.rb11
-rw-r--r--spec/support/helpers/javascript_fixtures_helpers.rb2
-rw-r--r--spec/support/helpers/kubernetes_helpers.rb60
-rw-r--r--spec/support/helpers/lfs_http_helpers.rb62
-rw-r--r--spec/support/helpers/login_helpers.rb8
-rw-r--r--spec/support/helpers/migrations_helpers.rb35
-rw-r--r--spec/support/helpers/next_instance_of.rb28
-rw-r--r--spec/support/helpers/rack_attack_spec_helpers.rb33
-rw-r--r--spec/support/helpers/repo_helpers.rb4
-rw-r--r--spec/support/helpers/select2_helper.rb12
-rw-r--r--spec/support/helpers/stub_experiments.rb15
-rw-r--r--spec/support/helpers/test_env.rb7
-rw-r--r--spec/support/helpers/wait_for_requests.rb6
-rw-r--r--spec/support/helpers/workhorse_helpers.rb55
-rw-r--r--spec/support/matchers/graphql_matchers.rb10
-rw-r--r--spec/support/matchers/log_spam.rb34
-rw-r--r--spec/support/matchers/markdown_matchers.rb11
-rw-r--r--spec/support/matchers/policy_matchers.rb17
-rw-r--r--spec/support/omniauth_strategy.rb2
-rw-r--r--spec/support/redis/redis_shared_examples.rb1
-rw-r--r--spec/support/shared_contexts/policies/group_policy_shared_context.rb17
-rw-r--r--spec/support/shared_contexts/policies/project_policy_shared_context.rb4
-rw-r--r--spec/support/shared_contexts/rack_attack_shared_context.rb13
-rw-r--r--spec/support/shared_contexts/services_shared_context.rb28
-rw-r--r--spec/support/shared_contexts/session_shared_context.rb15
-rw-r--r--spec/support/shared_examples/chat_slash_commands_shared_examples.rb24
-rw-r--r--spec/support/shared_examples/ci/pipeline_email_examples.rb20
-rw-r--r--spec/support/shared_examples/ci_trace_shared_examples.rb6
-rw-r--r--spec/support/shared_examples/common_system_notes_examples.rb27
-rw-r--r--spec/support/shared_examples/controllers/repository_lfs_file_load_examples.rb69
-rw-r--r--spec/support/shared_examples/controllers/uploads_actions_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/cycle_analytics_event_shared_examples.rb19
-rw-r--r--spec/support/shared_examples/cycle_analytics_stage_shared_examples.rb (renamed from spec/support/shared_examples/cycle_analytics_stage_examples.rb)37
-rw-r--r--spec/support/shared_examples/diff_file_collections.rb8
-rw-r--r--spec/support/shared_examples/evidence_updated_exposed_fields.rb29
-rw-r--r--spec/support/shared_examples/lfs_http_shared_examples.rb43
-rw-r--r--spec/support/shared_examples/lib/gitlab/ci/build/rules/rule/clause/clause_shared_examples.rb23
-rw-r--r--spec/support/shared_examples/lib/gitlab/import_export/project_tree_restorer_shared_examples.rb37
-rw-r--r--spec/support/shared_examples/models/cluster_application_status_shared_examples.rb19
-rw-r--r--spec/support/shared_examples/models/cluster_application_version_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/models/clusters/providers/provider_status.rb73
-rw-r--r--spec/support/shared_examples/models/concern/issuable_shared_examples.rb58
-rw-r--r--spec/support/shared_examples/models/with_uploads_shared_examples.rb3
-rw-r--r--spec/support/shared_examples/quick_actions/issue/zoom_quick_actions_shared_examples.rb16
-rw-r--r--spec/support/shared_examples/relative_positioning_shared_examples.rb36
-rw-r--r--spec/support/shared_examples/repo_type_shared_examples.rb31
-rw-r--r--spec/support/shared_examples/requests/rack_attack_shared_examples.rb21
-rw-r--r--spec/support/shared_examples/services/boards/boards_create_service.rb2
-rw-r--r--spec/support/shared_examples/services/boards/boards_list_service.rb4
-rw-r--r--spec/support/shared_examples/slack_mattermost_notifications_shared_examples.rb74
-rw-r--r--spec/support/shared_examples/snippet_visibility_shared_examples.rb26
-rw-r--r--spec/support/shared_examples/trackable_shared_examples.rb37
-rw-r--r--spec/support/shared_examples/updating_mentions_shared_examples.rb80
-rw-r--r--spec/support/shared_examples/versioned_description_shared_examples.rb65
-rw-r--r--spec/tasks/cache/clear/redis_spec.rb2
-rw-r--r--spec/tasks/config_lint_spec.rb2
-rw-r--r--spec/tasks/gitlab/artifacts/check_rake_spec.rb2
-rw-r--r--spec/tasks/gitlab/artifacts/migrate_rake_spec.rb44
-rw-r--r--spec/tasks/gitlab/backup_rake_spec.rb41
-rw-r--r--spec/tasks/gitlab/check_rake_spec.rb8
-rw-r--r--spec/tasks/gitlab/cleanup_rake_spec.rb70
-rw-r--r--spec/tasks/gitlab/db_rake_spec.rb2
-rw-r--r--spec/tasks/gitlab/git_rake_spec.rb2
-rw-r--r--spec/tasks/gitlab/gitaly_rake_spec.rb2
-rw-r--r--spec/tasks/gitlab/info_rake_spec.rb2
-rw-r--r--spec/tasks/gitlab/ldap_rake_spec.rb2
-rw-r--r--spec/tasks/gitlab/lfs/check_rake_spec.rb2
-rw-r--r--spec/tasks/gitlab/lfs/migrate_rake_spec.rb45
-rw-r--r--spec/tasks/gitlab/shell_rake_spec.rb8
-rw-r--r--spec/tasks/gitlab/storage_rake_spec.rb2
-rw-r--r--spec/tasks/gitlab/task_helpers_spec.rb2
-rw-r--r--spec/tasks/gitlab/traces_rake_spec.rb113
-rw-r--r--spec/tasks/gitlab/uploads/check_rake_spec.rb2
-rw-r--r--spec/tasks/gitlab/uploads/migrate_rake_spec.rb27
-rw-r--r--spec/tasks/gitlab/web_hook_rake_spec.rb2
-rw-r--r--spec/tasks/gitlab/workhorse_rake_spec.rb2
-rw-r--r--spec/tasks/tokens_spec.rb2
-rw-r--r--spec/uploaders/attachment_uploader_spec.rb2
-rw-r--r--spec/uploaders/avatar_uploader_spec.rb2
-rw-r--r--spec/uploaders/external_diff_uploader_spec.rb2
-rw-r--r--spec/uploaders/file_mover_spec.rb2
-rw-r--r--spec/uploaders/file_uploader_spec.rb12
-rw-r--r--spec/uploaders/gitlab_uploader_spec.rb12
-rw-r--r--spec/uploaders/import_export_uploader_spec.rb2
-rw-r--r--spec/uploaders/job_artifact_uploader_spec.rb2
-rw-r--r--spec/uploaders/lfs_object_uploader_spec.rb2
-rw-r--r--spec/uploaders/namespace_file_uploader_spec.rb2
-rw-r--r--spec/uploaders/object_storage_spec.rb2
-rw-r--r--spec/uploaders/personal_file_uploader_spec.rb2
-rw-r--r--spec/uploaders/records_uploads_spec.rb2
-rw-r--r--spec/uploaders/uploader_helper_spec.rb2
-rw-r--r--spec/uploaders/workers/object_storage/background_move_worker_spec.rb2
-rw-r--r--spec/uploaders/workers/object_storage/migrate_uploads_worker_spec.rb42
-rw-r--r--spec/validators/branch_filter_validator_spec.rb16
-rw-r--r--spec/validators/js_regex_validator_spec.rb2
-rw-r--r--spec/validators/named_ecdsa_key_validator_spec.rb2
-rw-r--r--spec/validators/namespace_path_validator_spec.rb4
-rw-r--r--spec/validators/project_path_validator_spec.rb4
-rw-r--r--spec/validators/public_url_validator_spec.rb2
-rw-r--r--spec/validators/variable_duplicates_validator_spec.rb2
-rw-r--r--spec/validators/x509_certificate_credentials_validator_spec.rb2
-rw-r--r--spec/views/admin/application_settings/_repository_storage.html.haml_spec.rb28
-rw-r--r--spec/views/admin/dashboard/index.html.haml_spec.rb3
-rw-r--r--spec/views/admin/sessions/new.html.haml_spec.rb31
-rw-r--r--spec/views/ci/status/_badge.html.haml_spec.rb2
-rw-r--r--spec/views/dashboard/projects/_blank_state_admin_welcome.haml_spec.rb2
-rw-r--r--spec/views/dashboard/projects/_nav.html.haml_spec.rb2
-rw-r--r--spec/views/devise/shared/_signin_box.html.haml_spec.rb3
-rw-r--r--spec/views/errors/access_denied.html.haml_spec.rb2
-rw-r--r--spec/views/events/event/_push.html.haml_spec.rb36
-rw-r--r--spec/views/groups/_home_panel.html.haml_spec.rb2
-rw-r--r--spec/views/groups/edit.html.haml_spec.rb2
-rw-r--r--spec/views/help/instance_configuration.html.haml_spec.rb2
-rw-r--r--spec/views/layouts/_head.html.haml_spec.rb3
-rw-r--r--spec/views/layouts/nav/sidebar/_admin.html.haml_spec.rb2
-rw-r--r--spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb2
-rw-r--r--spec/views/notify/pipeline_failed_email.html.haml_spec.rb4
-rw-r--r--spec/views/notify/pipeline_failed_email.text.erb_spec.rb2
-rw-r--r--spec/views/notify/pipeline_success_email.html.haml_spec.rb4
-rw-r--r--spec/views/notify/pipeline_success_email.text.erb_spec.rb26
-rw-r--r--spec/views/profiles/show.html.haml_spec.rb2
-rw-r--r--spec/views/projects/_home_panel.html.haml_spec.rb2
-rw-r--r--spec/views/projects/artifacts/_artifact.html.haml_spec.rb74
-rw-r--r--spec/views/projects/blob/_viewer.html.haml_spec.rb2
-rw-r--r--spec/views/projects/buttons/_dropdown.html.haml_spec.rb2
-rw-r--r--spec/views/projects/ci/lints/show.html.haml_spec.rb2
-rw-r--r--spec/views/projects/commit/_commit_box.html.haml_spec.rb2
-rw-r--r--spec/views/projects/commit/branches.html.haml_spec.rb2
-rw-r--r--spec/views/projects/commit/show.html.haml_spec.rb2
-rw-r--r--spec/views/projects/commits/_commit.html.haml_spec.rb2
-rw-r--r--spec/views/projects/diffs/_stats.html.haml_spec.rb2
-rw-r--r--spec/views/projects/diffs/_viewer.html.haml_spec.rb2
-rw-r--r--spec/views/projects/edit.html.haml_spec.rb2
-rw-r--r--spec/views/projects/environments/terminal.html.haml_spec.rb2
-rw-r--r--spec/views/projects/imports/new.html.haml_spec.rb2
-rw-r--r--spec/views/projects/issues/_related_branches.html.haml_spec.rb2
-rw-r--r--spec/views/projects/jobs/_build.html.haml_spec.rb2
-rw-r--r--spec/views/projects/jobs/_generic_commit_status.html.haml_spec.rb2
-rw-r--r--spec/views/projects/jobs/show.html.haml_spec.rb2
-rw-r--r--spec/views/projects/merge_requests/_commits.html.haml_spec.rb2
-rw-r--r--spec/views/projects/merge_requests/creations/_new_submit.html.haml_spec.rb2
-rw-r--r--spec/views/projects/merge_requests/diffs/_diffs.html.haml_spec.rb2
-rw-r--r--spec/views/projects/merge_requests/edit.html.haml_spec.rb2
-rw-r--r--spec/views/projects/merge_requests/show.html.haml_spec.rb2
-rw-r--r--spec/views/projects/notes/_more_actions_dropdown.html.haml_spec.rb2
-rw-r--r--spec/views/projects/pages_domains/show.html.haml_spec.rb2
-rw-r--r--spec/views/projects/pipeline_schedules/_pipeline_schedule.html.haml_spec.rb2
-rw-r--r--spec/views/projects/pipelines/_stage.html.haml_spec.rb2
-rw-r--r--spec/views/projects/services/_form.haml_spec.rb2
-rw-r--r--spec/views/projects/settings/ci_cd/_autodevops_form.html.haml_spec.rb2
-rw-r--r--spec/views/projects/tags/index.html.haml_spec.rb7
-rw-r--r--spec/views/projects/tree/_tree_header.html.haml_spec.rb44
-rw-r--r--spec/views/projects/tree/_tree_row.html.haml_spec.rb2
-rw-r--r--spec/views/projects/tree/show.html.haml_spec.rb2
-rw-r--r--spec/views/shared/milestones/_issuable.html.haml_spec.rb2
-rw-r--r--spec/views/shared/milestones/_issuables.html.haml_spec.rb2
-rw-r--r--spec/views/shared/milestones/_top.html.haml_spec.rb2
-rw-r--r--spec/views/shared/notes/_form.html.haml_spec.rb2
-rw-r--r--spec/views/shared/projects/_project.html.haml_spec.rb2
-rw-r--r--spec/workers/create_evidence_worker_spec.rb11
-rw-r--r--spec/workers/deployments/success_worker_spec.rb12
-rw-r--r--spec/workers/every_sidekiq_worker_spec.rb28
-rw-r--r--spec/workers/hashed_storage/migrator_worker_spec.rb1
-rw-r--r--spec/workers/hashed_storage/rollbacker_worker_spec.rb1
-rw-r--r--spec/workers/namespaceless_project_destroy_worker_spec.rb1
-rw-r--r--spec/workers/new_release_worker_spec.rb13
-rw-r--r--spec/workers/object_pool/destroy_worker_spec.rb4
-rw-r--r--spec/workers/post_receive_spec.rb155
-rw-r--r--spec/workers/prune_old_events_worker_spec.rb10
-rw-r--r--spec/workers/repository_check/dispatch_worker_spec.rb4
-rw-r--r--spec/workers/update_project_statistics_worker_spec.rb2
1288 files changed, 39381 insertions, 9397 deletions
diff --git a/spec/config/smime_signature_settings_spec.rb b/spec/config/smime_signature_settings_spec.rb
index 38f96e9b330..4f076a92b16 100644
--- a/spec/config/smime_signature_settings_spec.rb
+++ b/spec/config/smime_signature_settings_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe SmimeSignatureSettings do
diff --git a/spec/controllers/admin/application_settings_controller_spec.rb b/spec/controllers/admin/application_settings_controller_spec.rb
index d62e0a97609..bc14e9112a1 100644
--- a/spec/controllers/admin/application_settings_controller_spec.rb
+++ b/spec/controllers/admin/application_settings_controller_spec.rb
@@ -118,7 +118,7 @@ describe Admin::ApplicationSettingsController do
end
describe 'verify panel actions' do
- (Admin::ApplicationSettingsController::VALID_SETTING_PANELS - %w(templates geo)).each do |valid_action|
+ Admin::ApplicationSettingsController::VALID_SETTING_PANELS.each do |valid_action|
it_behaves_like 'renders correct panels' do
let(:action) { valid_action }
end
@@ -143,4 +143,22 @@ describe Admin::ApplicationSettingsController do
expect(response).to redirect_to(admin_runners_path)
end
end
+
+ describe 'GET #lets_encrypt_terms_of_service' do
+ include LetsEncryptHelpers
+
+ before do
+ sign_in(admin)
+
+ stub_lets_encrypt_client
+ end
+
+ subject { get :lets_encrypt_terms_of_service }
+
+ it 'redirects the user to the terms of service page' do
+ subject
+
+ expect(response).to redirect_to("https://letsencrypt.org/documents/LE-SA-v1.2-November-15-2017.pdf")
+ end
+ end
end
diff --git a/spec/controllers/admin/clusters_controller_spec.rb b/spec/controllers/admin/clusters_controller_spec.rb
index afc059d7561..233710b9fc3 100644
--- a/spec/controllers/admin/clusters_controller_spec.rb
+++ b/spec/controllers/admin/clusters_controller_spec.rb
@@ -469,7 +469,7 @@ describe Admin::ClustersController do
end
describe 'security' do
- set(:cluster) { create(:cluster, :provided_by_gcp, :instance) }
+ let_it_be(:cluster) { create(:cluster, :provided_by_gcp, :instance) }
it { expect { put_update }.to be_allowed_for(:admin) }
it { expect { put_update }.to be_denied_for(:user) }
@@ -531,7 +531,7 @@ describe Admin::ClustersController do
end
describe 'security' do
- set(:cluster) { create(:cluster, :provided_by_gcp, :production_environment, :instance) }
+ let_it_be(:cluster) { create(:cluster, :provided_by_gcp, :production_environment, :instance) }
it { expect { delete_destroy }.to be_allowed_for(:admin) }
it { expect { delete_destroy }.to be_denied_for(:user) }
diff --git a/spec/controllers/admin/requests_profiles_controller_spec.rb b/spec/controllers/admin/requests_profiles_controller_spec.rb
index 345f7720c25..853767199bc 100644
--- a/spec/controllers/admin/requests_profiles_controller_spec.rb
+++ b/spec/controllers/admin/requests_profiles_controller_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
describe Admin::RequestsProfilesController do
- set(:admin) { create(:admin) }
+ let_it_be(:admin) { create(:admin) }
before do
sign_in(admin)
diff --git a/spec/controllers/admin/sessions_controller_spec.rb b/spec/controllers/admin/sessions_controller_spec.rb
new file mode 100644
index 00000000000..c1cb57c0b9d
--- /dev/null
+++ b/spec/controllers/admin/sessions_controller_spec.rb
@@ -0,0 +1,98 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Admin::SessionsController, :do_not_mock_admin_mode do
+ include_context 'custom session'
+
+ let(:user) { create(:user) }
+
+ before do
+ sign_in(user)
+ end
+
+ describe '#new' do
+ context 'for regular users' do
+ it 'shows error page' do
+ get :new
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ expect(controller.send(:current_user_mode).admin_mode?).to be(false)
+ end
+ end
+
+ context 'for admin users' do
+ let(:user) { create(:admin) }
+
+ it 'renders a password form' do
+ get :new
+
+ expect(response).to render_template :new
+ expect(controller.send(:current_user_mode).admin_mode?).to be(false)
+ end
+ end
+ end
+
+ describe '#create' do
+ context 'for regular users' do
+ it 'shows error page' do
+ post :create
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ expect(controller.send(:current_user_mode).admin_mode?).to be(false)
+ end
+ end
+
+ context 'for admin users' do
+ let(:user) { create(:admin) }
+
+ it 'sets admin mode with a valid password' do
+ expect(controller.send(:current_user_mode).admin_mode?).to be(false)
+
+ controller.store_location_for(:redirect, admin_root_path)
+ post :create, params: { password: user.password }
+
+ expect(response).to redirect_to admin_root_path
+ expect(controller.send(:current_user_mode).admin_mode?).to be(true)
+ end
+
+ it 'fails with an invalid password' do
+ expect(controller.send(:current_user_mode).admin_mode?).to be(false)
+
+ controller.store_location_for(:redirect, admin_root_path)
+
+ post :create, params: { password: '' }
+
+ expect(response).to render_template :new
+ expect(controller.send(:current_user_mode).admin_mode?).to be(false)
+ end
+ end
+ end
+
+ describe '#destroy' do
+ context 'for regular users' do
+ it 'shows error page' do
+ get :destroy
+
+ expect(response).to have_gitlab_http_status(404)
+ expect(controller.send(:current_user_mode).admin_mode?).to be(false)
+ end
+ end
+
+ context 'for admin users' do
+ let(:user) { create(:admin) }
+
+ it 'disables admin mode and redirects to main page' do
+ expect(controller.send(:current_user_mode).admin_mode?).to be(false)
+ post :create, params: { password: user.password }
+ expect(controller.send(:current_user_mode).admin_mode?).to be(true)
+
+ get :destroy
+
+ expect(response).to have_gitlab_http_status(:found)
+ expect(response).to redirect_to(root_path)
+ expect(controller.send(:current_user_mode).admin_mode?).to be(false)
+ end
+ end
+ end
+end
diff --git a/spec/controllers/admin/users_controller_spec.rb b/spec/controllers/admin/users_controller_spec.rb
index d7428f8b52c..1d1653e67e3 100644
--- a/spec/controllers/admin/users_controller_spec.rb
+++ b/spec/controllers/admin/users_controller_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
describe Admin::UsersController do
let(:user) { create(:user) }
- set(:admin) { create(:admin) }
+ let_it_be(:admin) { create(:admin) }
before do
sign_in(admin)
@@ -60,6 +60,96 @@ describe Admin::UsersController do
end
end
+ describe 'PUT #activate' do
+ shared_examples 'a request that activates the user' do
+ it 'activates the user' do
+ put :activate, params: { id: user.username }
+ user.reload
+ expect(user.active?).to be_truthy
+ expect(flash[:notice]).to eq('Successfully activated')
+ end
+ end
+
+ context 'for a deactivated user' do
+ before do
+ user.deactivate
+ end
+
+ it_behaves_like 'a request that activates the user'
+ end
+
+ context 'for an active user' do
+ it_behaves_like 'a request that activates the user'
+ end
+
+ context 'for a blocked user' do
+ before do
+ user.block
+ end
+
+ it 'does not activate the user' do
+ put :activate, params: { id: user.username }
+ user.reload
+ expect(user.active?).to be_falsey
+ expect(flash[:notice]).to eq('Error occurred. A blocked user must be unblocked to be activated')
+ end
+ end
+ end
+
+ describe 'PUT #deactivate' do
+ shared_examples 'a request that deactivates the user' do
+ it 'deactivates the user' do
+ put :deactivate, params: { id: user.username }
+ user.reload
+ expect(user.deactivated?).to be_truthy
+ expect(flash[:notice]).to eq('Successfully deactivated')
+ end
+ end
+
+ context 'for an active user' do
+ let(:activity) { {} }
+ let(:user) { create(:user, **activity) }
+
+ context 'with no recent activity' do
+ let(:activity) { { last_activity_on: ::User::MINIMUM_INACTIVE_DAYS.next.days.ago } }
+
+ it_behaves_like 'a request that deactivates the user'
+ end
+
+ context 'with recent activity' do
+ let(:activity) { { last_activity_on: ::User::MINIMUM_INACTIVE_DAYS.pred.days.ago } }
+
+ it 'does not deactivate the user' do
+ put :deactivate, params: { id: user.username }
+ user.reload
+ expect(user.deactivated?).to be_falsey
+ expect(flash[:notice]).to eq("The user you are trying to deactivate has been active in the past 14 days and cannot be deactivated")
+ end
+ end
+ end
+
+ context 'for a deactivated user' do
+ before do
+ user.deactivate
+ end
+
+ it_behaves_like 'a request that deactivates the user'
+ end
+
+ context 'for a blocked user' do
+ before do
+ user.block
+ end
+
+ it 'does not deactivate the user' do
+ put :deactivate, params: { id: user.username }
+ user.reload
+ expect(user.deactivated?).to be_falsey
+ expect(flash[:notice]).to eq('Error occurred. A blocked user cannot be deactivated')
+ end
+ end
+ end
+
describe 'PUT block/:id' do
it 'blocks user' do
put :block, params: { id: user.username }
diff --git a/spec/controllers/application_controller_spec.rb b/spec/controllers/application_controller_spec.rb
index d7134d3d25a..ed91b5973b8 100644
--- a/spec/controllers/application_controller_spec.rb
+++ b/spec/controllers/application_controller_spec.rb
@@ -56,6 +56,8 @@ describe ApplicationController do
end
end
+ it_behaves_like 'a Trackable Controller'
+
describe '#add_gon_variables' do
before do
Gon.clear
@@ -94,14 +96,30 @@ describe ApplicationController do
request.path = '/-/peek'
end
- it_behaves_like 'not setting gon variables'
+ # TODO:
+ # remove line below once `privacy_policy_update_callout`
+ # feature flag is removed and `gon` reverts back to
+ # to not setting any variables.
+ if Gitlab.ee?
+ it_behaves_like 'setting gon variables'
+ else
+ it_behaves_like 'not setting gon variables'
+ end
end
end
context 'with json format' do
let(:format) { :json }
- it_behaves_like 'not setting gon variables'
+ # TODO:
+ # remove line below once `privacy_policy_update_callout`
+ # feature flag is removed and `gon` reverts back to
+ # to not setting any variables.
+ if Gitlab.ee?
+ it_behaves_like 'setting gon variables'
+ else
+ it_behaves_like 'not setting gon variables'
+ end
end
end
@@ -442,6 +460,25 @@ describe ApplicationController do
end
end
+ context 'deactivated user' do
+ controller(described_class) do
+ def index
+ render html: 'authenticated'
+ end
+ end
+
+ before do
+ sign_in user
+ user.deactivate
+ end
+
+ it 'signs out a deactivated user' do
+ get :index
+ expect(response).to redirect_to(new_user_session_path)
+ expect(flash[:alert]).to eq('Your account has been deactivated by your administrator. Please log back in to reactivate your account.')
+ end
+ end
+
context 'terms' do
controller(described_class) do
def index
@@ -761,4 +798,92 @@ describe ApplicationController do
end
end
end
+
+ describe '#current_user_mode', :do_not_mock_admin_mode do
+ include_context 'custom session'
+
+ controller(described_class) do
+ def index
+ render html: 'authenticated'
+ end
+ end
+
+ before do
+ allow(ActiveSession).to receive(:list_sessions).with(user).and_return([session])
+
+ sign_in(user)
+ get :index
+ end
+
+ context 'with a regular user' do
+ it 'admin mode is not set' do
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(Gitlab::Auth::CurrentUserMode.new(user).admin_mode?).to be(false)
+ end
+ end
+
+ context 'with an admin user' do
+ let(:user) { create(:admin) }
+
+ it 'admin mode is not set' do
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(Gitlab::Auth::CurrentUserMode.new(user).admin_mode?).to be(false)
+ end
+
+ context 'that re-authenticated' do
+ before do
+ Gitlab::Auth::CurrentUserMode.new(user).enable_admin_mode!(password: user.password)
+ end
+
+ it 'admin mode is set' do
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(Gitlab::Auth::CurrentUserMode.new(user).admin_mode?).to be(true)
+ end
+ end
+ end
+ end
+
+ describe '#require_role' do
+ controller(described_class) do
+ def index; end
+ end
+
+ let(:user) { create(:user) }
+ let(:experiment_enabled) { true }
+
+ before do
+ stub_experiment(signup_flow: experiment_enabled)
+ end
+
+ context 'experiment enabled and user with required role' do
+ before do
+ user.set_role_required!
+ sign_in(user)
+ get :index
+ end
+
+ it { is_expected.to redirect_to users_sign_up_welcome_path }
+ end
+
+ context 'experiment enabled and user without a role' do
+ before do
+ sign_in(user)
+ get :index
+ end
+
+ it { is_expected.not_to redirect_to users_sign_up_welcome_path }
+ end
+
+ context 'experiment disabled and user with required role' do
+ let(:experiment_enabled) { false }
+
+ before do
+ user.set_role_required!
+ sign_in(user)
+ get :index
+ end
+
+ it { is_expected.not_to redirect_to users_sign_up_welcome_path }
+ end
+ end
end
diff --git a/spec/controllers/boards/lists_controller_spec.rb b/spec/controllers/boards/lists_controller_spec.rb
index 77b5872fcb3..bc46d02556b 100644
--- a/spec/controllers/boards/lists_controller_spec.rb
+++ b/spec/controllers/boards/lists_controller_spec.rb
@@ -14,6 +14,10 @@ describe Boards::ListsController do
end
describe 'GET index' do
+ before do
+ create(:list, board: board)
+ end
+
it 'returns a successful 200 response' do
read_board_list user: user, board: board
@@ -22,27 +26,22 @@ describe Boards::ListsController do
end
it 'returns a list of board lists' do
- create(:list, board: board)
-
read_board_list user: user, board: board
expect(response).to match_response_schema('lists')
expect(json_response.length).to eq 3
end
- it 'avoids n+1 queries when serializing lists' do
- list_1 = create(:list, board: board)
- list_1.update_preferences_for(user, { collapsed: true })
-
- control_count = ActiveRecord::QueryRecorder.new { read_board_list user: user, board: board }.count
-
- list_2 = create(:list, board: board)
- list_2.update_preferences_for(user, { collapsed: true })
+ context 'when another user has list preferences' do
+ before do
+ board.lists.first.update_preferences_for(guest, collapsed: true)
+ end
- list_3 = create(:list, board: board)
- list_3.update_preferences_for(user, { collapsed: true })
+ it 'returns the complete list of board lists' do
+ read_board_list user: user, board: board
- expect { read_board_list user: user, board: board }.not_to exceed_query_limit(control_count)
+ expect(json_response.length).to eq 3
+ end
end
context 'with unauthorized user' do
diff --git a/spec/controllers/concerns/enforces_admin_authentication_spec.rb b/spec/controllers/concerns/enforces_admin_authentication_spec.rb
index e6a6702fdea..019a21e8cf0 100644
--- a/spec/controllers/concerns/enforces_admin_authentication_spec.rb
+++ b/spec/controllers/concerns/enforces_admin_authentication_spec.rb
@@ -2,7 +2,9 @@
require 'spec_helper'
-describe EnforcesAdminAuthentication do
+describe EnforcesAdminAuthentication, :do_not_mock_admin_mode do
+ include AdminModeHelper
+
let(:user) { create(:user) }
before do
@@ -10,30 +12,86 @@ describe EnforcesAdminAuthentication do
end
controller(ApplicationController) do
- # `described_class` is not available in this context
- include EnforcesAdminAuthentication # rubocop:disable RSpec/DescribedClass
+ include EnforcesAdminAuthentication
def index
head :ok
end
end
- describe 'authenticate_admin!' do
- context 'as an admin' do
- let(:user) { create(:admin) }
+ context 'feature flag :user_mode_in_session is enabled' do
+ describe 'authenticate_admin!' do
+ context 'as an admin' do
+ let(:user) { create(:admin) }
- it 'renders ok' do
- get :index
+ it 'renders redirect for re-authentication and does not set admin mode' do
+ get :index
+
+ expect(response).to redirect_to new_admin_session_path
+ expect(assigns(:current_user_mode)&.admin_mode?).to be(false)
+ end
+
+ context 'when admin mode is active' do
+ before do
+ enable_admin_mode!(user)
+ end
+
+ it 'renders ok' do
+ get :index
+
+ expect(response).to have_gitlab_http_status(200)
+ end
+ end
+ end
+
+ context 'as a user' do
+ it 'renders a 404' do
+ get :index
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+
+ it 'does not set admin mode' do
+ get :index
- expect(response).to have_gitlab_http_status(200)
+ # check for nil too since on 404, current_user_mode might not be initialized
+ expect(assigns(:current_user_mode)&.admin_mode?).to be_falsey
+ end
end
end
+ end
+
+ context 'feature flag :user_mode_in_session is disabled' do
+ before do
+ stub_feature_flags(user_mode_in_session: false)
+ end
- context 'as a user' do
- it 'renders a 404' do
+ describe 'authenticate_admin!' do
+ before do
get :index
+ end
+
+ context 'as an admin' do
+ let(:user) { create(:admin) }
+
+ it 'allows direct access to page' do
+ expect(response).to have_gitlab_http_status(200)
+ end
+
+ it 'does not set admin mode' do
+ expect(assigns(:current_user_mode)&.admin_mode?).to be_falsey
+ end
+ end
+
+ context 'as a user' do
+ it 'renders a 404' do
+ expect(response).to have_gitlab_http_status(404)
+ end
- expect(response).to have_gitlab_http_status(404)
+ it 'does not set admin mode' do
+ # check for nil too since on 404, current_user_mode might not be initialized
+ expect(assigns(:current_user_mode)&.admin_mode?).to be_falsey
+ end
end
end
end
diff --git a/spec/controllers/concerns/metrics_dashboard_spec.rb b/spec/controllers/concerns/metrics_dashboard_spec.rb
new file mode 100644
index 00000000000..a71e34fd1ca
--- /dev/null
+++ b/spec/controllers/concerns/metrics_dashboard_spec.rb
@@ -0,0 +1,58 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe MetricsDashboard do
+ describe 'GET #metrics_dashboard' do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:environment) { create(:environment, project: project) }
+
+ before do
+ sign_in(user)
+ project.add_maintainer(user)
+ end
+
+ controller(::ApplicationController) do
+ include MetricsDashboard # rubocop:disable RSpec/DescribedClass
+ end
+
+ let(:json_response) do
+ routes.draw { get "metrics_dashboard" => "anonymous#metrics_dashboard" }
+ response = get :metrics_dashboard, format: :json
+
+ JSON.parse(response.parsed_body)
+ end
+
+ context 'when no parameters are provided' do
+ it 'returns an error json_response' do
+ expect(json_response['status']).to eq('error')
+ end
+ end
+
+ context 'when params are provided' do
+ before do
+ allow(controller).to receive(:project).and_return(project)
+ allow(controller)
+ .to receive(:metrics_dashboard_params)
+ .and_return(environment: environment)
+ end
+
+ it 'returns the specified dashboard' do
+ expect(json_response['dashboard']['dashboard']).to eq('Environment metrics')
+ expect(json_response).not_to have_key('all_dashboards')
+ end
+
+ context 'when parameters are provided and the list of all dashboards is required' do
+ before do
+ allow(controller).to receive(:include_all_dashboards?).and_return(true)
+ end
+
+ it 'returns a dashboard in addition to the list of dashboards' do
+ expect(json_response['dashboard']['dashboard']).to eq('Environment metrics')
+ expect(json_response).to have_key('all_dashboards')
+ end
+ end
+ end
+ end
+end
diff --git a/spec/controllers/dashboard/todos_controller_spec.rb b/spec/controllers/dashboard/todos_controller_spec.rb
index c5af04f72ee..4ce445fe41a 100644
--- a/spec/controllers/dashboard/todos_controller_spec.rb
+++ b/spec/controllers/dashboard/todos_controller_spec.rb
@@ -131,7 +131,7 @@ describe Dashboard::TodosController do
expect(todo.reload).to be_pending
expect(response).to have_gitlab_http_status(200)
- expect(json_response).to eq({ "count" => "1", "done_count" => "0" })
+ expect(json_response).to eq({ "count" => 1, "done_count" => 0 })
end
end
@@ -145,7 +145,7 @@ describe Dashboard::TodosController do
expect(todo.reload).to be_pending
end
expect(response).to have_gitlab_http_status(200)
- expect(json_response).to eq({ 'count' => '2', 'done_count' => '0' })
+ expect(json_response).to eq({ 'count' => 2, 'done_count' => 0 })
end
end
end
diff --git a/spec/controllers/groups/clusters_controller_spec.rb b/spec/controllers/groups/clusters_controller_spec.rb
index 5a3ba51d4df..51a6dcca640 100644
--- a/spec/controllers/groups/clusters_controller_spec.rb
+++ b/spec/controllers/groups/clusters_controller_spec.rb
@@ -6,7 +6,7 @@ describe Groups::ClustersController do
include AccessMatchersForController
include GoogleApi::CloudPlatformHelpers
- set(:group) { create(:group) }
+ let_it_be(:group) { create(:group) }
let(:user) { create(:user) }
@@ -536,7 +536,7 @@ describe Groups::ClustersController do
end
describe 'security' do
- set(:cluster) { create(:cluster, :provided_by_gcp, cluster_type: :group_type, groups: [group]) }
+ let_it_be(:cluster) { create(:cluster, :provided_by_gcp, cluster_type: :group_type, groups: [group]) }
it { expect { go }.to be_allowed_for(:admin) }
it { expect { go }.to be_allowed_for(:owner).of(group) }
@@ -604,7 +604,7 @@ describe Groups::ClustersController do
end
describe 'security' do
- set(:cluster) { create(:cluster, :provided_by_gcp, :production_environment, cluster_type: :group_type, groups: [group]) }
+ let_it_be(:cluster) { create(:cluster, :provided_by_gcp, :production_environment, cluster_type: :group_type, groups: [group]) }
it { expect { go }.to be_allowed_for(:admin) }
it { expect { go }.to be_allowed_for(:owner).of(group) }
diff --git a/spec/controllers/groups/group_members_controller_spec.rb b/spec/controllers/groups/group_members_controller_spec.rb
index 0c3dd971582..22f970133e3 100644
--- a/spec/controllers/groups/group_members_controller_spec.rb
+++ b/spec/controllers/groups/group_members_controller_spec.rb
@@ -6,7 +6,7 @@ describe Groups::GroupMembersController do
include ExternalAuthorizationServiceHelpers
let(:user) { create(:user) }
- let(:group) { create(:group, :public, :access_requestable) }
+ let(:group) { create(:group, :public) }
let(:membership) { create(:group_member, group: group) }
describe 'GET index' do
diff --git a/spec/controllers/groups/labels_controller_spec.rb b/spec/controllers/groups/labels_controller_spec.rb
index 98a4c50fc49..d4780fa2675 100644
--- a/spec/controllers/groups/labels_controller_spec.rb
+++ b/spec/controllers/groups/labels_controller_spec.rb
@@ -3,9 +3,9 @@
require 'spec_helper'
describe Groups::LabelsController do
- set(:group) { create(:group) }
- set(:user) { create(:user) }
- set(:project) { create(:project, namespace: group) }
+ let_it_be(:group) { create(:group) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project, namespace: group) }
before do
group.add_owner(user)
@@ -14,8 +14,8 @@ describe Groups::LabelsController do
end
describe 'GET #index' do
- set(:label_1) { create(:label, project: project, title: 'label_1') }
- set(:group_label_1) { create(:group_label, group: group, title: 'group_label_1') }
+ let_it_be(:label_1) { create(:label, project: project, title: 'label_1') }
+ let_it_be(:group_label_1) { create(:group_label, group: group, title: 'group_label_1') }
it 'returns group and project labels by default' do
get :index, params: { group_id: group }, format: :json
@@ -25,8 +25,8 @@ describe Groups::LabelsController do
end
context 'with ancestor group' do
- set(:subgroup) { create(:group, parent: group) }
- set(:subgroup_label_1) { create(:group_label, group: subgroup, title: 'subgroup_label_1') }
+ let_it_be(:subgroup) { create(:group, parent: group) }
+ let_it_be(:subgroup_label_1) { create(:group_label, group: subgroup, title: 'subgroup_label_1') }
before do
subgroup.add_owner(user)
diff --git a/spec/controllers/groups/registry/repositories_controller_spec.rb b/spec/controllers/groups/registry/repositories_controller_spec.rb
new file mode 100644
index 00000000000..4129891914d
--- /dev/null
+++ b/spec/controllers/groups/registry/repositories_controller_spec.rb
@@ -0,0 +1,90 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Groups::Registry::RepositoriesController do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:guest) { create(:user) }
+ let_it_be(:group, reload: true) { create(:group) }
+
+ before do
+ stub_container_registry_config(enabled: true)
+ group.add_owner(user)
+ group.add_guest(guest)
+ sign_in(user)
+ end
+
+ context 'GET #index' do
+ context 'when container registry is enabled' do
+ it 'show index page' do
+ get :index, params: {
+ group_id: group
+ }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+
+ it 'has the correct response schema' do
+ get :index, params: {
+ group_id: group,
+ format: :json
+ }
+
+ expect(response).to match_response_schema('registry/repositories')
+ end
+
+ it 'returns a list of projects for json format' do
+ project = create(:project, group: group)
+ repo = create(:container_repository, project: project)
+
+ get :index, params: {
+ group_id: group,
+ format: :json
+ }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response).to be_kind_of(Array)
+ expect(json_response.first).to include(
+ 'id' => repo.id,
+ 'name' => repo.name
+ )
+ end
+
+ it 'tracks the event' do
+ expect(Gitlab::Tracking).to receive(:event).with(anything, 'list_repositories', {})
+
+ get :index, params: {
+ group_id: group
+ }
+ end
+ end
+
+ context 'container registry is disabled' do
+ before do
+ stub_container_registry_config(enabled: false)
+ end
+
+ it 'renders not found' do
+ get :index, params: {
+ group_id: group
+ }
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ context 'user do not have acces to container registry' do
+ before do
+ sign_out(user)
+ sign_in(guest)
+ end
+
+ it 'renders not found' do
+ get :index, params: {
+ group_id: group
+ }
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+ end
+end
diff --git a/spec/controllers/groups/settings/ci_cd_controller_spec.rb b/spec/controllers/groups/settings/ci_cd_controller_spec.rb
index 70b3a5fb496..897ba491036 100644
--- a/spec/controllers/groups/settings/ci_cd_controller_spec.rb
+++ b/spec/controllers/groups/settings/ci_cd_controller_spec.rb
@@ -156,4 +156,58 @@ describe Groups::Settings::CiCdController do
end
end
end
+
+ describe 'PATCH #update' do
+ subject do
+ patch :update, params: {
+ group_id: group,
+ group: { max_artifacts_size: 10 }
+ }
+ end
+
+ context 'when user is not an admin' do
+ before do
+ group.add_owner(user)
+ end
+
+ it { is_expected.to have_gitlab_http_status(404) }
+ end
+
+ context 'when user is an admin' do
+ let(:user) { create(:admin) }
+
+ before do
+ group.add_owner(user)
+ end
+
+ it { is_expected.to redirect_to(group_settings_ci_cd_path) }
+
+ context 'when service execution went wrong' do
+ let(:update_service) { double }
+
+ before do
+ allow(Groups::UpdateService).to receive(:new).and_return(update_service)
+ allow(update_service).to receive(:execute).and_return(false)
+ allow_any_instance_of(Group).to receive_message_chain(:errors, :full_messages)
+ .and_return(['Error 1'])
+
+ subject
+ end
+
+ it 'returns a flash alert' do
+ expect(response).to set_flash[:alert]
+ .to eq("There was a problem updating the pipeline settings: [\"Error 1\"].")
+ end
+ end
+
+ context 'when service execution was successful' do
+ it 'returns a flash notice' do
+ subject
+
+ expect(response).to set_flash[:notice]
+ .to eq('Pipeline settings was updated for the group')
+ end
+ end
+ end
+ end
end
diff --git a/spec/controllers/groups/shared_projects_controller_spec.rb b/spec/controllers/groups/shared_projects_controller_spec.rb
index 9f6c558c931..a4b2efa7c43 100644
--- a/spec/controllers/groups/shared_projects_controller_spec.rb
+++ b/spec/controllers/groups/shared_projects_controller_spec.rb
@@ -17,9 +17,9 @@ describe Groups::SharedProjectsController do
).execute(group)
end
- set(:group) { create(:group) }
- set(:user) { create(:user) }
- set(:shared_project) do
+ let_it_be(:group) { create(:group) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:shared_project) do
shared_project = create(:project, namespace: user.namespace)
share_project(shared_project)
diff --git a/spec/controllers/groups_controller_spec.rb b/spec/controllers/groups_controller_spec.rb
index 404e61c5271..3c39a6468e5 100644
--- a/spec/controllers/groups_controller_spec.rb
+++ b/spec/controllers/groups_controller_spec.rb
@@ -125,7 +125,7 @@ describe GroupsController do
end
context 'as json' do
- it 'includes all projects from groups and subgroups in event feed' do
+ it 'includes events from all projects in group and subgroups' do
2.times do
project = create(:project, group: group)
create(:event, project: project)
@@ -385,6 +385,29 @@ describe GroupsController do
expect(response).to have_gitlab_http_status(302)
expect(group.reload.project_creation_level).to eq(::Gitlab::Access::MAINTAINER_PROJECT_ACCESS)
end
+
+ context 'when a project inside the group has container repositories' do
+ before do
+ stub_container_registry_config(enabled: true)
+ stub_container_registry_tags(repository: /image/, tags: %w[rc1])
+ create(:container_repository, project: project, name: :image)
+ end
+
+ it 'does allow the group to be renamed' do
+ post :update, params: { id: group.to_param, group: { name: 'new_name' } }
+
+ expect(controller).to set_flash[:notice]
+ expect(response).to have_gitlab_http_status(302)
+ expect(group.reload.name).to eq('new_name')
+ end
+
+ it 'does not allow to path of the group to be changed' do
+ post :update, params: { id: group.to_param, group: { path: 'new_path' } }
+
+ expect(assigns(:group).errors[:base].first).to match(/Docker images in their Container Registry/)
+ expect(response).to have_gitlab_http_status(200)
+ end
+ end
end
describe '#ensure_canonical_path' do
@@ -673,6 +696,28 @@ describe GroupsController do
expect(response).to have_gitlab_http_status(404)
end
end
+
+ context 'transferring when a project has container images' do
+ let(:group) { create(:group, :public, :nested) }
+ let!(:group_member) { create(:group_member, :owner, group: group, user: user) }
+
+ before do
+ stub_container_registry_config(enabled: true)
+ stub_container_registry_tags(repository: /image/, tags: %w[rc1])
+ create(:container_repository, project: project, name: :image)
+
+ put :transfer,
+ params: {
+ id: group.to_param,
+ new_parent_group_id: ''
+ }
+ end
+
+ it 'does not allow the group to be transferred' do
+ expect(controller).to set_flash[:alert].to match(/Docker images in their Container Registry/)
+ expect(response).to redirect_to(edit_group_path(group))
+ end
+ end
end
context 'token authentication' do
diff --git a/spec/controllers/health_controller_spec.rb b/spec/controllers/health_controller_spec.rb
index e82dcfcdb64..8a2291bccd7 100644
--- a/spec/controllers/health_controller_spec.rb
+++ b/spec/controllers/health_controller_spec.rb
@@ -24,11 +24,26 @@ describe HealthController do
it 'responds with readiness checks data' do
subject
- expect(json_response['db_check']['status']).to eq('ok')
- expect(json_response['cache_check']['status']).to eq('ok')
- expect(json_response['queues_check']['status']).to eq('ok')
- expect(json_response['shared_state_check']['status']).to eq('ok')
- expect(json_response['gitaly_check']['status']).to eq('ok')
+ expect(json_response['db_check']).to contain_exactly({ 'status' => 'ok' })
+ expect(json_response['cache_check']).to contain_exactly({ 'status' => 'ok' })
+ expect(json_response['queues_check']).to contain_exactly({ 'status' => 'ok' })
+ expect(json_response['shared_state_check']).to contain_exactly({ 'status' => 'ok' })
+ expect(json_response['gitaly_check']).to contain_exactly(
+ { 'status' => 'ok', 'labels' => { 'shard' => 'default' } })
+ end
+
+ it 'responds with readiness checks data when a failure happens' do
+ allow(Gitlab::HealthChecks::Redis::RedisCheck).to receive(:readiness).and_return(
+ Gitlab::HealthChecks::Result.new('redis_check', false, "check error"))
+
+ subject
+
+ expect(json_response['cache_check']).to contain_exactly({ 'status' => 'ok' })
+ expect(json_response['redis_check']).to contain_exactly(
+ { 'status' => 'failed', 'message' => 'check error' })
+
+ expect(response.status).to eq(503)
+ expect(response.headers['X-GitLab-Custom-Error']).to eq(1)
end
end
@@ -76,10 +91,7 @@ describe HealthController do
it 'responds with liveness checks data' do
subject
- expect(json_response['db_check']['status']).to eq('ok')
- expect(json_response['cache_check']['status']).to eq('ok')
- expect(json_response['queues_check']['status']).to eq('ok')
- expect(json_response['shared_state_check']['status']).to eq('ok')
+ expect(json_response).to eq('status' => 'ok')
end
end
diff --git a/spec/controllers/import/bitbucket_controller_spec.rb b/spec/controllers/import/bitbucket_controller_spec.rb
index 38388c21749..d013bd6d427 100644
--- a/spec/controllers/import/bitbucket_controller_spec.rb
+++ b/spec/controllers/import/bitbucket_controller_spec.rb
@@ -80,6 +80,21 @@ describe Import::BitbucketController do
expect(assigns(:already_added_projects)).to eq([@project])
expect(assigns(:repos)).to eq([])
end
+
+ context 'when filtering' do
+ let(:filter) { '<html>test</html>' }
+ let(:expected_filter) { 'test' }
+
+ subject { get :status, params: { filter: filter }, as: :json }
+
+ it 'passes sanitized filter param to bitbucket client' do
+ expect_next_instance_of(Bitbucket::Client) do |client|
+ expect(client).to receive(:repos).with(filter: expected_filter).and_return([@repo])
+ end
+
+ subject
+ end
+ end
end
describe "POST create" do
diff --git a/spec/controllers/import/bitbucket_server_controller_spec.rb b/spec/controllers/import/bitbucket_server_controller_spec.rb
index e1aeab46fca..f30eace7d30 100644
--- a/spec/controllers/import/bitbucket_server_controller_spec.rb
+++ b/spec/controllers/import/bitbucket_server_controller_spec.rb
@@ -39,7 +39,7 @@ describe Import::BitbucketServerController do
assign_session_tokens
end
- set(:project) { create(:project) }
+ let_it_be(:project) { create(:project) }
it 'returns the new project' do
allow(Gitlab::BitbucketServerImport::ProjectCreator)
diff --git a/spec/controllers/import/gitlab_projects_controller_spec.rb b/spec/controllers/import/gitlab_projects_controller_spec.rb
index 51b398895bc..a3f6d8dcea2 100644
--- a/spec/controllers/import/gitlab_projects_controller_spec.rb
+++ b/spec/controllers/import/gitlab_projects_controller_spec.rb
@@ -3,8 +3,8 @@
require 'spec_helper'
describe Import::GitlabProjectsController do
- set(:namespace) { create(:namespace) }
- set(:user) { namespace.owner }
+ let_it_be(:namespace) { create(:namespace) }
+ let_it_be(:user) { namespace.owner }
let(:file) { fixture_file_upload('spec/fixtures/project_export.tar.gz', 'text/plain') }
before do
diff --git a/spec/controllers/omniauth_callbacks_controller_spec.rb b/spec/controllers/omniauth_callbacks_controller_spec.rb
index 9371c434f49..521dbe7ee23 100644
--- a/spec/controllers/omniauth_callbacks_controller_spec.rb
+++ b/spec/controllers/omniauth_callbacks_controller_spec.rb
@@ -18,6 +18,28 @@ describe OmniauthCallbacksController, type: :controller do
Rails.application.env_config['omniauth.auth'] = @original_env_config_omniauth_auth
end
+ context 'a deactivated user' do
+ let(:provider) { :github }
+ let(:extern_uid) { 'my-uid' }
+
+ before do
+ user.deactivate!
+ post provider
+ end
+
+ it 'allows sign in' do
+ expect(request.env['warden']).to be_authenticated
+ end
+
+ it 'activates the user' do
+ expect(user.reload.active?).to be_truthy
+ end
+
+ it 'shows reactivation flash message after logging in' do
+ expect(flash[:notice]).to eq('Welcome back! Your account had been deactivated due to inactivity but is now reactivated.')
+ end
+ end
+
context 'when the user is on the last sign in attempt' do
let(:extern_uid) { 'my-uid' }
diff --git a/spec/controllers/profiles/notifications_controller_spec.rb b/spec/controllers/profiles/notifications_controller_spec.rb
index f69847119d4..dbc408bcdd9 100644
--- a/spec/controllers/profiles/notifications_controller_spec.rb
+++ b/spec/controllers/profiles/notifications_controller_spec.rb
@@ -20,6 +20,38 @@ describe Profiles::NotificationsController do
expect(response).to render_template :show
end
+
+ context 'with groups that do not have notification preferences' do
+ set(:group) { create(:group) }
+ set(:subgroup) { create(:group, parent: group) }
+
+ before do
+ group.add_developer(user)
+ end
+
+ it 'still shows up in the list' do
+ sign_in(user)
+
+ get :show
+
+ expect(assigns(:group_notifications).map(&:source_id)).to include(subgroup.id)
+ end
+
+ it 'has an N+1 (but should not)' do
+ sign_in(user)
+
+ control = ActiveRecord::QueryRecorder.new do
+ get :show
+ end
+
+ create_list(:group, 2, parent: group)
+
+ # We currently have an N + 1, switch to `not_to` once fixed
+ expect do
+ get :show
+ end.to exceed_query_limit(control)
+ end
+ end
end
describe 'POST update' do
diff --git a/spec/controllers/profiles_controller_spec.rb b/spec/controllers/profiles_controller_spec.rb
index 08681c0341a..eb479d9a0aa 100644
--- a/spec/controllers/profiles_controller_spec.rb
+++ b/spec/controllers/profiles_controller_spec.rb
@@ -153,7 +153,7 @@ describe ProfilesController, :request_store do
user.reload
expect(response.status).to eq(302)
- expect(gitlab_shell.exists?(project.repository_storage, "#{new_username}/#{project.path}.git")).to be_truthy
+ expect(gitlab_shell.repository_exists?(project.repository_storage, "#{new_username}/#{project.path}.git")).to be_truthy
end
end
@@ -171,7 +171,7 @@ describe ProfilesController, :request_store do
user.reload
expect(response.status).to eq(302)
- expect(gitlab_shell.exists?(project.repository_storage, "#{project.disk_path}.git")).to be_truthy
+ expect(gitlab_shell.repository_exists?(project.repository_storage, "#{project.disk_path}.git")).to be_truthy
expect(before_disk_path).to eq(project.disk_path)
end
end
diff --git a/spec/controllers/projects/artifacts_controller_spec.rb b/spec/controllers/projects/artifacts_controller_spec.rb
index 6ea82785e98..acc6935cb71 100644
--- a/spec/controllers/projects/artifacts_controller_spec.rb
+++ b/spec/controllers/projects/artifacts_controller_spec.rb
@@ -4,9 +4,9 @@ require 'spec_helper'
describe Projects::ArtifactsController do
let(:user) { project.owner }
- set(:project) { create(:project, :repository, :public) }
+ let_it_be(:project) { create(:project, :repository, :public) }
- let(:pipeline) do
+ let_it_be(:pipeline, reload: true) do
create(:ci_pipeline,
project: project,
sha: project.commit.sha,
@@ -14,12 +14,119 @@ describe Projects::ArtifactsController do
status: 'success')
end
- let(:job) { create(:ci_build, :success, :artifacts, pipeline: pipeline) }
+ let!(:job) { create(:ci_build, :success, :artifacts, pipeline: pipeline) }
before do
sign_in(user)
end
+ describe 'GET index' do
+ subject { get :index, params: { namespace_id: project.namespace, project_id: project } }
+
+ context 'when feature flag is on' do
+ before do
+ stub_feature_flags(artifacts_management_page: true)
+ end
+
+ it 'sets the artifacts variable' do
+ subject
+
+ expect(assigns(:artifacts)).to contain_exactly(*project.job_artifacts)
+ end
+
+ it 'sets the total size variable' do
+ subject
+
+ expect(assigns(:total_size)).to eq(project.job_artifacts.total_size)
+ end
+
+ describe 'pagination' do
+ before do
+ stub_const("#{described_class}::MAX_PER_PAGE", 1)
+ end
+
+ it 'paginates artifacts' do
+ subject
+
+ expect(assigns(:artifacts)).to contain_exactly(project.reload.job_artifacts.last)
+ end
+ end
+ end
+
+ context 'when feature flag is off' do
+ before do
+ stub_feature_flags(artifacts_management_page: false)
+ end
+
+ it 'renders no content' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:no_content)
+ end
+
+ it 'does not set the artifacts variable' do
+ subject
+
+ expect(assigns(:artifacts)).to eq(nil)
+ end
+
+ it 'does not set the total size variable' do
+ subject
+
+ expect(assigns(:total_size)).to eq(nil)
+ end
+ end
+ end
+
+ describe 'DELETE destroy' do
+ let!(:artifact) { job.job_artifacts.erasable.first }
+
+ subject { delete :destroy, params: { namespace_id: project.namespace, project_id: project, id: artifact } }
+
+ it 'deletes the artifact' do
+ expect { subject }.to change { Ci::JobArtifact.count }.by(-1)
+ expect(artifact).not_to exist
+ end
+
+ it 'redirects to artifacts index page' do
+ subject
+
+ expect(response).to redirect_to(project_artifacts_path(project))
+ end
+
+ it 'sets the notice' do
+ subject
+
+ expect(flash[:notice]).to eq('Artifact was successfully deleted.')
+ end
+
+ context 'when artifact deletion fails' do
+ before do
+ allow_any_instance_of(Ci::JobArtifact).to receive(:destroy).and_return(false)
+ end
+
+ it 'redirects to artifacts index page' do
+ subject
+
+ expect(response).to redirect_to(project_artifacts_path(project))
+ end
+
+ it 'sets the notice' do
+ subject
+
+ expect(flash[:notice]).to eq('Artifact could not be deleted.')
+ end
+ end
+
+ context 'when user is not authorized' do
+ let(:user) { create(:user) }
+
+ it 'does not delete the artifact' do
+ expect { subject }.not_to change { Ci::JobArtifact.count }
+ end
+ end
+ end
+
describe 'GET download' do
def download_artifact(extra_params = {})
params = { namespace_id: project.namespace, project_id: project, job_id: job }.merge(extra_params)
@@ -179,6 +286,25 @@ describe Projects::ArtifactsController do
expect(response).to render_template('projects/artifacts/file')
end
end
+
+ context 'when the project is private and pages access control is enabled' do
+ let(:private_project) { create(:project, :repository, :private) }
+ let(:pipeline) { create(:ci_pipeline, project: private_project) }
+ let(:job) { create(:ci_build, :success, :artifacts, pipeline: pipeline) }
+
+ before do
+ private_project.add_developer(user)
+
+ allow(Gitlab.config.pages).to receive(:access_control).and_return(true)
+ allow(Gitlab.config.pages).to receive(:artifacts_server).and_return(true)
+ end
+
+ it 'renders the file view' do
+ get :file, params: { namespace_id: private_project.namespace, project_id: private_project, job_id: job, path: 'ci_artifacts.txt' }
+
+ expect(response).to have_gitlab_http_status(302)
+ end
+ end
end
describe 'GET raw' do
diff --git a/spec/controllers/projects/autocomplete_sources_controller_spec.rb b/spec/controllers/projects/autocomplete_sources_controller_spec.rb
index a9a058e7e17..34765ae3951 100644
--- a/spec/controllers/projects/autocomplete_sources_controller_spec.rb
+++ b/spec/controllers/projects/autocomplete_sources_controller_spec.rb
@@ -3,10 +3,10 @@
require 'spec_helper'
describe Projects::AutocompleteSourcesController do
- set(:group) { create(:group) }
- set(:project) { create(:project, namespace: group) }
- set(:issue) { create(:issue, project: project) }
- set(:user) { create(:user) }
+ let_it_be(:group) { create(:group) }
+ let_it_be(:project) { create(:project, namespace: group) }
+ let_it_be(:issue) { create(:issue, project: project) }
+ let_it_be(:user) { create(:user) }
describe 'GET members' do
before do
diff --git a/spec/controllers/projects/branches_controller_spec.rb b/spec/controllers/projects/branches_controller_spec.rb
index f5bcea4a097..affe0e0f970 100644
--- a/spec/controllers/projects/branches_controller_spec.rb
+++ b/spec/controllers/projects/branches_controller_spec.rb
@@ -578,7 +578,9 @@ describe Projects::BranchesController do
describe 'GET diverging_commit_counts' do
before do
sign_in(user)
+ end
+ it 'returns the commit counts behind and ahead of default branch' do
get :diverging_commit_counts,
format: :json,
params: {
@@ -586,14 +588,58 @@ describe Projects::BranchesController do
project_id: project,
names: ['fix', 'add-pdf-file', 'branch-merged']
}
- end
- it 'returns the commit counts behind and ahead of default branch' do
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to eq(
"fix" => { "behind" => 29, "ahead" => 2 },
"branch-merged" => { "behind" => 1, "ahead" => 0 },
"add-pdf-file" => { "behind" => 0, "ahead" => 3 }
)
end
+
+ it 'returns the commits counts with no names provided' do
+ allow_any_instance_of(Repository).to receive(:branch_count).and_return(Kaminari.config.default_per_page)
+
+ get :diverging_commit_counts,
+ format: :json,
+ params: {
+ namespace_id: project.namespace,
+ project_id: project
+ }
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response.count).to be > 1
+ end
+
+ describe 'with many branches' do
+ before do
+ allow_any_instance_of(Repository).to receive(:branch_count).and_return(Kaminari.config.default_per_page + 1)
+ end
+
+ it 'returns 422 if no names are specified' do
+ get :diverging_commit_counts,
+ format: :json,
+ params: {
+ namespace_id: project.namespace,
+ project_id: project
+ }
+
+ expect(response).to have_gitlab_http_status(422)
+ expect(json_response['error']).to eq("Specify at least one and at most #{Kaminari.config.default_per_page} branch names")
+ end
+
+ it 'returns the list of counts' do
+ get :diverging_commit_counts,
+ format: :json,
+ params: {
+ namespace_id: project.namespace,
+ project_id: project,
+ names: ['fix', 'add-pdf-file', 'branch-merged']
+ }
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response.count).to be > 1
+ end
+ end
end
end
diff --git a/spec/controllers/projects/clusters_controller_spec.rb b/spec/controllers/projects/clusters_controller_spec.rb
index 8ac72df5d20..e1f6d571d27 100644
--- a/spec/controllers/projects/clusters_controller_spec.rb
+++ b/spec/controllers/projects/clusters_controller_spec.rb
@@ -7,7 +7,7 @@ describe Projects::ClustersController do
include GoogleApi::CloudPlatformHelpers
include KubernetesHelpers
- set(:project) { create(:project) }
+ let_it_be(:project) { create(:project) }
let(:user) { create(:user) }
@@ -536,7 +536,7 @@ describe Projects::ClustersController do
end
describe 'security' do
- set(:cluster) { create(:cluster, :provided_by_gcp, projects: [project]) }
+ let_it_be(:cluster) { create(:cluster, :provided_by_gcp, projects: [project]) }
it { expect { go }.to be_allowed_for(:admin) }
it { expect { go }.to be_allowed_for(:owner).of(project) }
@@ -605,7 +605,7 @@ describe Projects::ClustersController do
end
describe 'security' do
- set(:cluster) { create(:cluster, :provided_by_gcp, :production_environment, projects: [project]) }
+ let_it_be(:cluster) { create(:cluster, :provided_by_gcp, :production_environment, projects: [project]) }
it { expect { go }.to be_allowed_for(:admin) }
it { expect { go }.to be_allowed_for(:owner).of(project) }
diff --git a/spec/controllers/projects/commit_controller_spec.rb b/spec/controllers/projects/commit_controller_spec.rb
index afd5cb15e0f..95112cfeabe 100644
--- a/spec/controllers/projects/commit_controller_spec.rb
+++ b/spec/controllers/projects/commit_controller_spec.rb
@@ -3,9 +3,10 @@
require 'spec_helper'
describe Projects::CommitController do
- set(:project) { create(:project, :repository) }
- set(:user) { create(:user) }
- let(:commit) { project.commit("master") }
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:user) { create(:user) }
+
+ let(:commit) { project.commit("master") }
let(:master_pickable_sha) { '7d3b0f7cff5f37573aea97cebfd5692ea1689924' }
let(:master_pickable_commit) { project.commit(master_pickable_sha) }
diff --git a/spec/controllers/projects/deploy_keys_controller_spec.rb b/spec/controllers/projects/deploy_keys_controller_spec.rb
index ccad76eaddd..8b1ca2efab2 100644
--- a/spec/controllers/projects/deploy_keys_controller_spec.rb
+++ b/spec/controllers/projects/deploy_keys_controller_spec.rb
@@ -5,6 +5,7 @@ require 'spec_helper'
describe Projects::DeployKeysController do
let(:project) { create(:project, :repository) }
let(:user) { create(:user) }
+ let(:admin) { create(:admin) }
before do
project.add_maintainer(user)
@@ -37,7 +38,7 @@ describe Projects::DeployKeysController do
create(:deploy_keys_project, project: project2, deploy_key: deploy_key_internal)
end
- let!(:deploy_keys_actual_project) do
+ let!(:deploy_keys_project_actual) do
create(:deploy_keys_project, project: project, deploy_key: deploy_key_actual)
end
@@ -154,7 +155,7 @@ describe Projects::DeployKeysController do
context 'with admin' do
before do
- sign_in(create(:admin))
+ sign_in(admin)
end
it 'returns 302' do
@@ -219,7 +220,7 @@ describe Projects::DeployKeysController do
context 'with admin' do
before do
- sign_in(create(:admin))
+ sign_in(admin)
end
it 'returns 302' do
@@ -234,4 +235,80 @@ describe Projects::DeployKeysController do
end
end
end
+
+ describe 'PUT update' do
+ let(:extra_params) { {} }
+
+ subject do
+ put :update, params: extra_params.reverse_merge(id: deploy_key.id,
+ namespace_id: project.namespace,
+ project_id: project)
+ end
+
+ def deploy_key_params(title, can_push)
+ deploy_keys_projects_attributes = { '0' => { id: deploy_keys_project, can_push: can_push } }
+ { deploy_key: { title: title, deploy_keys_projects_attributes: deploy_keys_projects_attributes } }
+ end
+
+ let(:deploy_key) { create(:deploy_key, public: true) }
+ let(:project) { create(:project) }
+ let!(:deploy_keys_project) do
+ create(:deploy_keys_project, project: project, deploy_key: deploy_key)
+ end
+
+ context 'with project maintainer' do
+ before do
+ project.add_maintainer(user)
+ end
+
+ context 'public deploy key attached to project' do
+ let(:extra_params) { deploy_key_params('updated title', '1') }
+
+ it 'does not update the title of the deploy key' do
+ expect { subject }.not_to change { deploy_key.reload.title }
+ end
+
+ it 'updates can_push of deploy_keys_project' do
+ expect { subject }.to change { deploy_keys_project.reload.can_push }.from(false).to(true)
+ end
+ end
+ end
+
+ context 'with admin' do
+ before do
+ sign_in(admin)
+ end
+
+ context 'public deploy key attached to project' do
+ let(:extra_params) { deploy_key_params('updated title', '1') }
+
+ it 'updates the title of the deploy key' do
+ expect { subject }.to change { deploy_key.reload.title }.to('updated title')
+ end
+
+ it 'updates can_push of deploy_keys_project' do
+ expect { subject }.to change { deploy_keys_project.reload.can_push }.from(false).to(true)
+ end
+ end
+ end
+
+ context 'with admin as project maintainer' do
+ before do
+ sign_in(admin)
+ project.add_maintainer(admin)
+ end
+
+ context 'public deploy key attached to project' do
+ let(:extra_params) { deploy_key_params('updated title', '1') }
+
+ it 'updates the title of the deploy key' do
+ expect { subject }.to change { deploy_key.reload.title }.to('updated title')
+ end
+
+ it 'updates can_push of deploy_keys_project' do
+ expect { subject }.to change { deploy_keys_project.reload.can_push }.from(false).to(true)
+ end
+ end
+ end
+ end
end
diff --git a/spec/controllers/projects/deployments_controller_spec.rb b/spec/controllers/projects/deployments_controller_spec.rb
index b9ee69a617b..66112c95742 100644
--- a/spec/controllers/projects/deployments_controller_spec.rb
+++ b/spec/controllers/projects/deployments_controller_spec.rb
@@ -75,15 +75,13 @@ describe Projects::DeploymentsController do
}
end
- before do
+ it 'returns a metrics JSON document' do
expect_next_instance_of(DeploymentMetrics) do |deployment_metrics|
allow(deployment_metrics).to receive(:has_metrics?).and_return(true)
expect(deployment_metrics).to receive(:metrics).and_return(empty_metrics)
end
- end
- it 'returns a metrics JSON document' do
get :metrics, params: deployment_params(id: deployment.to_param)
expect(response).to be_ok
@@ -91,6 +89,19 @@ describe Projects::DeploymentsController do
expect(json_response['metrics']).to eq({})
expect(json_response['last_update']).to eq(42)
end
+
+ it 'returns a 404 if the deployment failed' do
+ failed_deployment = create(
+ :deployment,
+ :failed,
+ project: project,
+ environment: environment
+ )
+
+ get :metrics, params: deployment_params(id: failed_deployment.to_param)
+
+ expect(response).to have_gitlab_http_status(404)
+ end
end
end
end
diff --git a/spec/controllers/projects/discussions_controller_spec.rb b/spec/controllers/projects/discussions_controller_spec.rb
index e30b28a4bd5..6ed822bbb10 100644
--- a/spec/controllers/projects/discussions_controller_spec.rb
+++ b/spec/controllers/projects/discussions_controller_spec.rb
@@ -189,7 +189,7 @@ describe Projects::DiscussionsController do
context "when vue_mr_discussions cookie is present" do
before do
- allow(controller).to receive(:cookies).and_return({ vue_mr_discussions: 'true' })
+ cookies[:vue_mr_discussions] = 'true'
end
it "renders discussion with serializer" do
diff --git a/spec/controllers/projects/environments/prometheus_api_controller_spec.rb b/spec/controllers/projects/environments/prometheus_api_controller_spec.rb
index 45328482ad7..b12964f8d8b 100644
--- a/spec/controllers/projects/environments/prometheus_api_controller_spec.rb
+++ b/spec/controllers/projects/environments/prometheus_api_controller_spec.rb
@@ -3,9 +3,9 @@
require 'spec_helper'
describe Projects::Environments::PrometheusApiController do
- set(:project) { create(:project) }
- set(:environment) { create(:environment, project: project) }
- set(:user) { create(:user) }
+ let_it_be(:project) { create(:project) }
+ let_it_be(:environment) { create(:environment, project: project) }
+ let_it_be(:user) { create(:user) }
before do
project.add_reporter(user)
diff --git a/spec/controllers/projects/environments_controller_spec.rb b/spec/controllers/projects/environments_controller_spec.rb
index 820ce159633..3fe5ff5feee 100644
--- a/spec/controllers/projects/environments_controller_spec.rb
+++ b/spec/controllers/projects/environments_controller_spec.rb
@@ -5,10 +5,10 @@ require 'spec_helper'
describe Projects::EnvironmentsController do
include MetricsDashboardHelpers
- set(:user) { create(:user) }
- set(:project) { create(:project) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project) }
- set(:environment) do
+ let_it_be(:environment) do
create(:environment, name: 'production', project: project)
end
@@ -256,7 +256,7 @@ describe Projects::EnvironmentsController do
it 'loads the terminals for the environment' do
# In EE we have to stub EE::Environment since it overwrites the
# "terminals" method.
- expect_any_instance_of(defined?(EE) ? EE::Environment : Environment)
+ expect_any_instance_of(Gitlab.ee? ? EE::Environment : Environment)
.to receive(:terminals)
get :terminal, params: environment_params
@@ -282,7 +282,7 @@ describe Projects::EnvironmentsController do
it 'returns the first terminal for the environment' do
# In EE we have to stub EE::Environment since it overwrites the
# "terminals" method.
- expect_any_instance_of(defined?(EE) ? EE::Environment : Environment)
+ expect_any_instance_of(Gitlab.ee? ? EE::Environment : Environment)
.to receive(:terminals)
.and_return([:fake_terminal])
@@ -483,10 +483,8 @@ describe Projects::EnvironmentsController do
end
shared_examples_for 'the default dashboard' do
- all_dashboards = Feature.enabled?(:environment_metrics_show_multiple_dashboards)
-
it_behaves_like '200 response'
- it_behaves_like 'includes all dashboards' if all_dashboards
+ it_behaves_like 'includes all dashboards'
it 'is the default dashboard' do
get :metrics_dashboard, params: environment_params(dashboard_params)
@@ -618,16 +616,6 @@ describe Projects::EnvironmentsController do
it_behaves_like 'the default dashboard'
it_behaves_like 'dashboard can be specified'
it_behaves_like 'dashboard can be embedded'
-
- context 'when multiple dashboards is disabled' do
- before do
- stub_feature_flags(environment_metrics_show_multiple_dashboards: false)
- end
-
- it_behaves_like 'the default dashboard'
- it_behaves_like 'dashboard cannot be specified'
- it_behaves_like 'dashboard can be embedded'
- end
end
describe 'GET #search' do
diff --git a/spec/controllers/projects/error_tracking_controller_spec.rb b/spec/controllers/projects/error_tracking_controller_spec.rb
index d11ef24ef96..4c224e960a6 100644
--- a/spec/controllers/projects/error_tracking_controller_spec.rb
+++ b/spec/controllers/projects/error_tracking_controller_spec.rb
@@ -3,8 +3,8 @@
require 'spec_helper'
describe Projects::ErrorTrackingController do
- set(:project) { create(:project) }
- set(:user) { create(:user) }
+ let_it_be(:project) { create(:project) }
+ let_it_be(:user) { create(:user) }
before do
sign_in(user)
diff --git a/spec/controllers/projects/git_http_controller_spec.rb b/spec/controllers/projects/git_http_controller_spec.rb
index 88fa2236e33..b756dd5662d 100644
--- a/spec/controllers/projects/git_http_controller_spec.rb
+++ b/spec/controllers/projects/git_http_controller_spec.rb
@@ -22,5 +22,30 @@ describe Projects::GitHttpController do
expect(response.status).to eq(401)
end
+
+ context 'with exceptions' do
+ let(:project) { create(:project, :public, :repository) }
+
+ before do
+ allow(controller).to receive(:verify_workhorse_api!).and_return(true)
+ end
+
+ it 'returns 503 with GRPC Unavailable' do
+ allow(controller).to receive(:access_check).and_raise(GRPC::Unavailable)
+
+ get :info_refs, params: { service: 'git-upload-pack', namespace_id: project.namespace.to_param, project_id: project.path + '.git' }
+
+ expect(response.status).to eq(503)
+ end
+
+ it 'returns 503 with timeout error' do
+ allow(controller).to receive(:access_check).and_raise(Gitlab::GitAccess::TimeoutError)
+
+ get :info_refs, params: { service: 'git-upload-pack', namespace_id: project.namespace.to_param, project_id: project.path + '.git' }
+
+ expect(response.status).to eq(503)
+ expect(response.body).to eq 'Gitlab::GitAccess::TimeoutError'
+ end
+ end
end
end
diff --git a/spec/controllers/projects/grafana_api_controller_spec.rb b/spec/controllers/projects/grafana_api_controller_spec.rb
new file mode 100644
index 00000000000..352a364295b
--- /dev/null
+++ b/spec/controllers/projects/grafana_api_controller_spec.rb
@@ -0,0 +1,97 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Projects::GrafanaApiController do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:user) { create(:user) }
+
+ before do
+ project.add_reporter(user)
+ sign_in(user)
+ end
+
+ describe 'GET #proxy' do
+ let(:proxy_service) { instance_double(Grafana::ProxyService) }
+ let(:params) do
+ {
+ namespace_id: project.namespace.full_path,
+ project_id: project.name,
+ proxy_path: 'api/v1/query_range',
+ datasource_id: '1',
+ query: 'rate(relevant_metric)',
+ start: '1570441248',
+ end: '1570444848',
+ step: '900'
+ }
+ end
+
+ before do
+ allow(Grafana::ProxyService).to receive(:new).and_return(proxy_service)
+ allow(proxy_service).to receive(:execute).and_return(service_result)
+ end
+
+ shared_examples_for 'error response' do |http_status|
+ it "returns #{http_status}" do
+ get :proxy, params: params
+
+ expect(response).to have_gitlab_http_status(http_status)
+ expect(json_response['status']).to eq('error')
+ expect(json_response['message']).to eq('error message')
+ end
+ end
+
+ context 'with a successful result' do
+ let(:service_result) { { status: :success, body: '{}' } }
+
+ it 'returns a grafana datasource response' do
+ get :proxy, params: params
+
+ expect(Grafana::ProxyService)
+ .to have_received(:new)
+ .with(project, '1', 'api/v1/query_range',
+ params.slice(:query, :start, :end, :step).stringify_keys)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response).to eq({})
+ end
+ end
+
+ context 'when the request is still unavailable' do
+ let(:service_result) { nil }
+
+ it 'returns 204 no content' do
+ get :proxy, params: params
+
+ expect(response).to have_gitlab_http_status(:no_content)
+ expect(json_response['status']).to eq('processing')
+ expect(json_response['message']).to eq('Not ready yet. Try again later.')
+ end
+ end
+
+ context 'when an error has occurred' do
+ context 'with an error accessing grafana' do
+ let(:service_result) do
+ {
+ http_status: :service_unavailable,
+ status: :error,
+ message: 'error message'
+ }
+ end
+
+ it_behaves_like 'error response', :service_unavailable
+ end
+
+ context 'with a processing error' do
+ let(:service_result) do
+ {
+ status: :error,
+ message: 'error message'
+ }
+ end
+
+ it_behaves_like 'error response', :bad_request
+ end
+ end
+ end
+end
diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb
index ad57c29850b..c9558abab33 100644
--- a/spec/controllers/projects/issues_controller_spec.rb
+++ b/spec/controllers/projects/issues_controller_spec.rb
@@ -408,6 +408,7 @@ describe Projects::IssuesController do
context 'when user has access to update issue' do
before do
+ project.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
project.add_developer(user)
end
@@ -421,14 +422,30 @@ describe Projects::IssuesController do
context 'when Akismet is enabled and the issue is identified as spam' do
before do
stub_application_setting(recaptcha_enabled: true)
- allow_any_instance_of(SpamService).to receive(:check_for_spam?).and_return(true)
- allow_any_instance_of(AkismetService).to receive(:spam?).and_return(true)
+ expect_next_instance_of(AkismetService) do |akismet_service|
+ expect(akismet_service).to receive_messages(spam?: true)
+ end
+ end
+
+ context 'when allow_possible_spam feature flag is false' do
+ before do
+ stub_feature_flags(allow_possible_spam: false)
+ end
+
+ it 'renders json with recaptcha_html' do
+ subject
+
+ expect(json_response).to have_key('recaptcha_html')
+ end
end
- it 'renders json with recaptcha_html' do
- subject
+ context 'when allow_possible_spam feature flag is true' do
+ it 'updates the issue' do
+ subject
- expect(json_response).to have_key('recaptcha_html')
+ expect(response).to have_http_status(:ok)
+ expect(issue.reload.title).to eq('New title')
+ end
end
end
end
@@ -681,13 +698,13 @@ describe Projects::IssuesController do
before do
project.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
stub_application_setting(recaptcha_enabled: true)
- allow_any_instance_of(SpamService).to receive(:check_for_spam?).and_return(true)
end
context 'when an issue is not identified as spam' do
before do
- allow_any_instance_of(described_class).to receive(:verify_recaptcha).and_return(false)
- allow_any_instance_of(AkismetService).to receive(:spam?).and_return(false)
+ expect_next_instance_of(AkismetService) do |akismet_service|
+ expect(akismet_service).to receive_messages(spam?: false)
+ end
end
it 'normally updates the issue' do
@@ -696,45 +713,64 @@ describe Projects::IssuesController do
end
context 'when an issue is identified as spam' do
- before do
- allow_any_instance_of(AkismetService).to receive(:spam?).and_return(true)
- end
-
context 'when captcha is not verified' do
before do
- allow_any_instance_of(described_class).to receive(:verify_recaptcha).and_return(false)
+ expect_next_instance_of(AkismetService) do |akismet_service|
+ expect(akismet_service).to receive_messages(spam?: true)
+ end
end
- it 'rejects an issue recognized as a spam' do
- expect { update_issue }.not_to change { issue.reload.title }
- end
+ context 'when allow_possible_spam feature flag is false' do
+ before do
+ stub_feature_flags(allow_possible_spam: false)
+ end
- it 'rejects an issue recognized as a spam when recaptcha disabled' do
- stub_application_setting(recaptcha_enabled: false)
+ it 'rejects an issue recognized as a spam' do
+ expect { update_issue }.not_to change { issue.reload.title }
+ end
- expect { update_issue }.not_to change { issue.reload.title }
- end
+ it 'rejects an issue recognized as a spam when recaptcha disabled' do
+ stub_application_setting(recaptcha_enabled: false)
- it 'creates a spam log' do
- update_issue(issue_params: { title: 'Spam title' })
+ expect { update_issue }.not_to change { issue.reload.title }
+ end
- spam_logs = SpamLog.all
+ it 'creates a spam log' do
+ expect { update_issue(issue_params: { title: 'Spam title' }) }
+ .to log_spam(title: 'Spam title', noteable_type: 'Issue')
+ end
- expect(spam_logs.count).to eq(1)
- expect(spam_logs.first.title).to eq('Spam title')
- expect(spam_logs.first.recaptcha_verified).to be_falsey
- end
+ it 'renders recaptcha_html json response' do
+ update_issue
+
+ expect(json_response).to have_key('recaptcha_html')
+ end
- it 'renders recaptcha_html json response' do
- update_issue
+ it 'returns 200 status' do
+ update_issue
- expect(json_response).to have_key('recaptcha_html')
+ expect(response).to have_gitlab_http_status(200)
+ end
end
- it 'returns 200 status' do
- update_issue
+ context 'when allow_possible_spam feature flag is true' do
+ it 'updates the issue recognized as spam' do
+ expect { update_issue }.to change { issue.reload.title }
+ end
- expect(response).to have_gitlab_http_status(200)
+ it 'creates a spam log' do
+ expect { update_issue(issue_params: { title: 'Spam title' }) }
+ .to log_spam(
+ title: 'Spam title', description: issue.description,
+ noteable_type: 'Issue', recaptcha_verified: false
+ )
+ end
+
+ it 'returns 200 status' do
+ update_issue
+
+ expect(response).to have_gitlab_http_status(200)
+ end
end
end
@@ -748,11 +784,6 @@ describe Projects::IssuesController do
additional_params: { spam_log_id: spam_logs.last.id, recaptcha_verification: true })
end
- before do
- allow_any_instance_of(described_class).to receive(:verify_recaptcha)
- .and_return(true)
- end
-
it 'returns 200 status' do
expect(response).to have_gitlab_http_status(200)
end
@@ -917,55 +948,72 @@ describe Projects::IssuesController do
context 'Akismet is enabled' do
before do
stub_application_setting(recaptcha_enabled: true)
- allow_any_instance_of(SpamService).to receive(:check_for_spam?).and_return(true)
end
context 'when an issue is not identified as spam' do
before do
- allow_any_instance_of(described_class).to receive(:verify_recaptcha).and_return(false)
- allow_any_instance_of(AkismetService).to receive(:spam?).and_return(false)
+ stub_feature_flags(allow_possible_spam: false)
+
+ expect_next_instance_of(AkismetService) do |akismet_service|
+ expect(akismet_service).to receive_messages(spam?: false)
+ end
end
- it 'does not create an issue' do
- expect { post_new_issue(title: '') }.not_to change(Issue, :count)
+ it 'creates an issue' do
+ expect { post_new_issue(title: 'Some title') }.to change(Issue, :count)
end
end
context 'when an issue is identified as spam' do
- before do
- allow_any_instance_of(AkismetService).to receive(:spam?).and_return(true)
- end
-
context 'when captcha is not verified' do
def post_spam_issue
post_new_issue(title: 'Spam Title', description: 'Spam lives here')
end
before do
- allow_any_instance_of(described_class).to receive(:verify_recaptcha).and_return(false)
+ expect_next_instance_of(AkismetService) do |akismet_service|
+ expect(akismet_service).to receive_messages(spam?: true)
+ end
end
- it 'rejects an issue recognized as a spam' do
- expect { post_spam_issue }.not_to change(Issue, :count)
- end
+ context 'when allow_possible_spam feature flag is false' do
+ before do
+ stub_feature_flags(allow_possible_spam: false)
+ end
- it 'creates a spam log' do
- post_spam_issue
- spam_logs = SpamLog.all
+ it 'rejects an issue recognized as a spam' do
+ expect { post_spam_issue }.not_to change(Issue, :count)
+ end
- expect(spam_logs.count).to eq(1)
- expect(spam_logs.first.title).to eq('Spam Title')
- expect(spam_logs.first.recaptcha_verified).to be_falsey
- end
+ it 'creates a spam log' do
+ expect { post_spam_issue }
+ .to log_spam(title: 'Spam Title', noteable_type: 'Issue', recaptcha_verified: false)
+ end
- it 'does not create an issue when it is not valid' do
- expect { post_new_issue(title: '') }.not_to change(Issue, :count)
+ it 'does not create an issue when it is not valid' do
+ expect { post_new_issue(title: '') }.not_to change(Issue, :count)
+ end
+
+ it 'does not create an issue when recaptcha is not enabled' do
+ stub_application_setting(recaptcha_enabled: false)
+
+ expect { post_spam_issue }.not_to change(Issue, :count)
+ end
end
- it 'does not create an issue when recaptcha is not enabled' do
- stub_application_setting(recaptcha_enabled: false)
+ context 'when allow_possible_spam feature flag is true' do
+ it 'creates an issue recognized as spam' do
+ expect { post_spam_issue }.to change(Issue, :count)
+ end
- expect { post_spam_issue }.not_to change(Issue, :count)
+ it 'creates a spam log' do
+ expect { post_spam_issue }
+ .to log_spam(title: 'Spam Title', noteable_type: 'Issue', recaptcha_verified: false)
+ end
+
+ it 'does not create an issue when it is not valid' do
+ expect { post_new_issue(title: '') }.not_to change(Issue, :count)
+ end
end
end
@@ -977,7 +1025,7 @@ describe Projects::IssuesController do
end
before do
- allow_any_instance_of(described_class).to receive(:verify_recaptcha).and_return(true)
+ expect(controller).to receive_messages(verify_recaptcha: true)
end
it 'accepts an issue after recaptcha is verified' do
@@ -1030,8 +1078,12 @@ describe Projects::IssuesController do
describe 'POST #mark_as_spam' do
context 'properly submits to Akismet' do
before do
- allow_any_instance_of(AkismetService).to receive_messages(submit_spam: true)
- allow_any_instance_of(ApplicationSetting).to receive_messages(akismet_enabled: true)
+ expect_next_instance_of(AkismetService) do |akismet_service|
+ expect(akismet_service).to receive_messages(submit_spam: true)
+ end
+ expect_next_instance_of(ApplicationSetting) do |setting|
+ expect(setting).to receive_messages(akismet_enabled: true)
+ end
end
def post_spam
@@ -1128,6 +1180,7 @@ describe Projects::IssuesController do
name: emoji_name
})
end
+
let(:emoji_name) { 'thumbsup' }
it "toggles the award emoji" do
@@ -1266,7 +1319,9 @@ describe Projects::IssuesController do
end
it "shows error when upload fails" do
- allow_any_instance_of(UploadService).to receive(:execute).and_return(nil)
+ expect_next_instance_of(UploadService) do |upload_service|
+ expect(upload_service).to receive(:execute).and_return(nil)
+ end
import_csv
diff --git a/spec/controllers/projects/jobs_controller_spec.rb b/spec/controllers/projects/jobs_controller_spec.rb
index 53d32665b0c..90ccb884927 100644
--- a/spec/controllers/projects/jobs_controller_spec.rb
+++ b/spec/controllers/projects/jobs_controller_spec.rb
@@ -527,6 +527,7 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do
describe 'GET trace.json' do
before do
+ stub_feature_flags(job_log_json: true)
get_trace
end
@@ -535,8 +536,119 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do
it 'returns a trace' do
expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to match_response_schema('job/build_trace')
expect(json_response['id']).to eq job.id
expect(json_response['status']).to eq job.status
+ expect(json_response['state']).to be_present
+ expect(json_response['append']).not_to be_nil
+ expect(json_response['truncated']).not_to be_nil
+ expect(json_response['size']).to be_present
+ expect(json_response['total']).to be_present
+ expect(json_response['lines'].count).to be_positive
+ end
+ end
+
+ context 'when job has a trace' do
+ let(:job) { create(:ci_build, :trace_live, pipeline: pipeline) }
+
+ it 'returns a trace' do
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to match_response_schema('job/build_trace')
+ expect(json_response['id']).to eq job.id
+ expect(json_response['status']).to eq job.status
+ expect(json_response['lines']).to eq [{ 'content' => [{ 'text' => 'BUILD TRACE' }], 'offset' => 0 }]
+ end
+ end
+
+ context 'when job has no traces' do
+ let(:job) { create(:ci_build, pipeline: pipeline) }
+
+ it 'returns no traces' do
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to match_response_schema('job/build_trace')
+ expect(json_response['id']).to eq job.id
+ expect(json_response['status']).to eq job.status
+ expect(json_response['lines']).to be_nil
+ end
+ end
+
+ context 'when job has a trace with ANSI sequence and Unicode' do
+ let(:job) { create(:ci_build, :unicode_trace_live, pipeline: pipeline) }
+
+ it 'returns a trace with Unicode' do
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to match_response_schema('job/build_trace')
+ expect(json_response['id']).to eq job.id
+ expect(json_response['status']).to eq job.status
+ expect(json_response['lines'].flat_map {|l| l['content'].map { |c| c['text'] } }).to include("ヾ(´༎ຶД༎ຶ`)ノ")
+ end
+ end
+
+ context 'when trace artifact is in ObjectStorage' do
+ let(:url) { 'http://object-storage/trace' }
+ let(:file_path) { expand_fixture_path('trace/sample_trace') }
+ let!(:job) { create(:ci_build, :success, :trace_artifact, pipeline: pipeline) }
+
+ before do
+ allow_any_instance_of(JobArtifactUploader).to receive(:file_storage?) { false }
+ allow_any_instance_of(JobArtifactUploader).to receive(:url) { url }
+ allow_any_instance_of(JobArtifactUploader).to receive(:size) { File.size(file_path) }
+ end
+
+ context 'when there are no network issues' do
+ before do
+ stub_remote_url_206(url, file_path)
+
+ get_trace
+ end
+
+ it 'returns a trace' do
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['id']).to eq job.id
+ expect(json_response['status']).to eq job.status
+ expect(json_response['lines'].count).to be_positive
+ end
+ end
+
+ context 'when there is a network issue' do
+ before do
+ stub_remote_url_500(url)
+ end
+
+ it 'returns a trace' do
+ expect { get_trace }.to raise_error(Gitlab::HttpIO::FailedToGetChunkError)
+ end
+ end
+ end
+
+ def get_trace
+ get :trace,
+ params: {
+ namespace_id: project.namespace,
+ project_id: project,
+ id: job.id
+ },
+ format: :json
+ end
+ end
+
+ describe 'GET legacy trace.json' do
+ before do
+ get_trace
+ end
+
+ context 'when job has a trace artifact' do
+ let(:job) { create(:ci_build, :trace_artifact, pipeline: pipeline) }
+
+ it 'returns a trace' do
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['id']).to eq job.id
+ expect(json_response['status']).to eq job.status
+ expect(json_response['state']).to be_present
+ expect(json_response['append']).not_to be_nil
+ expect(json_response['truncated']).not_to be_nil
+ expect(json_response['size']).to be_present
+ expect(json_response['total']).to be_present
expect(json_response['html']).to eq(job.trace.html)
end
end
@@ -612,12 +724,13 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do
end
def get_trace
- get :trace, params: {
- namespace_id: project.namespace,
- project_id: project,
- id: job.id
- },
- format: :json
+ get :trace,
+ params: {
+ namespace_id: project.namespace,
+ project_id: project,
+ id: job.id
+ },
+ format: :json
end
end
diff --git a/spec/controllers/projects/merge_requests/diffs_controller_spec.rb b/spec/controllers/projects/merge_requests/diffs_controller_spec.rb
index ac3e9901123..5c02e8d6461 100644
--- a/spec/controllers/projects/merge_requests/diffs_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests/diffs_controller_spec.rb
@@ -5,6 +5,49 @@ require 'spec_helper'
describe Projects::MergeRequests::DiffsController do
include ProjectForksHelper
+ shared_examples 'forked project with submodules' do
+ render_views
+
+ let(:project) { create(:project, :repository) }
+ let(:forked_project) { fork_project_with_submodules(project) }
+ let(:merge_request) { create(:merge_request_with_diffs, source_project: forked_project, source_branch: 'add-submodule-version-bump', target_branch: 'master', target_project: project) }
+
+ before do
+ project.add_developer(user)
+
+ merge_request.reload
+ go
+ end
+
+ it 'renders' do
+ expect(response).to be_successful
+ expect(response.body).to have_content('Subproject commit')
+ end
+ end
+
+ shared_examples 'persisted preferred diff view cookie' do
+ context 'with view param' do
+ before do
+ go(view: 'parallel')
+ end
+
+ it 'saves the preferred diff view in a cookie' do
+ expect(response.cookies['diff_view']).to eq('parallel')
+ end
+ end
+
+ context 'when the user cannot view the merge request' do
+ before do
+ project.team.truncate
+ go
+ end
+
+ it 'returns a 404' do
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+ end
+
let(:project) { create(:project, :repository) }
let(:user) { create(:user) }
let(:merge_request) { create(:merge_request_with_diffs, target_project: project, source_project: project) }
@@ -39,9 +82,9 @@ describe Projects::MergeRequests::DiffsController do
end
end
- context 'when note has no position' do
+ context 'when note is a legacy diff note' do
before do
- create(:legacy_diff_note_on_merge_request, project: project, noteable: merge_request, position: nil)
+ create(:legacy_diff_note_on_merge_request, project: project, noteable: merge_request)
end
it 'serializes merge request diff collection' do
@@ -51,34 +94,138 @@ describe Projects::MergeRequests::DiffsController do
end
end
- context 'with forked projects with submodules' do
- render_views
+ it_behaves_like 'forked project with submodules'
+ end
- let(:project) { create(:project, :repository) }
- let(:forked_project) { fork_project_with_submodules(project) }
- let(:merge_request) { create(:merge_request_with_diffs, source_project: forked_project, source_branch: 'add-submodule-version-bump', target_branch: 'master', target_project: project) }
+ it_behaves_like 'persisted preferred diff view cookie'
+ end
- before do
- project.add_developer(user)
+ describe 'GET diffs_metadata' do
+ def go(extra_params = {})
+ params = {
+ namespace_id: project.namespace.to_param,
+ project_id: project,
+ id: merge_request.iid,
+ format: 'json'
+ }
- merge_request.reload
- go
+ get :diffs_metadata, params: params.merge(extra_params)
+ end
+
+ context 'when not authorized' do
+ let(:another_user) { create(:user) }
+
+ before do
+ sign_in(another_user)
+ end
+
+ it 'returns 404 when not a member' do
+ go
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+
+ it 'returns 404 when visibility level is not enough' do
+ project.add_guest(another_user)
+
+ go
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+
+ context 'when diffable does not exists' do
+ it 'returns 404' do
+ go(diff_id: 9999)
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+
+ context 'with valid diff_id' do
+ it 'returns success' do
+ go(diff_id: merge_request.merge_request_diff.id)
+
+ expect(response).to have_gitlab_http_status(200)
+ end
+
+ it 'serializes diffs metadata with expected arguments' do
+ expected_options = {
+ environment: nil,
+ merge_request: merge_request,
+ merge_request_diff: merge_request.merge_request_diff,
+ merge_request_diffs: merge_request.merge_request_diffs,
+ start_version: nil,
+ start_sha: nil,
+ commit: nil,
+ latest_diff: true
+ }
+
+ expect_next_instance_of(DiffsMetadataSerializer) do |instance|
+ expect(instance).to receive(:represent)
+ .with(an_instance_of(Gitlab::Diff::FileCollection::MergeRequestDiff), expected_options)
+ .and_call_original
end
- it 'renders' do
- expect(response).to be_successful
- expect(response.body).to have_content('Subproject commit')
+ go(diff_id: merge_request.merge_request_diff.id)
+ end
+ end
+
+ context 'with MR regular diff params' do
+ it 'returns success' do
+ go
+
+ expect(response).to have_gitlab_http_status(200)
+ end
+
+ it 'serializes diffs metadata with expected arguments' do
+ expected_options = {
+ environment: nil,
+ merge_request: merge_request,
+ merge_request_diff: merge_request.merge_request_diff,
+ merge_request_diffs: merge_request.merge_request_diffs,
+ start_version: nil,
+ start_sha: nil,
+ commit: nil,
+ latest_diff: true
+ }
+
+ expect_next_instance_of(DiffsMetadataSerializer) do |instance|
+ expect(instance).to receive(:represent)
+ .with(an_instance_of(Gitlab::Diff::FileCollection::MergeRequestDiff), expected_options)
+ .and_call_original
end
+
+ go
end
end
- context 'with view' do
- before do
- go(view: 'parallel')
+ context 'with commit param' do
+ it 'returns success' do
+ go(commit_id: merge_request.diff_head_sha)
+
+ expect(response).to have_gitlab_http_status(200)
end
- it 'saves the preferred diff view in a cookie' do
- expect(response.cookies['diff_view']).to eq('parallel')
+ it 'serializes diffs metadata with expected arguments' do
+ expected_options = {
+ environment: nil,
+ merge_request: merge_request,
+ merge_request_diff: nil,
+ merge_request_diffs: merge_request.merge_request_diffs,
+ start_version: nil,
+ start_sha: nil,
+ commit: merge_request.diff_head_commit,
+ latest_diff: nil
+ }
+
+ expect_next_instance_of(DiffsMetadataSerializer) do |instance|
+ expect(instance).to receive(:represent)
+ .with(an_instance_of(Gitlab::Diff::FileCollection::Commit), expected_options)
+ .and_call_original
+ end
+
+ go(commit_id: merge_request.diff_head_sha)
end
end
end
@@ -154,4 +301,113 @@ describe Projects::MergeRequests::DiffsController do
end
end
end
+
+ describe 'GET diffs_batch' do
+ def go(extra_params = {})
+ params = {
+ namespace_id: project.namespace.to_param,
+ project_id: project,
+ id: merge_request.iid,
+ format: 'json'
+ }
+
+ get :diffs_batch, params: params.merge(extra_params)
+ end
+
+ context 'when feature is disabled' do
+ before do
+ stub_feature_flags(diffs_batch_load: false)
+ end
+
+ it 'returns 404' do
+ go
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+
+ context 'when not authorized' do
+ let(:other_user) { create(:user) }
+
+ before do
+ sign_in(other_user)
+ end
+
+ it 'returns 404' do
+ go
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+
+ context 'with default params' do
+ let(:expected_options) do
+ {
+ merge_request: merge_request,
+ pagination_data: {
+ current_page: 1,
+ next_page: nil,
+ total_pages: 1
+ }
+ }
+ end
+
+ it 'serializes paginated merge request diff collection' do
+ expect_next_instance_of(PaginatedDiffSerializer) do |instance|
+ expect(instance).to receive(:represent)
+ .with(an_instance_of(Gitlab::Diff::FileCollection::MergeRequestDiffBatch), expected_options)
+ .and_call_original
+ end
+
+ go
+ end
+ end
+
+ context 'with smaller diff batch params' do
+ let(:expected_options) do
+ {
+ merge_request: merge_request,
+ pagination_data: {
+ current_page: 2,
+ next_page: 3,
+ total_pages: 4
+ }
+ }
+ end
+
+ it 'serializes paginated merge request diff collection' do
+ expect_next_instance_of(PaginatedDiffSerializer) do |instance|
+ expect(instance).to receive(:represent)
+ .with(an_instance_of(Gitlab::Diff::FileCollection::MergeRequestDiffBatch), expected_options)
+ .and_call_original
+ end
+
+ go(page: 2, per_page: 5)
+ end
+ end
+
+ it_behaves_like 'forked project with submodules'
+ it_behaves_like 'persisted preferred diff view cookie'
+
+ context 'diff unfolding' do
+ let!(:unfoldable_diff_note) do
+ create(:diff_note_on_merge_request, :folded_position, project: project, noteable: merge_request)
+ end
+
+ let!(:diff_note) do
+ create(:diff_note_on_merge_request, project: project, noteable: merge_request)
+ end
+
+ it 'unfolds correct diff file positions' do
+ expect_next_instance_of(Gitlab::Diff::FileCollection::MergeRequestDiffBatch) do |instance|
+ expect(instance)
+ .to receive(:unfold_diff_files)
+ .with([unfoldable_diff_note.position])
+ .and_call_original
+ end
+
+ go
+ end
+ end
+ end
end
diff --git a/spec/controllers/projects/mirrors_controller_spec.rb b/spec/controllers/projects/mirrors_controller_spec.rb
index 51ce9e2544f..fb3dd75460a 100644
--- a/spec/controllers/projects/mirrors_controller_spec.rb
+++ b/spec/controllers/projects/mirrors_controller_spec.rb
@@ -6,7 +6,7 @@ describe Projects::MirrorsController do
include ReactiveCachingHelpers
describe 'setting up a remote mirror' do
- set(:project) { create(:project, :repository) }
+ let_it_be(:project) { create(:project, :repository) }
context 'when the current project is not a mirror' do
it 'allows to create a remote mirror' do
diff --git a/spec/controllers/projects/notes_controller_spec.rb b/spec/controllers/projects/notes_controller_spec.rb
index 4db77921f24..3ab191c0032 100644
--- a/spec/controllers/projects/notes_controller_spec.rb
+++ b/spec/controllers/projects/notes_controller_spec.rb
@@ -713,6 +713,7 @@ describe Projects::NotesController do
end
subject { post(:toggle_award_emoji, params: request_params.merge(name: emoji_name)) }
+
let(:emoji_name) { 'thumbsup' }
it "toggles the award emoji" do
diff --git a/spec/controllers/projects/pipeline_schedules_controller_spec.rb b/spec/controllers/projects/pipeline_schedules_controller_spec.rb
index 850ef9c92fb..63e2c8a339c 100644
--- a/spec/controllers/projects/pipeline_schedules_controller_spec.rb
+++ b/spec/controllers/projects/pipeline_schedules_controller_spec.rb
@@ -5,9 +5,9 @@ require 'spec_helper'
describe Projects::PipelineSchedulesController do
include AccessMatchersForController
- set(:user) { create(:user) }
- set(:project) { create(:project, :public, :repository) }
- set(:pipeline_schedule) { create(:ci_pipeline_schedule, project: project) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project, :public, :repository) }
+ let_it_be(:pipeline_schedule) { create(:ci_pipeline_schedule, project: project) }
before do
project.add_developer(user)
@@ -60,8 +60,6 @@ describe Projects::PipelineSchedulesController do
end
describe 'GET #new' do
- set(:user) { create(:user) }
-
before do
project.add_developer(user)
sign_in(user)
@@ -77,8 +75,6 @@ describe Projects::PipelineSchedulesController do
describe 'POST #create' do
describe 'functionality' do
- set(:user) { create(:user) }
-
before do
project.add_developer(user)
sign_in(user)
@@ -149,7 +145,6 @@ describe Projects::PipelineSchedulesController do
describe 'PUT #update' do
describe 'functionality' do
- set(:user) { create(:user) }
let!(:pipeline_schedule) { create(:ci_pipeline_schedule, project: project, owner: user) }
before do
@@ -383,7 +378,6 @@ describe Projects::PipelineSchedulesController do
end
describe 'POST #play', :clean_gitlab_redis_cache do
- set(:user) { create(:user) }
let(:ref) { 'master' }
before do
@@ -442,8 +436,6 @@ describe Projects::PipelineSchedulesController do
end
describe 'DELETE #destroy' do
- set(:user) { create(:user) }
-
context 'when a developer makes the request' do
before do
project.add_developer(user)
diff --git a/spec/controllers/projects/pipelines_controller_spec.rb b/spec/controllers/projects/pipelines_controller_spec.rb
index 212d8b15252..e3ad36f8d24 100644
--- a/spec/controllers/projects/pipelines_controller_spec.rb
+++ b/spec/controllers/projects/pipelines_controller_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
describe Projects::PipelinesController do
include ApiHelpers
- set(:user) { create(:user) }
+ let_it_be(:user) { create(:user) }
let(:project) { create(:project, :public, :repository) }
let(:feature) { ProjectFeature::ENABLED }
@@ -217,6 +217,193 @@ describe Projects::PipelinesController do
end
end
+ context 'with triggered pipelines' do
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:source_project) { create(:project, :repository) }
+ let_it_be(:target_project) { create(:project, :repository) }
+ let_it_be(:root_pipeline) { create_pipeline(project) }
+ let_it_be(:source_pipeline) { create_pipeline(source_project) }
+ let_it_be(:source_of_source_pipeline) { create_pipeline(source_project) }
+ let_it_be(:target_pipeline) { create_pipeline(target_project) }
+ let_it_be(:target_of_target_pipeline) { create_pipeline(target_project) }
+
+ before do
+ create_link(source_of_source_pipeline, source_pipeline)
+ create_link(source_pipeline, root_pipeline)
+ create_link(root_pipeline, target_pipeline)
+ create_link(target_pipeline, target_of_target_pipeline)
+ end
+
+ shared_examples 'not expanded' do
+ let(:expected_stages) { be_nil }
+
+ it 'does return base details' do
+ get_pipeline_json(root_pipeline)
+
+ expect(json_response['triggered_by']).to include('id' => source_pipeline.id)
+ expect(json_response['triggered']).to contain_exactly(
+ include('id' => target_pipeline.id))
+ end
+
+ it 'does not expand triggered_by pipeline' do
+ get_pipeline_json(root_pipeline)
+
+ triggered_by = json_response['triggered_by']
+ expect(triggered_by['triggered_by']).to be_nil
+ expect(triggered_by['triggered']).to be_nil
+ expect(triggered_by['details']['stages']).to expected_stages
+ end
+
+ it 'does not expand triggered pipelines' do
+ get_pipeline_json(root_pipeline)
+
+ first_triggered = json_response['triggered'].first
+ expect(first_triggered['triggered_by']).to be_nil
+ expect(first_triggered['triggered']).to be_nil
+ expect(first_triggered['details']['stages']).to expected_stages
+ end
+ end
+
+ shared_examples 'expanded' do
+ it 'does return base details' do
+ get_pipeline_json(root_pipeline)
+
+ expect(json_response['triggered_by']).to include('id' => source_pipeline.id)
+ expect(json_response['triggered']).to contain_exactly(
+ include('id' => target_pipeline.id))
+ end
+
+ it 'does expand triggered_by pipeline' do
+ get_pipeline_json(root_pipeline)
+
+ triggered_by = json_response['triggered_by']
+ expect(triggered_by['triggered_by']).to include(
+ 'id' => source_of_source_pipeline.id)
+ expect(triggered_by['details']['stages']).not_to be_nil
+ end
+
+ it 'does not recursively expand triggered_by' do
+ get_pipeline_json(root_pipeline)
+
+ triggered_by = json_response['triggered_by']
+ expect(triggered_by['triggered']).to be_nil
+ end
+
+ it 'does expand triggered pipelines' do
+ get_pipeline_json(root_pipeline)
+
+ first_triggered = json_response['triggered'].first
+ expect(first_triggered['triggered']).to contain_exactly(
+ include('id' => target_of_target_pipeline.id))
+ expect(first_triggered['details']['stages']).not_to be_nil
+ end
+
+ it 'does not recursively expand triggered' do
+ get_pipeline_json(root_pipeline)
+
+ first_triggered = json_response['triggered'].first
+ expect(first_triggered['triggered_by']).to be_nil
+ end
+ end
+
+ context 'when it does have permission to read other projects' do
+ before do
+ source_project.add_developer(user)
+ target_project.add_developer(user)
+ end
+
+ context 'when not-expanding any pipelines' do
+ let(:expanded) { nil }
+
+ it_behaves_like 'not expanded'
+ end
+
+ context 'when expanding non-existing pipeline' do
+ let(:expanded) { [-1] }
+
+ it_behaves_like 'not expanded'
+ end
+
+ context 'when expanding pipeline that is not directly expandable' do
+ let(:expanded) { [source_of_source_pipeline.id, target_of_target_pipeline.id] }
+
+ it_behaves_like 'not expanded'
+ end
+
+ context 'when expanding self' do
+ let(:expanded) { [root_pipeline.id] }
+
+ context 'it does not recursively expand pipelines' do
+ it_behaves_like 'not expanded'
+ end
+ end
+
+ context 'when expanding source and target pipeline' do
+ let(:expanded) { [source_pipeline.id, target_pipeline.id] }
+
+ it_behaves_like 'expanded'
+
+ context 'when expand depth is limited to 1' do
+ before do
+ stub_const('TriggeredPipelineEntity::MAX_EXPAND_DEPTH', 1)
+ end
+
+ it_behaves_like 'not expanded' do
+ # We expect that triggered/triggered_by is not expanded,
+ # but we still return details.stages for that pipeline
+ let(:expected_stages) { be_a(Array) }
+ end
+ end
+ end
+
+ context 'when expanding all' do
+ let(:expanded) do
+ [
+ source_of_source_pipeline.id,
+ source_pipeline.id,
+ root_pipeline.id,
+ target_pipeline.id,
+ target_of_target_pipeline.id
+ ]
+ end
+
+ it_behaves_like 'expanded'
+ end
+ end
+
+ context 'when does not have permission to read other projects' do
+ let(:expanded) { [source_pipeline.id, target_pipeline.id] }
+
+ it_behaves_like 'not expanded'
+ end
+
+ def create_pipeline(project)
+ create(:ci_empty_pipeline, project: project).tap do |pipeline|
+ create(:ci_build, pipeline: pipeline, stage: 'test', name: 'rspec')
+ end
+ end
+
+ def create_link(source_pipeline, pipeline)
+ source_pipeline.sourced_pipelines.create!(
+ source_job: source_pipeline.builds.all.sample,
+ source_project: source_pipeline.project,
+ project: pipeline.project,
+ pipeline: pipeline
+ )
+ end
+
+ def get_pipeline_json(pipeline)
+ params = {
+ namespace_id: pipeline.project.namespace,
+ project_id: pipeline.project,
+ id: pipeline,
+ expanded: expanded
+ }
+
+ get :show, params: params.compact, format: :json
+ end
+ end
+
def get_pipeline_json
get :show, params: { namespace_id: project.namespace, project_id: project, id: pipeline }, format: :json
end
@@ -398,6 +585,76 @@ describe Projects::PipelinesController do
end
end
+ describe 'GET test_report.json' do
+ subject(:get_test_report_json) do
+ post :test_report, params: {
+ namespace_id: project.namespace,
+ project_id: project,
+ id: pipeline.id
+ },
+ format: :json
+ end
+
+ context 'when feature is enabled' do
+ before do
+ stub_feature_flags(junit_pipeline_view: true)
+ end
+
+ context 'when pipeline does not have a test report' do
+ let(:pipeline) { create(:ci_pipeline, project: project) }
+
+ it 'renders an empty test report' do
+ get_test_report_json
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['total_count']).to eq(0)
+ end
+ end
+
+ context 'when pipeline has a test report' do
+ let(:pipeline) { create(:ci_pipeline, :with_test_reports, project: project) }
+
+ it 'renders the test report' do
+ get_test_report_json
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['total_count']).to eq(4)
+ end
+ end
+
+ context 'when pipeline has corrupt test reports' do
+ let(:pipeline) { create(:ci_pipeline, project: project) }
+
+ before do
+ job = create(:ci_build, pipeline: pipeline)
+ create(:ci_job_artifact, :junit_with_corrupted_data, job: job, project: project)
+ end
+
+ it 'renders the test reports' do
+ get_test_report_json
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['status']).to eq('error_parsing_report')
+ end
+ end
+ end
+
+ context 'when feature is disabled' do
+ let(:pipeline) { create(:ci_empty_pipeline, project: project) }
+
+ before do
+ stub_feature_flags(junit_pipeline_view: false)
+ end
+
+ it 'renders empty response' do
+ get_test_report_json
+
+ expect(response).to have_gitlab_http_status(:no_content)
+ expect(response.body).to be_empty
+ end
+ end
+ end
+
describe 'GET latest' do
let(:branch_main) { project.repository.branches[0] }
let(:branch_secondary) { project.repository.branches[1] }
diff --git a/spec/controllers/projects/pipelines_settings_controller_spec.rb b/spec/controllers/projects/pipelines_settings_controller_spec.rb
index 3656b4e3771..16a43f62bd5 100644
--- a/spec/controllers/projects/pipelines_settings_controller_spec.rb
+++ b/spec/controllers/projects/pipelines_settings_controller_spec.rb
@@ -3,8 +3,8 @@
require 'spec_helper'
describe Projects::PipelinesSettingsController do
- set(:user) { create(:user) }
- set(:project_auto_devops) { create(:project_auto_devops) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project_auto_devops) { create(:project_auto_devops) }
let(:project) { project_auto_devops.project }
before do
diff --git a/spec/controllers/projects/project_members_controller_spec.rb b/spec/controllers/projects/project_members_controller_spec.rb
index 5130e26c928..2f473d395ad 100644
--- a/spec/controllers/projects/project_members_controller_spec.rb
+++ b/spec/controllers/projects/project_members_controller_spec.rb
@@ -4,7 +4,7 @@ require('spec_helper')
describe Projects::ProjectMembersController do
let(:user) { create(:user) }
- let(:project) { create(:project, :public, :access_requestable) }
+ let(:project) { create(:project, :public) }
describe 'GET index' do
it 'has the project_members address with a 200 status code' do
diff --git a/spec/controllers/projects/registry/repositories_controller_spec.rb b/spec/controllers/projects/registry/repositories_controller_spec.rb
index 63d84b366d3..192e4ce2e73 100644
--- a/spec/controllers/projects/registry/repositories_controller_spec.rb
+++ b/spec/controllers/projects/registry/repositories_controller_spec.rb
@@ -40,6 +40,12 @@ describe Projects::Registry::RepositoriesController do
expect(response).to have_gitlab_http_status(:ok)
end
+ it 'tracks the event' do
+ expect(Gitlab::Tracking).to receive(:event).with(anything, 'list_repositories', {})
+
+ go_to_index
+ end
+
it 'creates a root container repository' do
expect { go_to_index }.to change { ContainerRepository.all.count }.by(1)
expect(ContainerRepository.first).to be_root_repository
@@ -92,8 +98,16 @@ describe Projects::Registry::RepositoriesController do
expect(DeleteContainerRepositoryWorker).to receive(:perform_async).with(user.id, repository.id)
delete_repository(repository)
+
expect(response).to have_gitlab_http_status(:no_content)
end
+
+ it 'tracks the event' do
+ expect(Gitlab::Tracking).to receive(:event).with(anything, 'delete_repository', {})
+ allow(DeleteContainerRepositoryWorker).to receive(:perform_async).with(user.id, repository.id)
+
+ delete_repository(repository)
+ end
end
end
end
diff --git a/spec/controllers/projects/registry/tags_controller_spec.rb b/spec/controllers/projects/registry/tags_controller_spec.rb
index c6e063d8229..5ab32b7d81d 100644
--- a/spec/controllers/projects/registry/tags_controller_spec.rb
+++ b/spec/controllers/projects/registry/tags_controller_spec.rb
@@ -10,6 +10,8 @@ describe Projects::Registry::TagsController do
create(:container_repository, name: 'image', project: project)
end
+ let(:service) { double('service') }
+
before do
sign_in(user)
stub_container_registry_config(enabled: true)
@@ -36,6 +38,12 @@ describe Projects::Registry::TagsController do
expect(response).to match_response_schema('registry/tags')
expect(response).to include_pagination_headers
end
+
+ it 'tracks the event' do
+ expect(Gitlab::Tracking).to receive(:event).with(anything, 'list_tags', {})
+
+ get_tags
+ end
end
context 'when user can read the registry' do
@@ -84,17 +92,24 @@ describe Projects::Registry::TagsController do
context 'when there is matching tag present' do
before do
- stub_container_registry_tags(repository: repository.path, tags: %w[rc1 test.])
+ stub_container_registry_tags(repository: repository.path, tags: %w[rc1], with_manifest: true)
end
it 'makes it possible to delete regular tag' do
- expect_any_instance_of(ContainerRegistry::Tag).to receive(:delete)
+ expect_delete_tags(%w[rc1])
destroy_tag('rc1')
end
it 'makes it possible to delete a tag that ends with a dot' do
- expect_any_instance_of(ContainerRegistry::Tag).to receive(:delete)
+ expect_delete_tags(%w[test.])
+
+ destroy_tag('test.')
+ end
+
+ it 'tracks the event' do
+ expect_delete_tags(%w[test.])
+ expect(controller).to receive(:track_event).with(:delete_tag)
destroy_tag('test.')
end
@@ -125,11 +140,19 @@ describe Projects::Registry::TagsController do
stub_container_registry_tags(repository: repository.path, tags: %w[rc1 test.])
end
+ let(:tags) { %w[tc1 test.] }
+
it 'makes it possible to delete tags in bulk' do
- allow_any_instance_of(ContainerRegistry::Tag).to receive(:delete) { |*args| ContainerRegistry::Tag.delete(*args) }
- expect(ContainerRegistry::Tag).to receive(:delete).exactly(2).times
+ expect_delete_tags(tags)
+
+ bulk_destroy_tags(tags)
+ end
+
+ it 'tracks the event' do
+ expect_delete_tags(tags)
+ expect(Gitlab::Tracking).to receive(:event).with(anything, 'delete_tag_bulk', {})
- bulk_destroy_tags(['rc1', 'test.'])
+ bulk_destroy_tags(tags)
end
end
end
@@ -146,4 +169,9 @@ describe Projects::Registry::TagsController do
format: :json
end
end
+
+ def expect_delete_tags(tags, status = :success)
+ expect(service).to receive(:execute).with(repository) { { status: status } }
+ expect(Projects::ContainerRepository::DeleteTagsService).to receive(:new).with(repository.project, user, tags: tags) { service }
+ end
end
diff --git a/spec/controllers/projects/serverless/functions_controller_spec.rb b/spec/controllers/projects/serverless/functions_controller_spec.rb
index 9f1ef3a4be8..eccc8e1d5de 100644
--- a/spec/controllers/projects/serverless/functions_controller_spec.rb
+++ b/spec/controllers/projects/serverless/functions_controller_spec.rb
@@ -107,26 +107,50 @@ describe Projects::Serverless::FunctionsController do
end
end
- context 'valid data', :use_clean_rails_memory_store_caching do
- before do
- stub_kubeclient_service_pods
- stub_reactive_cache(knative_services_finder,
- {
- services: kube_knative_services_body(namespace: namespace.namespace, name: cluster.project.name)["items"],
- pods: kube_knative_pods_body(cluster.project.name, namespace.namespace)["items"]
- },
- *knative_services_finder.cache_args)
+ context 'with valid data', :use_clean_rails_memory_store_caching do
+ shared_examples 'GET #show with valid data' do
+ it 'has a valid function name' do
+ get :show, params: params({ format: :json, environment_id: "*", id: cluster.project.name })
+ expect(response).to have_gitlab_http_status(200)
+
+ expect(json_response).to include(
+ "name" => project.name,
+ "url" => "http://#{project.name}.#{namespace.namespace}.example.com",
+ "podcount" => 1
+ )
+ end
end
- it 'has a valid function name' do
- get :show, params: params({ format: :json, environment_id: "*", id: cluster.project.name })
- expect(response).to have_gitlab_http_status(200)
+ context 'on Knative 0.5' do
+ before do
+ stub_kubeclient_service_pods
+ stub_reactive_cache(knative_services_finder,
+ {
+ services: kube_knative_services_body(
+ legacy_knative: true,
+ namespace: namespace.namespace,
+ name: cluster.project.name
+ )["items"],
+ pods: kube_knative_pods_body(cluster.project.name, namespace.namespace)["items"]
+ },
+ *knative_services_finder.cache_args)
+ end
- expect(json_response).to include(
- "name" => project.name,
- "url" => "http://#{project.name}.#{namespace.namespace}.example.com",
- "podcount" => 1
- )
+ include_examples 'GET #show with valid data'
+ end
+
+ context 'on Knative 0.6 or 0.7' do
+ before do
+ stub_kubeclient_service_pods
+ stub_reactive_cache(knative_services_finder,
+ {
+ services: kube_knative_services_body(namespace: namespace.namespace, name: cluster.project.name)["items"],
+ pods: kube_knative_pods_body(cluster.project.name, namespace.namespace)["items"]
+ },
+ *knative_services_finder.cache_args)
+ end
+
+ include_examples 'GET #show with valid data'
end
end
end
@@ -141,38 +165,60 @@ describe Projects::Serverless::FunctionsController do
end
describe 'GET #index with data', :use_clean_rails_memory_store_caching do
- before do
- stub_kubeclient_service_pods
- stub_reactive_cache(knative_services_finder,
- {
- services: kube_knative_services_body(namespace: namespace.namespace, name: cluster.project.name)["items"],
- pods: kube_knative_pods_body(cluster.project.name, namespace.namespace)["items"]
- },
- *knative_services_finder.cache_args)
+ shared_examples 'GET #index with data' do
+ it 'has data' do
+ get :index, params: params({ format: :json })
+
+ expect(response).to have_gitlab_http_status(200)
+
+ expect(json_response).to match({
+ "knative_installed" => "checking",
+ "functions" => [
+ a_hash_including(
+ "name" => project.name,
+ "url" => "http://#{project.name}.#{namespace.namespace}.example.com"
+ )
+ ]
+ })
+ end
+
+ it 'has data in html' do
+ get :index, params: params
+
+ expect(response).to have_gitlab_http_status(200)
+ end
end
- it 'has data' do
- get :index, params: params({ format: :json })
-
- expect(response).to have_gitlab_http_status(200)
-
- expect(json_response).to match(
- {
- "knative_installed" => "checking",
- "functions" => [
- a_hash_including(
- "name" => project.name,
- "url" => "http://#{project.name}.#{namespace.namespace}.example.com"
- )
- ]
- }
- )
+ context 'on Knative 0.5' do
+ before do
+ stub_kubeclient_service_pods
+ stub_reactive_cache(knative_services_finder,
+ {
+ services: kube_knative_services_body(
+ legacy_knative: true,
+ namespace: namespace.namespace,
+ name: cluster.project.name
+ )["items"],
+ pods: kube_knative_pods_body(cluster.project.name, namespace.namespace)["items"]
+ },
+ *knative_services_finder.cache_args)
+ end
+
+ include_examples 'GET #index with data'
end
- it 'has data in html' do
- get :index, params: params
+ context 'on Knative 0.6 or 0.7' do
+ before do
+ stub_kubeclient_service_pods
+ stub_reactive_cache(knative_services_finder,
+ {
+ services: kube_knative_services_body(namespace: namespace.namespace, name: cluster.project.name)["items"],
+ pods: kube_knative_pods_body(cluster.project.name, namespace.namespace)["items"]
+ },
+ *knative_services_finder.cache_args)
+ end
- expect(response).to have_gitlab_http_status(200)
+ include_examples 'GET #index with data'
end
end
end
diff --git a/spec/controllers/projects/settings/ci_cd_controller_spec.rb b/spec/controllers/projects/settings/ci_cd_controller_spec.rb
index 5bfbcf6eeb5..c67e7f7dadd 100644
--- a/spec/controllers/projects/settings/ci_cd_controller_spec.rb
+++ b/spec/controllers/projects/settings/ci_cd_controller_spec.rb
@@ -3,8 +3,8 @@
require('spec_helper')
describe Projects::Settings::CiCdController do
- set(:user) { create(:user) }
- set(:project_auto_devops) { create(:project_auto_devops) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project_auto_devops) { create(:project_auto_devops) }
let(:project) { project_auto_devops.project }
before do
@@ -78,6 +78,7 @@ describe Projects::Settings::CiCdController do
describe 'PUT #reset_registration_token' do
subject { put :reset_registration_token, params: { namespace_id: project.namespace, project_id: project } }
+
it 'resets runner registration token' do
expect { subject }.to change { project.reload.runners_token }
end
@@ -215,6 +216,30 @@ describe Projects::Settings::CiCdController do
expect(project.ci_default_git_depth).to eq(10)
end
end
+
+ context 'when max_artifacts_size is specified' do
+ let(:params) { { max_artifacts_size: 10 } }
+
+ context 'and user is not an admin' do
+ it 'does not set max_artifacts_size' do
+ subject
+
+ project.reload
+ expect(project.max_artifacts_size).to be_nil
+ end
+ end
+
+ context 'and user is an admin' do
+ let(:user) { create(:admin) }
+
+ it 'sets max_artifacts_size' do
+ subject
+
+ project.reload
+ expect(project.max_artifacts_size).to eq(10)
+ end
+ end
+ end
end
end
end
diff --git a/spec/controllers/projects/settings/operations_controller_spec.rb b/spec/controllers/projects/settings/operations_controller_spec.rb
index aa9cd41ed19..0b34656e9e2 100644
--- a/spec/controllers/projects/settings/operations_controller_spec.rb
+++ b/spec/controllers/projects/settings/operations_controller_spec.rb
@@ -3,8 +3,8 @@
require 'spec_helper'
describe Projects::Settings::OperationsController do
- set(:user) { create(:user) }
- set(:project) { create(:project) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project) }
before do
sign_in(user)
@@ -180,6 +180,21 @@ describe Projects::Settings::OperationsController do
end
end
+ context 'grafana integration' do
+ describe 'PATCH #update' do
+ let(:params) do
+ {
+ grafana_integration_attributes: {
+ grafana_url: 'https://grafana.gitlab.com',
+ token: 'eyJrIjoicDRlRTREdjhhOEZ5WjZPWXUzazJOSW0zZHJUejVOd3IiLCJuIjoiVGVzdCBLZXkiLCJpZCI6MX0='
+ }
+ }
+ end
+
+ it_behaves_like 'PATCHable'
+ end
+ end
+
private
def project_params(project, params = {})
diff --git a/spec/controllers/projects/snippets_controller_spec.rb b/spec/controllers/projects/snippets_controller_spec.rb
index b13534b9088..042a5542786 100644
--- a/spec/controllers/projects/snippets_controller_spec.rb
+++ b/spec/controllers/projects/snippets_controller_spec.rb
@@ -111,7 +111,7 @@ describe Projects::SnippetsController do
it 'creates a spam log' do
expect { create_snippet(project, visibility_level: Snippet::PUBLIC) }
- .to change { SpamLog.count }.by(1)
+ .to log_spam(title: 'Title', user_id: user.id, noteable_type: 'ProjectSnippet')
end
it 'renders :new with recaptcha disabled' do
@@ -192,7 +192,7 @@ describe Projects::SnippetsController do
it 'creates a spam log' do
expect { update_snippet(title: 'Foo') }
- .to change { SpamLog.count }.by(1)
+ .to log_spam(title: 'Foo', user_id: user.id, noteable_type: 'ProjectSnippet')
end
it 'renders :edit with recaptcha disabled' do
@@ -237,7 +237,7 @@ describe Projects::SnippetsController do
it 'creates a spam log' do
expect { update_snippet(title: 'Foo', visibility_level: Snippet::PUBLIC) }
- .to change { SpamLog.count }.by(1)
+ .to log_spam(title: 'Foo', user_id: user.id, noteable_type: 'ProjectSnippet')
end
it 'renders :edit with recaptcha disabled' do
diff --git a/spec/controllers/projects/templates_controller_spec.rb b/spec/controllers/projects/templates_controller_spec.rb
index d5ef2b0e114..07b8a36fefc 100644
--- a/spec/controllers/projects/templates_controller_spec.rb
+++ b/spec/controllers/projects/templates_controller_spec.rb
@@ -99,4 +99,44 @@ describe Projects::TemplatesController do
include_examples 'renders 404 when params are invalid'
end
end
+
+ describe '#names' do
+ before do
+ project.add_developer(user)
+ sign_in(user)
+ end
+
+ shared_examples 'template names request' do
+ it 'returns the template names' do
+ get(:names, params: { namespace_id: project.namespace, template_type: template_type, project_id: project }, format: :json)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response.size).to eq(1)
+ expect(json_response[0]['name']).to eq(expected_template_name)
+ end
+
+ it 'fails for user with no access' do
+ other_user = create(:user)
+ sign_in(other_user)
+
+ get(:names, params: { namespace_id: project.namespace, template_type: template_type, project_id: project }, format: :json)
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+
+ context 'when querying for issue templates' do
+ it_behaves_like 'template names request' do
+ let(:template_type) { 'issue' }
+ let(:expected_template_name) { 'issue_template' }
+ end
+ end
+
+ context 'when querying for merge_request templates' do
+ it_behaves_like 'template names request' do
+ let(:template_type) { 'merge_request' }
+ let(:expected_template_name) { 'merge_request_template' }
+ end
+ end
+ end
end
diff --git a/spec/controllers/projects/wikis_controller_spec.rb b/spec/controllers/projects/wikis_controller_spec.rb
index 6fea6bca4f2..f46da908218 100644
--- a/spec/controllers/projects/wikis_controller_spec.rb
+++ b/spec/controllers/projects/wikis_controller_spec.rb
@@ -3,8 +3,8 @@
require 'spec_helper'
describe Projects::WikisController do
- set(:project) { create(:project, :public, :repository) }
- set(:user) { project.owner }
+ let_it_be(:project) { create(:project, :public, :repository) }
+ let(:user) { project.owner }
let(:project_wiki) { ProjectWiki.new(project, user) }
let(:wiki) { project_wiki.wiki }
let(:wiki_title) { 'page title test' }
diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb
index c732caa6160..ea7dd78329a 100644
--- a/spec/controllers/projects_controller_spec.rb
+++ b/spec/controllers/projects_controller_spec.rb
@@ -149,7 +149,7 @@ describe ProjectsController do
end
context 'when the storage is not available', :broken_storage do
- set(:project) { create(:project, :broken_storage) }
+ let_it_be(:project) { create(:project, :broken_storage) }
before do
project.add_developer(user)
diff --git a/spec/controllers/registrations_controller_spec.rb b/spec/controllers/registrations_controller_spec.rb
index 5d87dbdee8b..ebeed94c274 100644
--- a/spec/controllers/registrations_controller_spec.rb
+++ b/spec/controllers/registrations_controller_spec.rb
@@ -114,9 +114,14 @@ describe RegistrationsController do
context 'when invisible captcha is enabled' do
before do
stub_feature_flags(invisible_captcha: true)
+ InvisibleCaptcha.timestamp_enabled = true
InvisibleCaptcha.timestamp_threshold = treshold
end
+ after do
+ InvisibleCaptcha.timestamp_enabled = false
+ end
+
let(:treshold) { 4 }
let(:session_params) { { invisible_captcha_timestamp: form_rendered_time.iso8601 } }
let(:form_rendered_time) { Time.current }
diff --git a/spec/controllers/search_controller_spec.rb b/spec/controllers/search_controller_spec.rb
index 3e0d53a6573..3dcafae295a 100644
--- a/spec/controllers/search_controller_spec.rb
+++ b/spec/controllers/search_controller_spec.rb
@@ -67,7 +67,7 @@ describe SearchController do
using RSpec::Parameterized::TableSyntax
render_views
- set(:project) { create(:project, :public, :repository, :wiki_repo) }
+ let_it_be(:project) { create(:project, :public, :repository, :wiki_repo) }
before do
expect(::Gitlab::GitalyClient).to receive(:allow_ref_name_caching).and_call_original
diff --git a/spec/controllers/sessions_controller_spec.rb b/spec/controllers/sessions_controller_spec.rb
index 68b7bf61231..2108cf1c8ae 100644
--- a/spec/controllers/sessions_controller_spec.rb
+++ b/spec/controllers/sessions_controller_spec.rb
@@ -61,6 +61,25 @@ describe SessionsController do
expect(subject.current_user).to eq user
end
+ context 'a deactivated user' do
+ before do
+ user.deactivate!
+ post(:create, params: { user: user_params })
+ end
+
+ it 'is allowed to login' do
+ expect(subject.current_user).to eq user
+ end
+
+ it 'activates the user' do
+ expect(subject.current_user.active?).to be_truthy
+ end
+
+ it 'shows reactivation flash message after logging in' do
+ expect(flash[:notice]).to eq('Welcome back! Your account had been deactivated due to inactivity but is now reactivated.')
+ end
+ end
+
context 'with password authentication disabled' do
before do
stub_application_setting(password_authentication_enabled_for_web: false)
diff --git a/spec/controllers/snippets_controller_spec.rb b/spec/controllers/snippets_controller_spec.rb
index 1b3a8965342..e892c736c69 100644
--- a/spec/controllers/snippets_controller_spec.rb
+++ b/spec/controllers/snippets_controller_spec.rb
@@ -269,7 +269,7 @@ describe SnippetsController do
it 'creates a spam log' do
expect { create_snippet(visibility_level: Snippet::PUBLIC) }
- .to change { SpamLog.count }.by(1)
+ .to log_spam(title: 'Title', user: user, noteable_type: 'PersonalSnippet')
end
it 'renders :new with recaptcha disabled' do
@@ -345,7 +345,7 @@ describe SnippetsController do
it 'creates a spam log' do
expect { update_snippet(title: 'Foo', visibility_level: Snippet::PUBLIC) }
- .to change { SpamLog.count }.by(1)
+ .to log_spam(title: 'Foo', user: user, noteable_type: 'PersonalSnippet')
end
it 'renders :edit with recaptcha disabled' do
@@ -389,8 +389,8 @@ describe SnippetsController do
end
it 'creates a spam log' do
- expect { update_snippet(title: 'Foo') }
- .to change { SpamLog.count }.by(1)
+ expect {update_snippet(title: 'Foo') }
+ .to log_spam(title: 'Foo', user: user, noteable_type: 'PersonalSnippet')
end
it 'renders :edit with recaptcha disabled' do
diff --git a/spec/controllers/uploads_controller_spec.rb b/spec/controllers/uploads_controller_spec.rb
index 5f4a6bf8ee7..dd7ab4f9d47 100644
--- a/spec/controllers/uploads_controller_spec.rb
+++ b/spec/controllers/uploads_controller_spec.rb
@@ -1,16 +1,15 @@
# frozen_string_literal: true
require 'spec_helper'
-shared_examples 'content not cached without revalidation' do
+shared_examples 'content 5 min private cached with revalidation' do
it 'ensures content will not be cached without revalidation' do
- expect(subject['Cache-Control']).to eq('max-age=0, private, must-revalidate')
+ expect(subject['Cache-Control']).to eq('max-age=300, private, must-revalidate')
end
end
-shared_examples 'content not cached without revalidation and no-store' do
+shared_examples 'content long term private cached with revalidation' do
it 'ensures content will not be cached without revalidation' do
- # Fixed in newer versions of ActivePack, it will only output a single `private`.
- expect(subject['Cache-Control']).to eq('max-age=0, private, must-revalidate, no-store')
+ expect(subject['Cache-Control']).to eq('max-age=15778476, private, must-revalidate')
end
end
@@ -285,7 +284,7 @@ describe UploadsController do
expect(response).to have_gitlab_http_status(200)
end
- it_behaves_like 'content not cached without revalidation' do
+ it_behaves_like 'content 5 min private cached with revalidation' do
subject do
get :show, params: { model: 'project', mounted_as: 'avatar', id: project.id, filename: 'dk.png' }
@@ -305,7 +304,7 @@ describe UploadsController do
expect(response).to have_gitlab_http_status(200)
end
- it_behaves_like 'content not cached without revalidation and no-store' do
+ it_behaves_like 'content 5 min private cached with revalidation' do
subject do
get :show, params: { model: 'project', mounted_as: 'avatar', id: project.id, filename: 'dk.png' }
@@ -358,7 +357,7 @@ describe UploadsController do
expect(response).to have_gitlab_http_status(200)
end
- it_behaves_like 'content not cached without revalidation and no-store' do
+ it_behaves_like 'content 5 min private cached with revalidation' do
subject do
get :show, params: { model: 'project', mounted_as: 'avatar', id: project.id, filename: 'dk.png' }
@@ -390,7 +389,7 @@ describe UploadsController do
expect(response).to have_gitlab_http_status(200)
end
- it_behaves_like 'content not cached without revalidation' do
+ it_behaves_like 'content 5 min private cached with revalidation' do
subject do
get :show, params: { model: 'group', mounted_as: 'avatar', id: group.id, filename: 'dk.png' }
@@ -410,7 +409,7 @@ describe UploadsController do
expect(response).to have_gitlab_http_status(200)
end
- it_behaves_like 'content not cached without revalidation and no-store' do
+ it_behaves_like 'content 5 min private cached with revalidation' do
subject do
get :show, params: { model: 'group', mounted_as: 'avatar', id: group.id, filename: 'dk.png' }
@@ -454,7 +453,7 @@ describe UploadsController do
expect(response).to have_gitlab_http_status(200)
end
- it_behaves_like 'content not cached without revalidation and no-store' do
+ it_behaves_like 'content 5 min private cached with revalidation' do
subject do
get :show, params: { model: 'group', mounted_as: 'avatar', id: group.id, filename: 'dk.png' }
@@ -491,7 +490,7 @@ describe UploadsController do
expect(response).to have_gitlab_http_status(200)
end
- it_behaves_like 'content not cached without revalidation' do
+ it_behaves_like 'content long term private cached with revalidation' do
subject do
get :show, params: { model: 'note', mounted_as: 'attachment', id: note.id, filename: 'dk.png' }
@@ -511,7 +510,7 @@ describe UploadsController do
expect(response).to have_gitlab_http_status(200)
end
- it_behaves_like 'content not cached without revalidation and no-store' do
+ it_behaves_like 'content long term private cached with revalidation' do
subject do
get :show, params: { model: 'note', mounted_as: 'attachment', id: note.id, filename: 'dk.png' }
@@ -564,7 +563,7 @@ describe UploadsController do
expect(response).to have_gitlab_http_status(200)
end
- it_behaves_like 'content not cached without revalidation and no-store' do
+ it_behaves_like 'content long term private cached with revalidation' do
subject do
get :show, params: { model: 'note', mounted_as: 'attachment', id: note.id, filename: 'dk.png' }
diff --git a/spec/db/production/settings_spec.rb b/spec/db/production/settings_spec.rb
index db19e98b851..02e25aa37e3 100644
--- a/spec/db/production/settings_spec.rb
+++ b/spec/db/production/settings_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
require 'rainbow/ext/string'
diff --git a/spec/db/schema_spec.rb b/spec/db/schema_spec.rb
index 5216683bd36..53f4a261092 100644
--- a/spec/db/schema_spec.rb
+++ b/spec/db/schema_spec.rb
@@ -19,6 +19,7 @@ describe 'Database schema' do
approver_groups: %w[target_id],
audit_events: %w[author_id entity_id],
award_emoji: %w[awardable_id user_id],
+ aws_roles: %w[role_external_id],
boards: %w[milestone_id],
chat_names: %w[chat_id service_id team_id user_id],
chat_teams: %w[team_id],
@@ -26,6 +27,7 @@ describe 'Database schema' do
ci_pipelines: %w[user_id],
ci_runner_projects: %w[runner_id],
ci_trigger_requests: %w[commit_id],
+ cluster_providers_aws: %w[security_group_id vpc_id access_key_id],
cluster_providers_gcp: %w[gcp_project_id operation_id],
deploy_keys_projects: %w[deploy_key_id],
deployments: %w[deployable_id environment_id user_id],
diff --git a/spec/dependencies/omniauth_saml_spec.rb b/spec/dependencies/omniauth_saml_spec.rb
index ccc604dc230..8a685648c71 100644
--- a/spec/dependencies/omniauth_saml_spec.rb
+++ b/spec/dependencies/omniauth_saml_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
require 'omniauth/strategies/saml'
diff --git a/spec/factories/abuse_reports.rb b/spec/factories/abuse_reports.rb
index 578af9ed895..4174faae1ed 100644
--- a/spec/factories/abuse_reports.rb
+++ b/spec/factories/abuse_reports.rb
@@ -4,6 +4,6 @@ FactoryBot.define do
factory :abuse_report do
reporter factory: :user
user
- message 'User sends spam'
+ message { 'User sends spam' }
end
end
diff --git a/spec/factories/analytics/cycle_analytics/project_stages.rb b/spec/factories/analytics/cycle_analytics/project_stages.rb
new file mode 100644
index 00000000000..6f8c140ed8a
--- /dev/null
+++ b/spec/factories/analytics/cycle_analytics/project_stages.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :cycle_analytics_project_stage, class: Analytics::CycleAnalytics::ProjectStage do
+ project
+ sequence(:name) { |n| "Stage ##{n}" }
+ hidden { false }
+ issue_stage
+
+ trait :issue_stage do
+ start_event_identifier { Gitlab::Analytics::CycleAnalytics::StageEvents::IssueCreated.identifier }
+ end_event_identifier { Gitlab::Analytics::CycleAnalytics::StageEvents::IssueStageEnd.identifier }
+ end
+ end
+end
diff --git a/spec/factories/appearances.rb b/spec/factories/appearances.rb
index bdd5964fb93..e2922662ea4 100644
--- a/spec/factories/appearances.rb
+++ b/spec/factories/appearances.rb
@@ -4,9 +4,9 @@
FactoryBot.define do
factory :appearance do
- title "GitLab Community Edition"
- description "Open source software to collaborate on code"
- new_project_guidelines "Custom project guidelines"
+ title { "GitLab Community Edition" }
+ description { "Open source software to collaborate on code" }
+ new_project_guidelines { "Custom project guidelines" }
end
trait :with_logo do
diff --git a/spec/factories/application_settings.rb b/spec/factories/application_settings.rb
index 90b6b9e648a..d4571b9861d 100644
--- a/spec/factories/application_settings.rb
+++ b/spec/factories/application_settings.rb
@@ -2,6 +2,6 @@
FactoryBot.define do
factory :application_setting do
- default_projects_limit 42
+ default_projects_limit { 42 }
end
end
diff --git a/spec/factories/award_emoji.rb b/spec/factories/award_emoji.rb
index a8bb806381e..a430210ab49 100644
--- a/spec/factories/award_emoji.rb
+++ b/spec/factories/award_emoji.rb
@@ -2,7 +2,7 @@
FactoryBot.define do
factory :award_emoji do
- name "thumbsup"
+ name { "thumbsup" }
user
awardable factory: :issue
@@ -12,7 +12,7 @@ FactoryBot.define do
trait :upvote
trait :downvote do
- name "thumbsdown"
+ name { "thumbsdown" }
end
end
end
diff --git a/spec/factories/aws/roles.rb b/spec/factories/aws/roles.rb
new file mode 100644
index 00000000000..c078033dfad
--- /dev/null
+++ b/spec/factories/aws/roles.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :aws_role, class: Aws::Role do
+ user
+
+ role_arn { 'arn:aws:iam::123456789012:role/role-name' }
+ sequence(:role_external_id) { |n| "external-id-#{n}" }
+ end
+end
diff --git a/spec/factories/boards.rb b/spec/factories/boards.rb
index a5aff5c7504..a201ca94380 100644
--- a/spec/factories/boards.rb
+++ b/spec/factories/boards.rb
@@ -3,11 +3,11 @@
FactoryBot.define do
factory :board do
transient do
- project nil
- group nil
- project_id nil
- group_id nil
- parent nil
+ project { nil }
+ group { nil }
+ project_id { nil }
+ group_id { nil }
+ resource_parent { nil }
end
after(:build, :stub) do |board, evaluator|
@@ -19,9 +19,9 @@ FactoryBot.define do
board.project = evaluator.project
elsif evaluator.project_id
board.project_id = evaluator.project_id
- elsif evaluator.parent
- id = evaluator.parent.id
- evaluator.parent.is_a?(Group) ? board.group_id = id : evaluator.project_id = id
+ elsif evaluator.resource_parent
+ id = evaluator.resource_parent.id
+ evaluator.resource_parent.is_a?(Group) ? board.group_id = id : evaluator.project_id = id
else
board.project = create(:project, :empty_repo)
end
diff --git a/spec/factories/broadcast_messages.rb b/spec/factories/broadcast_messages.rb
index 2a30e2034b1..ed6e267e7c4 100644
--- a/spec/factories/broadcast_messages.rb
+++ b/spec/factories/broadcast_messages.rb
@@ -2,7 +2,7 @@
FactoryBot.define do
factory :broadcast_message do
- message "MyText"
+ message { "MyText" }
starts_at { 1.day.ago }
ends_at { 1.day.from_now }
diff --git a/spec/factories/chat_names.rb b/spec/factories/chat_names.rb
index 07bf990162f..ace5d5e83c9 100644
--- a/spec/factories/chat_names.rb
+++ b/spec/factories/chat_names.rb
@@ -5,8 +5,8 @@ FactoryBot.define do
user factory: :user
service factory: :service
- team_id 'T0001'
- team_domain 'Awesome Team'
+ team_id { 'T0001' }
+ team_domain { 'Awesome Team' }
sequence(:chat_id) { |n| "U#{n}" }
chat_name { generate(:username) }
diff --git a/spec/factories/ci/bridge.rb b/spec/factories/ci/bridge.rb
index b1b714277e4..60219b07cf0 100644
--- a/spec/factories/ci/bridge.rb
+++ b/spec/factories/ci/bridge.rb
@@ -2,22 +2,26 @@
FactoryBot.define do
factory :ci_bridge, class: Ci::Bridge do
- name 'bridge'
- stage 'test'
- stage_idx 0
- ref 'master'
- tag false
- created_at 'Di 29. Okt 09:50:00 CET 2013'
- status :created
+ name { 'bridge' }
+ stage { 'test' }
+ stage_idx { 0 }
+ ref { 'master' }
+ tag { false }
+ created_at { 'Di 29. Okt 09:50:00 CET 2013' }
+ status { :created }
pipeline factory: :ci_pipeline
trait :variables do
- yaml_variables [{ key: 'BRIDGE', value: 'cross', public: true }]
+ yaml_variables do
+ [{ key: 'BRIDGE', value: 'cross', public: true }]
+ end
end
- transient { downstream nil }
- transient { upstream nil }
+ transient do
+ downstream { nil }
+ upstream { nil }
+ end
after(:build) do |bridge, evaluator|
bridge.project ||= bridge.pipeline.project
diff --git a/spec/factories/ci/build_trace_chunks.rb b/spec/factories/ci/build_trace_chunks.rb
index 492dc47f083..22f091f8e76 100644
--- a/spec/factories/ci/build_trace_chunks.rb
+++ b/spec/factories/ci/build_trace_chunks.rb
@@ -3,14 +3,14 @@
FactoryBot.define do
factory :ci_build_trace_chunk, class: Ci::BuildTraceChunk do
build factory: :ci_build
- chunk_index 0
- data_store :redis
+ chunk_index { 0 }
+ data_store { :redis }
trait :redis_with_data do
- data_store :redis
+ data_store { :redis }
transient do
- initial_data 'test data'
+ initial_data { 'test data' }
end
after(:create) do |build_trace_chunk, evaluator|
@@ -19,14 +19,14 @@ FactoryBot.define do
end
trait :redis_without_data do
- data_store :redis
+ data_store { :redis }
end
trait :database_with_data do
- data_store :database
+ data_store { :database}
transient do
- initial_data 'test data'
+ initial_data { 'test data' }
end
after(:build) do |build_trace_chunk, evaluator|
@@ -35,14 +35,14 @@ FactoryBot.define do
end
trait :database_without_data do
- data_store :database
+ data_store { :database }
end
trait :fog_with_data do
- data_store :fog
+ data_store { :fog }
transient do
- initial_data 'test data'
+ initial_data { 'test data' }
end
after(:create) do |build_trace_chunk, evaluator|
@@ -51,7 +51,7 @@ FactoryBot.define do
end
trait :fog_without_data do
- data_store :fog
+ data_store { :fog }
end
end
end
diff --git a/spec/factories/ci/builds.rb b/spec/factories/ci/builds.rb
index e3b7c64176a..0bd39d4cdcf 100644
--- a/spec/factories/ci/builds.rb
+++ b/spec/factories/ci/builds.rb
@@ -4,13 +4,13 @@ include ActionDispatch::TestProcess
FactoryBot.define do
factory :ci_build, class: Ci::Build do
- name 'test'
- stage 'test'
- stage_idx 0
- ref 'master'
- tag false
- protected false
- created_at 'Di 29. Okt 09:50:00 CET 2013'
+ name { 'test' }
+ stage { 'test' }
+ stage_idx { 0 }
+ ref { 'master' }
+ tag { false }
+ add_attribute(:protected) { false }
+ created_at { 'Di 29. Okt 09:50:00 CET 2013' }
pending
options do
@@ -30,127 +30,127 @@ FactoryBot.define do
pipeline factory: :ci_pipeline
trait :degenerated do
- options nil
- yaml_variables nil
+ options { nil }
+ yaml_variables { nil }
end
trait :started do
- started_at 'Di 29. Okt 09:51:28 CET 2013'
+ started_at { 'Di 29. Okt 09:51:28 CET 2013' }
end
trait :finished do
started
- finished_at 'Di 29. Okt 09:53:28 CET 2013'
+ finished_at { 'Di 29. Okt 09:53:28 CET 2013' }
end
trait :success do
finished
- status 'success'
+ status { 'success' }
end
trait :failed do
finished
- status 'failed'
+ status { 'failed' }
end
trait :canceled do
finished
- status 'canceled'
+ status { 'canceled' }
end
trait :skipped do
started
- status 'skipped'
+ status { 'skipped' }
end
trait :running do
started
- status 'running'
+ status { 'running' }
end
trait :pending do
- queued_at 'Di 29. Okt 09:50:59 CET 2013'
- status 'pending'
+ queued_at { 'Di 29. Okt 09:50:59 CET 2013' }
+ status { 'pending' }
end
trait :created do
- status 'created'
+ status { 'created' }
end
trait :preparing do
- status 'preparing'
+ status { 'preparing' }
end
trait :scheduled do
schedulable
- status 'scheduled'
+ status { 'scheduled' }
scheduled_at { 1.minute.since }
end
trait :expired_scheduled do
schedulable
- status 'scheduled'
+ status { 'scheduled' }
scheduled_at { 1.minute.ago }
end
trait :manual do
- status 'manual'
- self.when 'manual'
+ status { 'manual' }
+ self.when { 'manual' }
end
trait :teardown_environment do
- environment 'staging'
+ environment { 'staging' }
options do
{
script: %w(ls),
environment: { name: 'staging',
- action: 'stop',
- url: 'http://staging.example.com/$CI_JOB_NAME' }
+ action: 'stop',
+ url: 'http://staging.example.com/$CI_JOB_NAME' }
}
end
end
trait :deploy_to_production do
- environment 'production'
+ environment { 'production' }
options do
{
script: %w(ls),
environment: { name: 'production',
- url: 'http://prd.example.com/$CI_JOB_NAME' }
+ url: 'http://prd.example.com/$CI_JOB_NAME' }
}
end
end
trait :start_review_app do
- environment 'review/$CI_COMMIT_REF_NAME'
+ environment { 'review/$CI_COMMIT_REF_NAME' }
options do
{
script: %w(ls),
environment: { name: 'review/$CI_COMMIT_REF_NAME',
- url: 'http://staging.example.com/$CI_JOB_NAME',
- on_stop: 'stop_review_app' }
+ url: 'http://staging.example.com/$CI_JOB_NAME',
+ on_stop: 'stop_review_app' }
}
end
end
trait :stop_review_app do
- name 'stop_review_app'
- environment 'review/$CI_COMMIT_REF_NAME'
+ name { 'stop_review_app' }
+ environment { 'review/$CI_COMMIT_REF_NAME' }
options do
{
script: %w(ls),
environment: { name: 'review/$CI_COMMIT_REF_NAME',
- url: 'http://staging.example.com/$CI_JOB_NAME',
- action: 'stop' }
+ url: 'http://staging.example.com/$CI_JOB_NAME',
+ action: 'stop' }
}
end
end
trait :allowed_to_fail do
- allow_failure true
+ allow_failure { true }
end
trait :ignored do
@@ -166,7 +166,7 @@ FactoryBot.define do
end
trait :schedulable do
- self.when 'delayed'
+ self.when { 'delayed' }
options do
{
@@ -177,11 +177,11 @@ FactoryBot.define do
end
trait :actionable do
- self.when 'manual'
+ self.when { 'manual' }
end
trait :retried do
- retried true
+ retried { true }
end
trait :cancelable do
@@ -194,11 +194,13 @@ FactoryBot.define do
end
trait :tags do
- tag_list [:docker, :ruby]
+ tag_list do
+ [:docker, :ruby]
+ end
end
trait :on_tag do
- tag true
+ tag { true }
end
trait :triggered do
@@ -209,13 +211,24 @@ FactoryBot.define do
build.project ||= build.pipeline.project
end
+ trait :with_deployment do
+ after(:build) do |build, evaluator|
+ ##
+ # Build deployment/environment relations if environment name is set
+ # to the job. If `build.deployment` has already been set, it doesn't
+ # build a new instance.
+ build.deployment =
+ Gitlab::Ci::Pipeline::Seed::Deployment.new(build).to_resource
+ end
+ end
+
trait :tag do
- tag true
+ tag { true }
end
trait :coverage do
- coverage 99.9
- coverage_regex '/(d+)/'
+ coverage { 99.9 }
+ coverage_regex { '/(d+)/' }
end
trait :trace_live do
@@ -303,23 +316,23 @@ FactoryBot.define do
trait :extended_options do
options do
{
- image: { name: 'ruby:2.1', entrypoint: '/bin/sh' },
- services: ['postgres', { name: 'docker:stable-dind', entrypoint: '/bin/sh', command: 'sleep 30', alias: 'docker' }],
- script: %w(echo),
- after_script: %w(ls date),
- artifacts: {
- name: 'artifacts_file',
- untracked: false,
- paths: ['out/'],
- when: 'always',
- expire_in: '7d'
- },
- cache: {
- key: 'cache_key',
- untracked: false,
- paths: ['vendor/*'],
- policy: 'pull-push'
- }
+ image: { name: 'ruby:2.1', entrypoint: '/bin/sh' },
+ services: ['postgres', { name: 'docker:stable-dind', entrypoint: '/bin/sh', command: 'sleep 30', alias: 'docker' }],
+ script: %w(echo),
+ after_script: %w(ls date),
+ artifacts: {
+ name: 'artifacts_file',
+ untracked: false,
+ paths: ['out/'],
+ when: 'always',
+ expire_in: '7d'
+ },
+ cache: {
+ key: 'cache_key',
+ untracked: false,
+ paths: ['vendor/*'],
+ policy: 'pull-push'
+ }
}
end
end
@@ -328,28 +341,60 @@ FactoryBot.define do
options { {} }
end
+ trait :dast do
+ options do
+ {
+ artifacts: { reports: { dast: 'gl-dast-report.json' } }
+ }
+ end
+ end
+
+ trait :sast do
+ options do
+ {
+ artifacts: { reports: { sast: 'gl-sast-report.json' } }
+ }
+ end
+ end
+
+ trait :dependency_scanning do
+ options do
+ {
+ artifacts: { reports: { dependency_scanning: 'gl-dependency-scanning-report.json' } }
+ }
+ end
+ end
+
+ trait :container_scanning do
+ options do
+ {
+ artifacts: { reports: { container_scanning: 'gl-container-scanning-report.json' } }
+ }
+ end
+ end
+
trait :non_playable do
- status 'created'
- self.when 'manual'
+ status { 'created' }
+ self.when { 'manual' }
end
trait :protected do
- protected true
+ add_attribute(:protected) { true }
end
trait :script_failure do
failed
- failure_reason 1
+ failure_reason { 1 }
end
trait :api_failure do
failed
- failure_reason 2
+ failure_reason { 2 }
end
trait :prerequisite_failure do
failed
- failure_reason 10
+ failure_reason { 10 }
end
trait :with_runner_session do
diff --git a/spec/factories/ci/group_variables.rb b/spec/factories/ci/group_variables.rb
index 13c2b78e61b..217f05a088e 100644
--- a/spec/factories/ci/group_variables.rb
+++ b/spec/factories/ci/group_variables.rb
@@ -3,11 +3,11 @@
FactoryBot.define do
factory :ci_group_variable, class: Ci::GroupVariable do
sequence(:key) { |n| "VARIABLE_#{n}" }
- value 'VARIABLE_VALUE'
- masked false
+ value { 'VARIABLE_VALUE' }
+ masked { false }
trait(:protected) do
- protected true
+ add_attribute(:protected) { true }
end
group factory: :group
diff --git a/spec/factories/ci/job_artifacts.rb b/spec/factories/ci/job_artifacts.rb
index 6f553cadfa3..bdc6cc2f169 100644
--- a/spec/factories/ci/job_artifacts.rb
+++ b/spec/factories/ci/job_artifacts.rb
@@ -5,15 +5,15 @@ include ActionDispatch::TestProcess
FactoryBot.define do
factory :ci_job_artifact, class: Ci::JobArtifact do
job factory: :ci_build
- file_type :archive
- file_format :zip
+ file_type { :archive }
+ file_format { :zip }
trait :expired do
expire_at { Date.yesterday }
end
trait :remote_store do
- file_store JobArtifactUploader::Store::REMOTE
+ file_store { JobArtifactUploader::Store::REMOTE}
end
after :build do |artifact|
@@ -21,7 +21,7 @@ FactoryBot.define do
end
trait :raw do
- file_format :raw
+ file_format { :raw }
after(:build) do |artifact, _|
artifact.file = fixture_file_upload(
@@ -30,7 +30,7 @@ FactoryBot.define do
end
trait :zip do
- file_format :zip
+ file_format { :zip }
after(:build) do |artifact, _|
artifact.file = fixture_file_upload(
@@ -39,7 +39,7 @@ FactoryBot.define do
end
trait :gzip do
- file_format :gzip
+ file_format { :gzip }
after(:build) do |artifact, _|
artifact.file = fixture_file_upload(
@@ -48,8 +48,8 @@ FactoryBot.define do
end
trait :archive do
- file_type :archive
- file_format :zip
+ file_type { :archive }
+ file_format { :zip }
transient do
file { fixture_file_upload(Rails.root.join('spec/fixtures/ci_build_artifacts.zip'), 'application/zip') }
@@ -63,12 +63,12 @@ FactoryBot.define do
trait :legacy_archive do
archive
- file_location :legacy_path
+ file_location { :legacy_path }
end
trait :metadata do
- file_type :metadata
- file_format :gzip
+ file_type { :metadata }
+ file_format { :gzip }
transient do
file { fixture_file_upload(Rails.root.join('spec/fixtures/ci_build_artifacts_metadata.gz'), 'application/x-gzip') }
@@ -80,8 +80,8 @@ FactoryBot.define do
end
trait :trace do
- file_type :trace
- file_format :raw
+ file_type { :trace }
+ file_format { :raw }
after(:build) do |artifact, evaluator|
artifact.file = fixture_file_upload(
@@ -90,8 +90,8 @@ FactoryBot.define do
end
trait :junit do
- file_type :junit
- file_format :gzip
+ file_type { :junit }
+ file_format { :gzip }
after(:build) do |artifact, evaluator|
artifact.file = fixture_file_upload(
@@ -100,8 +100,8 @@ FactoryBot.define do
end
trait :junit_with_ant do
- file_type :junit
- file_format :gzip
+ file_type { :junit }
+ file_format { :gzip }
after(:build) do |artifact, evaluator|
artifact.file = fixture_file_upload(
@@ -110,8 +110,8 @@ FactoryBot.define do
end
trait :junit_with_three_testsuites do
- file_type :junit
- file_format :gzip
+ file_type { :junit }
+ file_format { :gzip }
after(:build) do |artifact, evaluator|
artifact.file = fixture_file_upload(
@@ -120,8 +120,8 @@ FactoryBot.define do
end
trait :junit_with_corrupted_data do
- file_type :junit
- file_format :gzip
+ file_type { :junit }
+ file_format { :gzip }
after(:build) do |artifact, evaluator|
artifact.file = fixture_file_upload(
@@ -130,8 +130,8 @@ FactoryBot.define do
end
trait :codequality do
- file_type :codequality
- file_format :raw
+ file_type { :codequality }
+ file_format { :raw }
after(:build) do |artifact, evaluator|
artifact.file = fixture_file_upload(
diff --git a/spec/factories/ci/job_variables.rb b/spec/factories/ci/job_variables.rb
index d664b763abd..bfc631b8126 100644
--- a/spec/factories/ci/job_variables.rb
+++ b/spec/factories/ci/job_variables.rb
@@ -3,7 +3,7 @@
FactoryBot.define do
factory :ci_job_variable, class: Ci::JobVariable do
sequence(:key) { |n| "VARIABLE_#{n}" }
- value 'VARIABLE_VALUE'
+ value { 'VARIABLE_VALUE' }
job factory: :ci_build
end
diff --git a/spec/factories/ci/pipeline_schedule.rb b/spec/factories/ci/pipeline_schedule.rb
index 8fae6986869..c752dc1c9dd 100644
--- a/spec/factories/ci/pipeline_schedule.rb
+++ b/spec/factories/ci/pipeline_schedule.rb
@@ -2,40 +2,40 @@
FactoryBot.define do
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
- description "pipeline schedule"
+ cron { '0 1 * * *' }
+ cron_timezone { Gitlab::Ci::CronParser::VALID_SYNTAX_SAMPLE_TIME_ZONE }
+ ref { 'master' }
+ active { true }
+ description { "pipeline schedule" }
project
trait :every_minute do
- cron '*/1 * * * *'
- cron_timezone Gitlab::Ci::CronParser::VALID_SYNTAX_SAMPLE_TIME_ZONE
+ cron { '*/1 * * * *' }
+ cron_timezone { Gitlab::Ci::CronParser::VALID_SYNTAX_SAMPLE_TIME_ZONE }
end
trait :hourly do
- cron '* */1 * * *'
- cron_timezone Gitlab::Ci::CronParser::VALID_SYNTAX_SAMPLE_TIME_ZONE
+ cron { '* */1 * * *' }
+ cron_timezone { Gitlab::Ci::CronParser::VALID_SYNTAX_SAMPLE_TIME_ZONE }
end
trait :nightly do
- cron '0 1 * * *'
- cron_timezone Gitlab::Ci::CronParser::VALID_SYNTAX_SAMPLE_TIME_ZONE
+ cron { '0 1 * * *' }
+ cron_timezone { Gitlab::Ci::CronParser::VALID_SYNTAX_SAMPLE_TIME_ZONE }
end
trait :weekly do
- cron '0 1 * * 6'
- cron_timezone Gitlab::Ci::CronParser::VALID_SYNTAX_SAMPLE_TIME_ZONE
+ cron { '0 1 * * 6' }
+ cron_timezone { Gitlab::Ci::CronParser::VALID_SYNTAX_SAMPLE_TIME_ZONE }
end
trait :monthly do
- cron '0 1 22 * *'
- cron_timezone Gitlab::Ci::CronParser::VALID_SYNTAX_SAMPLE_TIME_ZONE
+ cron { '0 1 22 * *' }
+ cron_timezone { Gitlab::Ci::CronParser::VALID_SYNTAX_SAMPLE_TIME_ZONE }
end
trait :inactive do
- active false
+ active { false }
end
end
end
diff --git a/spec/factories/ci/pipeline_schedule_variables.rb b/spec/factories/ci/pipeline_schedule_variables.rb
index fd7cfada65b..24913c614f4 100644
--- a/spec/factories/ci/pipeline_schedule_variables.rb
+++ b/spec/factories/ci/pipeline_schedule_variables.rb
@@ -3,8 +3,8 @@
FactoryBot.define do
factory :ci_pipeline_schedule_variable, class: Ci::PipelineScheduleVariable do
sequence(:key) { |n| "VARIABLE_#{n}" }
- value 'VARIABLE_VALUE'
- variable_type 'env_var'
+ value { 'VARIABLE_VALUE' }
+ variable_type { 'env_var' }
pipeline_schedule factory: :ci_pipeline_schedule
end
diff --git a/spec/factories/ci/pipeline_variables.rb b/spec/factories/ci/pipeline_variables.rb
index af0982124d7..48f6e35fe70 100644
--- a/spec/factories/ci/pipeline_variables.rb
+++ b/spec/factories/ci/pipeline_variables.rb
@@ -3,7 +3,7 @@
FactoryBot.define do
factory :ci_pipeline_variable, class: Ci::PipelineVariable do
sequence(:key) { |n| "VARIABLE_#{n}" }
- value 'VARIABLE_VALUE'
+ value { 'VARIABLE_VALUE' }
pipeline factory: :ci_empty_pipeline
end
diff --git a/spec/factories/ci/pipelines.rb b/spec/factories/ci/pipelines.rb
index 9652b0000a9..fefd89728e6 100644
--- a/spec/factories/ci/pipelines.rb
+++ b/spec/factories/ci/pipelines.rb
@@ -2,11 +2,11 @@
FactoryBot.define do
factory :ci_empty_pipeline, class: Ci::Pipeline do
- source :push
- ref 'master'
- sha '97de212e80737a608d939f648d959671fb0a0142'
- status 'pending'
- protected false
+ source { :push }
+ ref { 'master' }
+ sha { '97de212e80737a608d939f648d959671fb0a0142' }
+ status { 'pending' }
+ add_attribute(:protected) { false }
project
@@ -26,7 +26,7 @@ FactoryBot.define do
# Persist merge request head_pipeline_id
# on pipeline factories to avoid circular references
- transient { head_pipeline_of nil }
+ transient { head_pipeline_of { nil } }
after(:create) do |pipeline, evaluator|
merge_request = evaluator.head_pipeline_of
@@ -34,7 +34,7 @@ FactoryBot.define do
end
factory :ci_pipeline do
- transient { config nil }
+ transient { config { nil } }
after(:build) do |pipeline, evaluator|
if evaluator.config
@@ -48,44 +48,47 @@ FactoryBot.define do
end
trait :invalid do
- config(rspec: nil)
- failure_reason :config_error
+ config do
+ { rspec: nil }
+ end
+
+ failure_reason { :config_error }
end
trait :created do
- status :created
+ status { :created }
end
trait :preparing do
- status :preparing
+ status { :preparing }
end
trait :blocked do
- status :manual
+ status { :manual }
end
trait :scheduled do
- status :scheduled
+ status { :scheduled }
end
trait :success do
- status :success
+ status { :success }
end
trait :running do
- status :running
+ status { :running }
end
trait :failed do
- status :failed
+ status { :failed }
end
trait :protected do
- protected true
+ add_attribute(:protected) { true }
end
trait :with_test_reports do
- status :success
+ status { :success }
after(:build) do |pipeline, evaluator|
pipeline.builds << build(:ci_build, :test_reports, pipeline: pipeline, project: pipeline.project)
diff --git a/spec/factories/ci/runners.rb b/spec/factories/ci/runners.rb
index 1e4344b814d..3697970721b 100644
--- a/spec/factories/ci/runners.rb
+++ b/spec/factories/ci/runners.rb
@@ -4,22 +4,22 @@ FactoryBot.define do
factory :ci_runner, class: Ci::Runner do
sequence(:description) { |n| "My runner#{n}" }
- platform "darwin"
- active true
- access_level :not_protected
+ platform { "darwin" }
+ active { true }
+ access_level { :not_protected }
- runner_type :instance_type
+ runner_type { :instance_type }
trait :online do
contacted_at { Time.now }
end
trait :instance do
- runner_type :instance_type
+ runner_type { :instance_type }
end
trait :group do
- runner_type :group_type
+ runner_type { :group_type }
after(:build) do |runner, evaluator|
runner.groups << build(:group) if runner.groups.empty?
@@ -27,7 +27,7 @@ FactoryBot.define do
end
trait :project do
- runner_type :project_type
+ runner_type { :project_type }
after(:build) do |runner, evaluator|
runner.projects << build(:project) if runner.projects.empty?
@@ -43,21 +43,21 @@ FactoryBot.define do
end
trait :inactive do
- active false
+ active { false }
end
trait :ref_protected do
- access_level :ref_protected
+ access_level { :ref_protected }
end
trait :tagged_only do
- run_untagged false
+ run_untagged { false }
- tag_list %w(tag1 tag2)
+ tag_list { %w(tag1 tag2) }
end
trait :locked do
- locked true
+ locked { true }
end
end
end
diff --git a/spec/factories/ci/sources/pipelines.rb b/spec/factories/ci/sources/pipelines.rb
new file mode 100644
index 00000000000..57495502944
--- /dev/null
+++ b/spec/factories/ci/sources/pipelines.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :ci_sources_pipeline, class: Ci::Sources::Pipeline do
+ after(:build) do |source|
+ source.project ||= source.pipeline.project
+ source.source_pipeline ||= source.source_job.pipeline
+ source.source_project ||= source.source_pipeline.project
+ end
+
+ source_job factory: :ci_build
+
+ pipeline factory: :ci_empty_pipeline
+ end
+end
diff --git a/spec/factories/ci/stages.rb b/spec/factories/ci/stages.rb
index 88ff8d7dc53..67f4db41d96 100644
--- a/spec/factories/ci/stages.rb
+++ b/spec/factories/ci/stages.rb
@@ -5,16 +5,16 @@ FactoryBot.define do
skip_create
transient do
- name 'test'
- status nil
- warnings nil
+ name { 'test' }
+ status { nil }
+ warnings { nil }
pipeline factory: :ci_empty_pipeline
end
initialize_with do
Ci::LegacyStage.new(pipeline, name: name,
- status: status,
- warnings: warnings)
+ status: status,
+ warnings: warnings)
end
end
@@ -22,8 +22,8 @@ FactoryBot.define do
project factory: :project
pipeline factory: :ci_empty_pipeline
- name 'test'
- position 1
- status 'pending'
+ name { 'test' }
+ position { 1 }
+ status { 'pending' }
end
end
diff --git a/spec/factories/ci/variables.rb b/spec/factories/ci/variables.rb
index 55d11085040..9d2501c4e18 100644
--- a/spec/factories/ci/variables.rb
+++ b/spec/factories/ci/variables.rb
@@ -3,11 +3,11 @@
FactoryBot.define do
factory :ci_variable, class: Ci::Variable do
sequence(:key) { |n| "VARIABLE_#{n}" }
- value 'VARIABLE_VALUE'
- masked false
+ value { 'VARIABLE_VALUE' }
+ masked { false }
trait(:protected) do
- protected true
+ add_attribute(:protected) { true }
end
project
diff --git a/spec/factories/clusters/applications/helm.rb b/spec/factories/clusters/applications/helm.rb
index 89f7bc15217..c7ec7c11743 100644
--- a/spec/factories/clusters/applications/helm.rb
+++ b/spec/factories/clusters/applications/helm.rb
@@ -19,50 +19,50 @@ FactoryBot.define do
end
trait :not_installable do
- status(-2)
+ status { -2 }
end
trait :errored do
- status(-1)
- status_reason 'something went wrong'
+ status { -1 }
+ status_reason { 'something went wrong' }
end
trait :installable do
- status 0
+ status { 0 }
end
trait :scheduled do
- status 1
+ status { 1 }
end
trait :installing do
- status 2
+ status { 2 }
end
trait :installed do
- status 3
+ status { 3 }
end
trait :updating do
- status 4
+ status { 4 }
end
trait :updated do
- status 5
+ status { 5 }
end
trait :update_errored do
- status(6)
- status_reason 'something went wrong'
+ status { 6 }
+ status_reason { 'something went wrong' }
end
trait :uninstalling do
- status 7
+ status { 7 }
end
trait :uninstall_errored do
- status(8)
- status_reason 'something went wrong'
+ status { 8 }
+ status_reason { 'something went wrong' }
end
trait :timed_out do
@@ -75,7 +75,7 @@ FactoryBot.define do
end
factory :clusters_applications_cert_manager, class: Clusters::Applications::CertManager do
- email 'admin@example.com'
+ email { 'admin@example.com' }
cluster factory: %i(cluster with_installed_helm provided_by_gcp)
end
@@ -89,7 +89,7 @@ FactoryBot.define do
end
factory :clusters_applications_knative, class: Clusters::Applications::Knative do
- hostname 'example.com'
+ hostname { 'example.com' }
cluster factory: %i(cluster with_installed_helm provided_by_gcp)
end
diff --git a/spec/factories/clusters/clusters.rb b/spec/factories/clusters/clusters.rb
index d294e6d055e..63f33633a3c 100644
--- a/spec/factories/clusters/clusters.rb
+++ b/spec/factories/clusters/clusters.rb
@@ -3,10 +3,10 @@
FactoryBot.define do
factory :cluster, class: Clusters::Cluster do
user
- name 'test-cluster'
- cluster_type :project_type
- managed true
- namespace_per_environment true
+ name { 'test-cluster' }
+ cluster_type { :project_type }
+ managed { true }
+ namespace_per_environment { true }
factory :cluster_for_group, traits: [:provided_by_gcp, :group]
@@ -30,27 +30,39 @@ FactoryBot.define do
end
end
+ trait :management_project do
+ management_project factory: :project
+ end
+
trait :namespace_per_environment_disabled do
- namespace_per_environment false
+ namespace_per_environment { false }
end
trait :provided_by_user do
- provider_type :user
- platform_type :kubernetes
+ provider_type { :user }
+ platform_type { :kubernetes }
platform_kubernetes factory: [:cluster_platform_kubernetes, :configured]
end
trait :provided_by_gcp do
- provider_type :gcp
- platform_type :kubernetes
+ provider_type { :gcp }
+ platform_type { :kubernetes }
provider_gcp factory: [:cluster_provider_gcp, :created]
platform_kubernetes factory: [:cluster_platform_kubernetes, :configured]
end
+ trait :provided_by_aws do
+ provider_type { :aws }
+ platform_type { :kubernetes }
+
+ provider_aws factory: [:cluster_provider_aws, :created]
+ platform_kubernetes factory: [:cluster_platform_kubernetes, :configured]
+ end
+
trait :providing_by_gcp do
- provider_type :gcp
+ provider_type { :gcp }
provider_gcp factory: [:cluster_provider_gcp, :creating]
end
@@ -58,8 +70,12 @@ FactoryBot.define do
platform_kubernetes factory: [:cluster_platform_kubernetes, :configured, :rbac_disabled]
end
+ trait :cloud_run_enabled do
+ provider_gcp factory: [:cluster_provider_gcp, :created, :cloud_run_enabled]
+ end
+
trait :disabled do
- enabled false
+ enabled { false }
end
trait :production_environment do
@@ -71,11 +87,11 @@ FactoryBot.define do
end
trait :with_domain do
- domain 'example.com'
+ domain { 'example.com' }
end
trait :not_managed do
- managed false
+ managed { false }
end
end
end
diff --git a/spec/factories/clusters/kubernetes_namespaces.rb b/spec/factories/clusters/kubernetes_namespaces.rb
index 8d6ad1b9f79..75895e1c020 100644
--- a/spec/factories/clusters/kubernetes_namespaces.rb
+++ b/spec/factories/clusters/kubernetes_namespaces.rb
@@ -27,7 +27,7 @@ FactoryBot.define do
end
trait :without_token do
- service_account_token nil
+ service_account_token { nil }
end
end
end
diff --git a/spec/factories/clusters/platforms/kubernetes.rb b/spec/factories/clusters/platforms/kubernetes.rb
index d5dc288fddb..2757498e36b 100644
--- a/spec/factories/clusters/platforms/kubernetes.rb
+++ b/spec/factories/clusters/platforms/kubernetes.rb
@@ -3,14 +3,14 @@
FactoryBot.define do
factory :cluster_platform_kubernetes, class: Clusters::Platforms::Kubernetes do
cluster
- namespace nil
- api_url 'https://kubernetes.example.com'
+ namespace { nil }
+ api_url { 'https://kubernetes.example.com' }
token { 'a' * 40 }
trait :configured do
- api_url 'https://kubernetes.example.com'
- username 'xxxxxx'
- password 'xxxxxx'
+ api_url { 'https://kubernetes.example.com' }
+ username { 'xxxxxx' }
+ password { 'xxxxxx' }
before(:create) do |platform_kubernetes, evaluator|
pem_file = File.expand_path(Rails.root.join('spec/fixtures/clusters/sample_cert.pem'))
@@ -19,7 +19,7 @@ FactoryBot.define do
end
trait :rbac_disabled do
- authorization_type :abac
+ authorization_type { :abac }
end
end
end
diff --git a/spec/factories/clusters/providers/aws.rb b/spec/factories/clusters/providers/aws.rb
new file mode 100644
index 00000000000..f4bc61455c5
--- /dev/null
+++ b/spec/factories/clusters/providers/aws.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :cluster_provider_aws, class: Clusters::Providers::Aws do
+ cluster
+ created_by_user factory: :user
+
+ role_arn { 'arn:aws:iam::123456789012:role/role-name' }
+ vpc_id { 'vpc-00000000000000000' }
+ subnet_ids { %w(subnet-00000000000000000 subnet-11111111111111111) }
+ security_group_id { 'sg-00000000000000000' }
+ key_name { 'user' }
+
+ trait :scheduled do
+ access_key_id { 'access_key_id' }
+ secret_access_key { 'secret_access_key' }
+ session_token { 'session_token' }
+ end
+
+ trait :creating do
+ after(:build) do |provider|
+ provider.make_creating
+ end
+ end
+
+ trait :created do
+ after(:build) do |provider|
+ provider.make_created
+ end
+ end
+
+ trait :errored do
+ after(:build) do |provider|
+ provider.make_errored('An error occurred')
+ end
+ end
+ end
+end
diff --git a/spec/factories/clusters/providers/gcp.rb b/spec/factories/clusters/providers/gcp.rb
index 22462651b6a..83b65dc8087 100644
--- a/spec/factories/clusters/providers/gcp.rb
+++ b/spec/factories/clusters/providers/gcp.rb
@@ -3,14 +3,14 @@
FactoryBot.define do
factory :cluster_provider_gcp, class: Clusters::Providers::Gcp do
cluster
- gcp_project_id 'test-gcp-project'
+ gcp_project_id { 'test-gcp-project' }
trait :scheduled do
- access_token 'access_token_123'
+ access_token { 'access_token_123' }
end
trait :creating do
- access_token 'access_token_123'
+ access_token { 'access_token_123' }
after(:build) do |gcp, evaluator|
gcp.make_creating('operation-123')
@@ -18,7 +18,7 @@ FactoryBot.define do
end
trait :created do
- endpoint '111.111.111.111'
+ endpoint { '111.111.111.111' }
after(:build) do |gcp, evaluator|
gcp.make_created
@@ -32,7 +32,11 @@ FactoryBot.define do
end
trait :abac_enabled do
- legacy_abac true
+ legacy_abac { true }
+ end
+
+ trait :cloud_run_enabled do
+ cloud_run { true }
end
end
end
diff --git a/spec/factories/commit_statuses.rb b/spec/factories/commit_statuses.rb
index a76da30217e..3ce71a1b05d 100644
--- a/spec/factories/commit_statuses.rb
+++ b/spec/factories/commit_statuses.rb
@@ -2,53 +2,53 @@
FactoryBot.define do
factory :commit_status, class: CommitStatus do
- name 'default'
- stage 'test'
- stage_idx 0
- status 'success'
- description 'commit status'
+ name { 'default' }
+ stage { 'test' }
+ stage_idx { 0 }
+ status { 'success' }
+ description { 'commit status'}
pipeline factory: :ci_pipeline_with_one_job
- started_at 'Tue, 26 Jan 2016 08:21:42 +0100'
- finished_at 'Tue, 26 Jan 2016 08:23:42 +0100'
+ started_at { 'Tue, 26 Jan 2016 08:21:42 +0100'}
+ finished_at { 'Tue, 26 Jan 2016 08:23:42 +0100'}
trait :success do
- status 'success'
+ status { 'success' }
end
trait :failed do
- status 'failed'
+ status { 'failed' }
end
trait :canceled do
- status 'canceled'
+ status { 'canceled' }
end
trait :skipped do
- status 'skipped'
+ status { 'skipped' }
end
trait :running do
- status 'running'
+ status { 'running' }
end
trait :pending do
- status 'pending'
+ status { 'pending' }
end
trait :preparing do
- status 'preparing'
+ status { 'preparing' }
end
trait :created do
- status 'created'
+ status { 'created' }
end
trait :manual do
- status 'manual'
+ status { 'manual' }
end
trait :scheduled do
- status 'scheduled'
+ status { 'scheduled' }
end
after(:build) do |build, evaluator|
@@ -56,8 +56,8 @@ FactoryBot.define do
end
factory :generic_commit_status, class: GenericCommitStatus do
- name 'generic'
- description 'external commit status'
+ name { 'generic' }
+ description { 'external commit status' }
end
end
end
diff --git a/spec/factories/commits.rb b/spec/factories/commits.rb
index d1554426a76..d006f9baf1f 100644
--- a/spec/factories/commits.rb
+++ b/spec/factories/commits.rb
@@ -5,7 +5,7 @@ require_relative '../support/helpers/repo_helpers'
FactoryBot.define do
factory :commit do
transient do
- author nil
+ author { nil }
end
git_commit do
diff --git a/spec/factories/container_repositories.rb b/spec/factories/container_repositories.rb
index 0b756220d68..4cf1537f64b 100644
--- a/spec/factories/container_repositories.rb
+++ b/spec/factories/container_repositories.rb
@@ -6,29 +6,32 @@ FactoryBot.define do
project
transient do
- tags []
+ tags { [] }
end
trait :root do
- name ''
+ name { '' }
end
after(:build) do |repository, evaluator|
next if evaluator.tags.to_a.none?
+ tags = evaluator.tags
+ # convert Array into Hash
+ tags = tags.product(['sha256:4c8e63ca4cb663ce6c688cb06f1c372b088dac5b6d7ad7d49cd620d85cf72a15']).to_h unless tags.is_a?(Hash)
+
allow(repository.client)
.to receive(:repository_tags)
.and_return({
'name' => repository.path,
- 'tags' => evaluator.tags
+ 'tags' => tags.keys
})
- evaluator.tags.each do |tag|
+ tags.each_pair do |tag, digest|
allow(repository.client)
.to receive(:repository_tag_digest)
.with(repository.path, tag)
- .and_return('sha256:4c8e63ca4cb663ce6c688cb06f1c3' \
- '72b088dac5b6d7ad7d49cd620d85cf72a15')
+ .and_return(digest)
end
end
end
diff --git a/spec/factories/conversational_development_index_metrics.rb b/spec/factories/conversational_development_index_metrics.rb
index ea5816684c6..f039bac81d0 100644
--- a/spec/factories/conversational_development_index_metrics.rb
+++ b/spec/factories/conversational_development_index_metrics.rb
@@ -2,44 +2,44 @@
FactoryBot.define do
factory :conversational_development_index_metric, class: ConversationalDevelopmentIndex::Metric do
- leader_issues 9.256
- instance_issues 1.234
- percentage_issues 13.331
+ leader_issues { 9.256 }
+ instance_issues { 1.234 }
+ percentage_issues { 13.331 }
- leader_notes 30.33333
- instance_notes 28.123
- percentage_notes 92.713
+ leader_notes { 30.33333 }
+ instance_notes { 28.123 }
+ percentage_notes { 92.713 }
- leader_milestones 16.2456
- instance_milestones 1.234
- percentage_milestones 7.595
+ leader_milestones { 16.2456 }
+ instance_milestones { 1.234 }
+ percentage_milestones { 7.595 }
- leader_boards 5.2123
- instance_boards 3.254
- percentage_boards 62.429
+ leader_boards { 5.2123 }
+ instance_boards { 3.254 }
+ percentage_boards { 62.429 }
- leader_merge_requests 1.2
- instance_merge_requests 0.6
- percentage_merge_requests 50.0
+ leader_merge_requests { 1.2 }
+ instance_merge_requests { 0.6 }
+ percentage_merge_requests { 50.0 }
- leader_ci_pipelines 12.1234
- instance_ci_pipelines 2.344
- percentage_ci_pipelines 19.334
+ leader_ci_pipelines { 12.1234 }
+ instance_ci_pipelines { 2.344 }
+ percentage_ci_pipelines { 19.334 }
- leader_environments 3.3333
- instance_environments 2.2222
- percentage_environments 66.672
+ leader_environments { 3.3333 }
+ instance_environments { 2.2222 }
+ percentage_environments { 66.672 }
- leader_deployments 1.200
- instance_deployments 0.771
- percentage_deployments 64.25
+ leader_deployments { 1.200 }
+ instance_deployments { 0.771 }
+ percentage_deployments { 64.25 }
- leader_projects_prometheus_active 0.111
- instance_projects_prometheus_active 0.109
- percentage_projects_prometheus_active 98.198
+ leader_projects_prometheus_active { 0.111 }
+ instance_projects_prometheus_active { 0.109 }
+ percentage_projects_prometheus_active { 98.198 }
- leader_service_desk_issues 15.891
- instance_service_desk_issues 13.345
- percentage_service_desk_issues 83.978
+ leader_service_desk_issues { 15.891 }
+ instance_service_desk_issues { 13.345 }
+ percentage_service_desk_issues { 83.978 }
end
end
diff --git a/spec/factories/deploy_keys_projects.rb b/spec/factories/deploy_keys_projects.rb
index 7f82902dee7..2a429bf8e56 100644
--- a/spec/factories/deploy_keys_projects.rb
+++ b/spec/factories/deploy_keys_projects.rb
@@ -6,7 +6,7 @@ FactoryBot.define do
project
trait :write_access do
- can_push true
+ can_push { true }
end
end
end
diff --git a/spec/factories/deploy_tokens.rb b/spec/factories/deploy_tokens.rb
index 99486acc2ab..42ed66ac191 100644
--- a/spec/factories/deploy_tokens.rb
+++ b/spec/factories/deploy_tokens.rb
@@ -2,20 +2,20 @@
FactoryBot.define do
factory :deploy_token do
- token nil
- token_encrypted { Gitlab::CryptoHelper.aes256_gcm_encrypt( SecureRandom.hex(50) ) }
+ token { nil }
+ token_encrypted { Gitlab::CryptoHelper.aes256_gcm_encrypt(SecureRandom.hex(50)) }
sequence(:name) { |n| "PDT #{n}" }
- read_repository true
- read_registry true
- revoked false
+ read_repository { true }
+ read_registry { true }
+ revoked { false }
expires_at { 5.days.from_now }
trait :revoked do
- revoked true
+ revoked { true }
end
trait :gitlab_deploy_token do
- name DeployToken::GITLAB_DEPLOY_TOKEN_NAME
+ name { DeployToken::GITLAB_DEPLOY_TOKEN_NAME }
end
trait :expired do
diff --git a/spec/factories/deployments.rb b/spec/factories/deployments.rb
index 50dc304a10e..f4da206990c 100644
--- a/spec/factories/deployments.rb
+++ b/spec/factories/deployments.rb
@@ -2,11 +2,11 @@
FactoryBot.define do
factory :deployment, class: Deployment do
- sha 'b83d6e391c22777fca1ed3012fce84f633d7fed0'
- ref 'master'
- tag false
- user nil
- project nil
+ sha { 'b83d6e391c22777fca1ed3012fce84f633d7fed0' }
+ ref { 'master' }
+ tag { false }
+ user { nil }
+ project { nil }
deployable factory: :ci_build
environment factory: :environment
@@ -25,7 +25,7 @@ FactoryBot.define do
trait :review_app do
sha { TestEnv::BRANCH_SHA['pages-deploy'] }
- ref 'pages-deploy'
+ ref { 'pages-deploy' }
end
trait :on_cluster do
@@ -33,21 +33,21 @@ FactoryBot.define do
end
trait :running do
- status :running
+ status { :running }
end
trait :success do
- status :success
+ status { :success }
finished_at { Time.now }
end
trait :failed do
- status :failed
+ status { :failed }
finished_at { Time.now }
end
trait :canceled do
- status :canceled
+ status { :canceled }
finished_at { Time.now }
end
diff --git a/spec/factories/environments.rb b/spec/factories/environments.rb
index b5c8f0ca4f0..9286f49bc59 100644
--- a/spec/factories/environments.rb
+++ b/spec/factories/environments.rb
@@ -9,7 +9,7 @@ FactoryBot.define do
trait :with_review_app do |environment|
transient do
- ref 'master'
+ ref { 'master' }
end
# At this point `review app` is an ephemeral concept related to
@@ -41,8 +41,8 @@ FactoryBot.define do
end
trait :non_playable do
- status 'created'
- self.when 'manual'
+ status { 'created' }
+ self.when { 'manual' }
end
end
end
diff --git a/spec/factories/error_tracking/error.rb b/spec/factories/error_tracking/error.rb
index ff883a3d22c..541bc410462 100644
--- a/spec/factories/error_tracking/error.rb
+++ b/spec/factories/error_tracking/error.rb
@@ -2,22 +2,22 @@
FactoryBot.define do
factory :error_tracking_error, class: Gitlab::ErrorTracking::Error do
- id 'id'
- title 'title'
- type 'error'
- user_count 1
- count 2
+ id { 'id' }
+ title { 'title' }
+ type { 'error' }
+ user_count { 1 }
+ count { 2 }
first_seen { Time.now }
last_seen { Time.now }
- message 'message'
- culprit 'culprit'
- external_url 'http://example.com/id'
- project_id 'project1'
- project_name 'project name'
- project_slug 'project_name'
- short_id 'ID'
- status 'unresolved'
- frequency []
+ message { 'message' }
+ culprit { 'culprit' }
+ external_url { 'http://example.com/id' }
+ project_id { 'project1' }
+ project_name { 'project name' }
+ project_slug { 'project_name' }
+ short_id { 'ID' }
+ status { 'unresolved' }
+ frequency { [] }
skip_create
end
diff --git a/spec/factories/error_tracking/project.rb b/spec/factories/error_tracking/project.rb
index 5e9219b241f..885d398d433 100644
--- a/spec/factories/error_tracking/project.rb
+++ b/spec/factories/error_tracking/project.rb
@@ -2,13 +2,13 @@
FactoryBot.define do
factory :error_tracking_project, class: Gitlab::ErrorTracking::Project do
- id '1'
- name 'Sentry Example'
- slug 'sentry-example'
- status 'active'
- organization_name 'Sentry'
- organization_id '1'
- organization_slug 'sentry'
+ id { '1' }
+ name { 'Sentry Example' }
+ slug { 'sentry-example' }
+ status { 'active' }
+ organization_name { 'Sentry' }
+ organization_id { '1' }
+ organization_slug { 'sentry' }
skip_create
end
diff --git a/spec/factories/events.rb b/spec/factories/events.rb
index b15eb1592fc..4eedcd02c9a 100644
--- a/spec/factories/events.rb
+++ b/spec/factories/events.rb
@@ -4,19 +4,19 @@ FactoryBot.define do
factory :event do
project
author(factory: :user) { project.creator }
- action Event::JOINED
+ action { Event::JOINED }
- trait(:created) { action Event::CREATED }
- trait(:updated) { action Event::UPDATED }
- trait(:closed) { action Event::CLOSED }
- trait(:reopened) { action Event::REOPENED }
- trait(:pushed) { action Event::PUSHED }
- trait(:commented) { action Event::COMMENTED }
- trait(:merged) { action Event::MERGED }
- trait(:joined) { action Event::JOINED }
- trait(:left) { action Event::LEFT }
- trait(:destroyed) { action Event::DESTROYED }
- trait(:expired) { action Event::EXPIRED }
+ trait(:created) { action { Event::CREATED } }
+ trait(:updated) { action { Event::UPDATED } }
+ trait(:closed) { action { Event::CLOSED } }
+ trait(:reopened) { action { Event::REOPENED } }
+ trait(:pushed) { action { Event::PUSHED } }
+ trait(:commented) { action { Event::COMMENTED } }
+ trait(:merged) { action { Event::MERGED } }
+ trait(:joined) { action { Event::JOINED } }
+ trait(:left) { action { Event::LEFT } }
+ trait(:destroyed) { action { Event::DESTROYED } }
+ trait(:expired) { action { Event::EXPIRED } }
factory :closed_issue_event do
action { Event::CLOSED }
@@ -27,15 +27,15 @@ FactoryBot.define do
factory :push_event, class: PushEvent do
project factory: :project_empty_repo
author(factory: :user) { project.creator }
- action Event::PUSHED
+ action { Event::PUSHED }
end
factory :push_event_payload do
event
- commit_count 1
- action :pushed
- ref_type :branch
- ref 'master'
- commit_to '3cdce97ed87c91368561584e7358f4d46e3e173c'
+ commit_count { 1 }
+ action { :pushed }
+ ref_type { :branch }
+ ref { 'master' }
+ commit_to { '3cdce97ed87c91368561584e7358f4d46e3e173c' }
end
end
diff --git a/spec/factories/evidences.rb b/spec/factories/evidences.rb
new file mode 100644
index 00000000000..964f232a1c9
--- /dev/null
+++ b/spec/factories/evidences.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :evidence do
+ release
+ end
+end
diff --git a/spec/factories/external_pull_requests.rb b/spec/factories/external_pull_requests.rb
index 08d0fa4d419..7a6e77f8572 100644
--- a/spec/factories/external_pull_requests.rb
+++ b/spec/factories/external_pull_requests.rb
@@ -4,14 +4,14 @@ FactoryBot.define do
factory :external_pull_request do
sequence(:pull_request_iid)
project
- source_branch 'feature'
- source_repository 'the-repository'
- source_sha '97de212e80737a608d939f648d959671fb0a0142'
- target_branch 'master'
- target_repository 'the-repository'
- target_sha 'a09386439ca39abe575675ffd4b89ae824fec22f'
- status :open
+ source_branch { 'feature' }
+ source_repository { 'the-repository' }
+ source_sha { '97de212e80737a608d939f648d959671fb0a0142' }
+ target_branch { 'master' }
+ target_repository { 'the-repository' }
+ target_sha { 'a09386439ca39abe575675ffd4b89ae824fec22f' }
+ status { :open }
- trait(:closed) { status 'closed' }
+ trait(:closed) { status { 'closed'} }
end
end
diff --git a/spec/factories/file_uploaders.rb b/spec/factories/file_uploaders.rb
index ec8f5c9af2d..dc888fdd535 100644
--- a/spec/factories/file_uploaders.rb
+++ b/spec/factories/file_uploaders.rb
@@ -5,7 +5,7 @@ FactoryBot.define do
skip_create
project
- secret nil
+ secret { nil }
transient do
fixture { 'rails_sample.jpg' }
diff --git a/spec/factories/gitaly/commit.rb b/spec/factories/gitaly/commit.rb
index 954b5338846..ef5301db770 100644
--- a/spec/factories/gitaly/commit.rb
+++ b/spec/factories/gitaly/commit.rb
@@ -12,6 +12,7 @@ FactoryBot.define do
Google::Protobuf::RepeatedField.new(:string, ids)
end
subject { "My commit" }
+
body { subject + "\nMy body" }
author { build(:gitaly_commit_author) }
committer { build(:gitaly_commit_author) }
diff --git a/spec/factories/gpg_signature.rb b/spec/factories/gpg_signature.rb
index a0fc1740d77..2ab4d190276 100644
--- a/spec/factories/gpg_signature.rb
+++ b/spec/factories/gpg_signature.rb
@@ -6,6 +6,6 @@ FactoryBot.define do
project
gpg_key
gpg_key_primary_keyid { gpg_key.keyid }
- verification_status :verified
+ verification_status { :verified }
end
end
diff --git a/spec/factories/grafana_integrations.rb b/spec/factories/grafana_integrations.rb
new file mode 100644
index 00000000000..c19417f5a90
--- /dev/null
+++ b/spec/factories/grafana_integrations.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :grafana_integration, class: GrafanaIntegration do
+ project
+ grafana_url { 'https://grafana.com' }
+ token { SecureRandom.hex(10) }
+ end
+end
diff --git a/spec/factories/group_members.rb b/spec/factories/group_members.rb
index a93f13395a2..3c9d469f23c 100644
--- a/spec/factories/group_members.rb
+++ b/spec/factories/group_members.rb
@@ -6,23 +6,23 @@ FactoryBot.define do
group
user
- trait(:guest) { access_level GroupMember::GUEST }
- trait(:reporter) { access_level GroupMember::REPORTER }
- trait(:developer) { access_level GroupMember::DEVELOPER }
- trait(:maintainer) { access_level GroupMember::MAINTAINER }
- trait(:owner) { access_level GroupMember::OWNER }
+ trait(:guest) { access_level { GroupMember::GUEST } }
+ trait(:reporter) { access_level { GroupMember::REPORTER } }
+ trait(:developer) { access_level { GroupMember::DEVELOPER } }
+ trait(:maintainer) { access_level { GroupMember::MAINTAINER } }
+ trait(:owner) { access_level { GroupMember::OWNER } }
trait(:access_request) { requested_at { Time.now } }
trait(:invited) do
- user_id nil
- invite_token 'xxx'
+ user_id { nil }
+ invite_token { 'xxx' }
sequence :invite_email do |n|
"email#{n}@email.com"
end
end
trait(:ldap) do
- ldap true
+ ldap { true }
end
trait :blocked do
diff --git a/spec/factories/groups.rb b/spec/factories/groups.rb
index d3c6101bad4..93c01f8034d 100644
--- a/spec/factories/groups.rb
+++ b/spec/factories/groups.rb
@@ -4,9 +4,9 @@ FactoryBot.define do
factory :group, class: Group, parent: :namespace do
sequence(:name) { |n| "group#{n}" }
path { name.downcase.gsub(/\s/, '_') }
- type 'Group'
- owner nil
- project_creation_level ::Gitlab::Access::MAINTAINER_PROJECT_ACCESS
+ type { 'Group' }
+ owner { nil }
+ project_creation_level { ::Gitlab::Access::MAINTAINER_PROJECT_ACCESS}
after(:create) do |group|
if group.owner
@@ -17,23 +17,23 @@ FactoryBot.define do
end
trait :public do
- visibility_level Gitlab::VisibilityLevel::PUBLIC
+ visibility_level { Gitlab::VisibilityLevel::PUBLIC}
end
trait :internal do
- visibility_level Gitlab::VisibilityLevel::INTERNAL
+ visibility_level {Gitlab::VisibilityLevel::INTERNAL}
end
trait :private do
- visibility_level Gitlab::VisibilityLevel::PRIVATE
+ visibility_level { Gitlab::VisibilityLevel::PRIVATE}
end
trait :with_avatar do
avatar { fixture_file_upload('spec/fixtures/dk.png') }
end
- trait :access_requestable do
- request_access_enabled true
+ trait :request_access_disabled do
+ request_access_enabled { false }
end
trait :nested do
@@ -41,15 +41,15 @@ FactoryBot.define do
end
trait :auto_devops_enabled do
- auto_devops_enabled true
+ auto_devops_enabled { true }
end
trait :auto_devops_disabled do
- auto_devops_enabled false
+ auto_devops_enabled { false }
end
trait :owner_subgroup_creation_only do
- subgroup_creation_level ::Gitlab::Access::OWNER_SUBGROUP_ACCESS
+ subgroup_creation_level { ::Gitlab::Access::OWNER_SUBGROUP_ACCESS}
end
end
end
diff --git a/spec/factories/identities.rb b/spec/factories/identities.rb
index 21cfe7fe623..a2615ce30c3 100644
--- a/spec/factories/identities.rb
+++ b/spec/factories/identities.rb
@@ -2,7 +2,7 @@
FactoryBot.define do
factory :identity do
- provider 'ldapmain'
- extern_uid 'my-ldap-id'
+ provider { 'ldapmain' }
+ extern_uid { 'my-ldap-id' }
end
end
diff --git a/spec/factories/import_states.rb b/spec/factories/import_states.rb
index 8e778200389..576f68ab57f 100644
--- a/spec/factories/import_states.rb
+++ b/spec/factories/import_states.rb
@@ -2,12 +2,12 @@
FactoryBot.define do
factory :import_state, class: ProjectImportState do
- status :none
+ status { :none }
association :project, factory: :project
transient do
import_url { generate(:url) }
- import_type nil
+ import_type { nil }
end
trait :repository do
@@ -15,23 +15,23 @@ FactoryBot.define do
end
trait :none do
- status :none
+ status { :none }
end
trait :scheduled do
- status :scheduled
+ status { :scheduled }
end
trait :started do
- status :started
+ status { :started }
end
trait :finished do
- status :finished
+ status { :finished }
end
trait :failed do
- status :failed
+ status { :failed }
end
after(:create) do |import_state, evaluator|
diff --git a/spec/factories/internal_ids.rb b/spec/factories/internal_ids.rb
index df5c5beeb42..bc6ea41ec06 100644
--- a/spec/factories/internal_ids.rb
+++ b/spec/factories/internal_ids.rb
@@ -3,7 +3,7 @@
FactoryBot.define do
factory :internal_id do
project
- usage :issues
+ usage { :issues }
last_value { project.issues.maximum(:iid) || 0 }
end
end
diff --git a/spec/factories/issues.rb b/spec/factories/issues.rb
index 434225f7022..46910078ee5 100644
--- a/spec/factories/issues.rb
+++ b/spec/factories/issues.rb
@@ -8,28 +8,32 @@ FactoryBot.define do
updated_by { author }
trait :confidential do
- confidential true
+ confidential { true }
end
trait :opened do
- state :opened
+ state_id { Issue.available_states[:opened] }
end
trait :locked do
- discussion_locked true
+ discussion_locked { true }
end
trait :closed do
- state :closed
+ state_id { Issue.available_states[:closed] }
closed_at { Time.now }
end
+ after(:build) do |issue, evaluator|
+ issue.state_id = Issue.available_states[evaluator.state]
+ end
+
factory :closed_issue, traits: [:closed]
factory :reopened_issue, traits: [:opened]
factory :labeled_issue do
transient do
- labels []
+ labels { [] }
end
after(:create) do |issue, evaluator|
diff --git a/spec/factories/labels.rb b/spec/factories/labels.rb
index 3eed750be03..89fcd8b1a9d 100644
--- a/spec/factories/labels.rb
+++ b/spec/factories/labels.rb
@@ -3,14 +3,14 @@
FactoryBot.define do
trait :base_label do
title { generate(:label_title) }
- color "#990000"
+ color { "#990000" }
end
factory :label, traits: [:base_label], class: ProjectLabel do
project
transient do
- priority nil
+ priority { nil }
end
after(:create) do |label, evaluator|
diff --git a/spec/factories/lfs_file_locks.rb b/spec/factories/lfs_file_locks.rb
index 73675d076ab..a676dabeff2 100644
--- a/spec/factories/lfs_file_locks.rb
+++ b/spec/factories/lfs_file_locks.rb
@@ -4,6 +4,6 @@ FactoryBot.define do
factory :lfs_file_lock do
user
project
- path 'README.md'
+ path { 'README.md' }
end
end
diff --git a/spec/factories/lfs_objects.rb b/spec/factories/lfs_objects.rb
index 631d87cfb12..35fc4db8519 100644
--- a/spec/factories/lfs_objects.rb
+++ b/spec/factories/lfs_objects.rb
@@ -5,7 +5,7 @@ include ActionDispatch::TestProcess
FactoryBot.define do
factory :lfs_object do
sequence(:oid) { |n| "b68143e6463773b1b6c6fd009a76c32aeec041faff32ba2ed42fd7f708a%05x" % n }
- size 499013
+ size { 499013 }
end
trait :with_file do
@@ -15,8 +15,8 @@ FactoryBot.define do
# The uniqueness constraint means we can't use the correct OID for all LFS
# objects, so the test needs to decide which (if any) object gets it
trait :correct_oid do
- oid 'b804383982bb89b00e828e3f44c038cc991d3d1768009fc39ba8e2c081b9fb75'
- size 1062
+ oid { 'b804383982bb89b00e828e3f44c038cc991d3d1768009fc39ba8e2c081b9fb75' }
+ size { 1062 }
end
trait :object_storage do
diff --git a/spec/factories/lfs_objects_projects.rb b/spec/factories/lfs_objects_projects.rb
index 7b55cc57f75..7d3e61ea2b1 100644
--- a/spec/factories/lfs_objects_projects.rb
+++ b/spec/factories/lfs_objects_projects.rb
@@ -4,6 +4,6 @@ FactoryBot.define do
factory :lfs_objects_project do
lfs_object
project
- repository_type :project
+ repository_type { :project }
end
end
diff --git a/spec/factories/lists.rb b/spec/factories/lists.rb
index e68611ec518..8785d3f0468 100644
--- a/spec/factories/lists.rb
+++ b/spec/factories/lists.rb
@@ -4,19 +4,19 @@ FactoryBot.define do
factory :list do
board
label
- list_type :label
+ list_type { :label }
sequence(:position)
end
factory :backlog_list, parent: :list do
- list_type :backlog
- label nil
- position nil
+ list_type { :backlog }
+ label { nil }
+ position { nil }
end
factory :closed_list, parent: :list do
- list_type :closed
- label nil
- position nil
+ list_type { :closed }
+ label { nil }
+ position { nil }
end
end
diff --git a/spec/factories/merge_request_diff_files.rb b/spec/factories/merge_request_diff_files.rb
index 469a7a0ac8d..86eff445ec8 100644
--- a/spec/factories/merge_request_diff_files.rb
+++ b/spec/factories/merge_request_diff_files.rb
@@ -4,44 +4,44 @@ FactoryBot.define do
factory :merge_request_diff_file do
association :merge_request_diff
- relative_order 0
- new_file true
- renamed_file false
- deleted_file false
- too_large false
- a_mode 0
- b_mode 100644
- new_path 'foo'
- old_path 'foo'
- diff ''
- binary false
+ relative_order { 0 }
+ new_file { true }
+ renamed_file { false }
+ deleted_file { false }
+ too_large { false }
+ a_mode { 0 }
+ b_mode { 100644 }
+ new_path { 'foo' }
+ old_path { 'foo' }
+ diff { '' }
+ binary { false }
trait :new_file do
- relative_order 0
- new_file true
- renamed_file false
- deleted_file false
- too_large false
- a_mode 0
- b_mode 100644
- new_path 'foo'
- old_path 'foo'
- diff ''
- binary false
+ relative_order { 0 }
+ new_file { true }
+ renamed_file { false }
+ deleted_file { false }
+ too_large { false }
+ a_mode { 0 }
+ b_mode { 100644 }
+ new_path { 'foo' }
+ old_path { 'foo' }
+ diff { '' }
+ binary { false }
end
trait :renamed_file do
- relative_order 662
- new_file false
- renamed_file true
- deleted_file false
- too_large false
- a_mode 100644
- b_mode 100644
- new_path 'bar'
- old_path 'baz'
- diff ''
- binary false
+ relative_order { 662 }
+ new_file { false }
+ renamed_file { true }
+ deleted_file { false }
+ too_large { false }
+ a_mode { 100644 }
+ b_mode { 100644 }
+ new_path { 'bar' }
+ old_path { 'baz' }
+ diff { '' }
+ binary { false }
end
end
end
diff --git a/spec/factories/merge_request_diffs.rb b/spec/factories/merge_request_diffs.rb
index e7b51189538..0c4c3244af5 100644
--- a/spec/factories/merge_request_diffs.rb
+++ b/spec/factories/merge_request_diffs.rb
@@ -3,8 +3,8 @@
FactoryBot.define do
factory :merge_request_diff do
association :merge_request
- state :collected
- commits_count 1
+ state { :collected }
+ commits_count { 1 }
base_commit_sha { Digest::SHA1.hexdigest(SecureRandom.hex) }
head_commit_sha { Digest::SHA1.hexdigest(SecureRandom.hex) }
diff --git a/spec/factories/merge_requests.rb b/spec/factories/merge_requests.rb
index 3d12ff98257..d16e0c10671 100644
--- a/spec/factories/merge_requests.rb
+++ b/spec/factories/merge_requests.rb
@@ -16,36 +16,36 @@ FactoryBot.define do
#
# See also RepoHelpers.sample_compare
#
- source_branch "master"
- target_branch "feature"
+ source_branch { "master" }
+ target_branch { "feature" }
- merge_status "can_be_merged"
+ merge_status { "can_be_merged" }
trait :with_diffs do
end
trait :with_image_diffs do
- source_branch "add_images_and_changes"
- target_branch "master"
+ source_branch { "add_images_and_changes" }
+ target_branch { "master" }
end
trait :without_diffs do
- source_branch "improve/awesome"
- target_branch "master"
+ source_branch { "improve/awesome" }
+ target_branch { "master" }
end
trait :conflict do
- source_branch "feature_conflict"
- target_branch "feature"
+ source_branch { "feature_conflict" }
+ target_branch { "feature" }
end
trait :merged do
- state :merged
+ state_id { MergeRequest.available_states[:merged] }
end
trait :merged_target do
- source_branch "merged-target"
- target_branch "improve/awesome"
+ source_branch { "merged-target" }
+ target_branch { "improve/awesome" }
end
trait :merged_last_month do
@@ -57,7 +57,7 @@ FactoryBot.define do
end
trait :closed do
- state :closed
+ state_id { MergeRequest.available_states[:closed] }
end
trait :closed_last_month do
@@ -69,36 +69,36 @@ FactoryBot.define do
end
trait :opened do
- state :opened
+ state_id { MergeRequest.available_states[:opened] }
end
trait :invalid do
- source_branch "feature_one"
- target_branch "feature_two"
+ source_branch { "feature_one" }
+ target_branch { "feature_two" }
end
trait :locked do
- state :locked
+ state_id { MergeRequest.available_states[:locked] }
end
trait :simple do
- source_branch "feature"
- target_branch "master"
+ source_branch { "feature" }
+ target_branch { "master" }
end
trait :rebased do
- source_branch "markdown"
- target_branch "improve/awesome"
+ source_branch { "markdown" }
+ target_branch { "improve/awesome" }
end
trait :diverged do
- source_branch "feature"
- target_branch "master"
+ source_branch { "feature" }
+ target_branch { "master" }
end
trait :merge_when_pipeline_succeeds do
- auto_merge_enabled true
- auto_merge_strategy AutoMergeService::STRATEGY_MERGE_WHEN_PIPELINE_SUCCEEDS
+ auto_merge_enabled { true }
+ auto_merge_strategy { AutoMergeService::STRATEGY_MERGE_WHEN_PIPELINE_SUCCEEDS }
merge_user { author }
end
@@ -162,7 +162,7 @@ FactoryBot.define do
end
trait :deployed_review_app do
- target_branch 'pages-deploy-target'
+ target_branch { 'pages-deploy-target' }
transient do
deployment { create(:deployment, :review_app) }
@@ -186,6 +186,10 @@ FactoryBot.define do
end
end
+ after(:build) do |merge_request, evaluator|
+ merge_request.state_id = MergeRequest.available_states[evaluator.state]
+ end
+
after(:create) do |merge_request, evaluator|
merge_request.cache_merge_request_closes_issues!
end
@@ -203,7 +207,7 @@ FactoryBot.define do
factory :labeled_merge_request do
transient do
- labels []
+ labels { [] }
end
after(:create) do |merge_request, evaluator|
diff --git a/spec/factories/milestones.rb b/spec/factories/milestones.rb
index 7d623000fc9..32eee645f6a 100644
--- a/spec/factories/milestones.rb
+++ b/spec/factories/milestones.rb
@@ -5,19 +5,19 @@ FactoryBot.define do
title
transient do
- project nil
- group nil
- project_id nil
- group_id nil
- parent nil
+ project { nil }
+ group { nil }
+ project_id { nil }
+ group_id { nil }
+ resource_parent { nil }
end
trait :active do
- state "active"
+ state { "active" }
end
trait :closed do
- state "closed"
+ state { "closed" }
end
trait :with_dates do
@@ -34,9 +34,9 @@ FactoryBot.define do
milestone.project = evaluator.project
elsif evaluator.project_id
milestone.project_id = evaluator.project_id
- elsif evaluator.parent
- id = evaluator.parent.id
- evaluator.parent.is_a?(Group) ? evaluator.group_id = id : evaluator.project_id = id
+ elsif evaluator.resource_parent
+ id = evaluator.resource_parent.id
+ evaluator.resource_parent.is_a?(Group) ? evaluator.group_id = id : evaluator.project_id = id
else
milestone.project = create(:project)
end
diff --git a/spec/factories/notes.rb b/spec/factories/notes.rb
index 5b9a7e6f864..2f02acca794 100644
--- a/spec/factories/notes.rb
+++ b/spec/factories/notes.rb
@@ -41,14 +41,14 @@ FactoryBot.define do
factory :legacy_diff_note_on_merge_request, traits: [:on_merge_request, :legacy_diff_note], class: LegacyDiffNote do
association :project, :repository
- position ''
+ position { '' }
end
factory :diff_note_on_merge_request, traits: [:on_merge_request], class: DiffNote do
association :project, :repository
transient do
- line_number 14
+ line_number { 14 }
diff_refs { noteable.try(:diff_refs) }
end
@@ -62,6 +62,18 @@ FactoryBot.define do
)
end
+ trait :folded_position do
+ position do
+ Gitlab::Diff::Position.new(
+ old_path: "files/ruby/popen.rb",
+ new_path: "files/ruby/popen.rb",
+ old_line: 1,
+ new_line: 1,
+ diff_refs: diff_refs
+ )
+ end
+ end
+
trait :resolved do
resolved_at { Time.now }
resolved_by { create(:user) }
@@ -87,7 +99,7 @@ FactoryBot.define do
association :project, :repository
transient do
- line_number 14
+ line_number { 14 }
diff_refs { project.commit(commit_id).try(:diff_refs) }
end
@@ -104,14 +116,14 @@ FactoryBot.define do
trait :on_commit do
association :project, :repository
- noteable nil
- noteable_type 'Commit'
- noteable_id nil
+ noteable { nil }
+ noteable_type { 'Commit' }
+ noteable_id { nil }
commit_id { RepoHelpers.sample_commit.id }
end
trait :legacy_diff_note do
- line_code "0_184_184"
+ line_code { "0_184_184" }
end
trait :on_issue do
@@ -132,19 +144,19 @@ FactoryBot.define do
trait :on_personal_snippet do
noteable { create(:personal_snippet) }
- project nil
+ project { nil }
end
trait :system do
- system true
+ system { true }
end
trait :downvote do
- note "thumbsdown"
+ note { "thumbsdown" }
end
trait :upvote do
- note "thumbsup"
+ note { "thumbsup" }
end
trait :with_attachment do
@@ -156,7 +168,7 @@ FactoryBot.define do
end
transient do
- in_reply_to nil
+ in_reply_to { nil }
end
before(:create) do |note, evaluator|
diff --git a/spec/factories/notification_settings.rb b/spec/factories/notification_settings.rb
index c16b0e456ba..025120fefc1 100644
--- a/spec/factories/notification_settings.rb
+++ b/spec/factories/notification_settings.rb
@@ -4,6 +4,6 @@ FactoryBot.define do
factory :notification_setting do
source factory: :project
user
- level 3
+ level { 3 }
end
end
diff --git a/spec/factories/oauth_applications.rb b/spec/factories/oauth_applications.rb
index 4748b320298..aff32805f03 100644
--- a/spec/factories/oauth_applications.rb
+++ b/spec/factories/oauth_applications.rb
@@ -6,6 +6,6 @@ FactoryBot.define do
uid { Doorkeeper::OAuth::Helpers::UniqueToken.generate }
redirect_uri { generate(:url) }
owner
- owner_type 'User'
+ owner_type { 'User' }
end
end
diff --git a/spec/factories/pages_domains.rb b/spec/factories/pages_domains.rb
index ae3988bdd69..91423832888 100644
--- a/spec/factories/pages_domains.rb
+++ b/spec/factories/pages_domains.rb
@@ -6,7 +6,8 @@ FactoryBot.define do
verified_at { Time.now }
enabled_until { 1.week.from_now }
- certificate '-----BEGIN CERTIFICATE-----
+ certificate do
+ '-----BEGIN CERTIFICATE-----
MIICGzCCAYSgAwIBAgIBATANBgkqhkiG9w0BAQUFADAbMRkwFwYDVQQDExB0ZXN0
LWNlcnRpZmljYXRlMB4XDTE2MDIxMjE0MzIwMFoXDTIwMDQxMjE0MzIwMFowGzEZ
MBcGA1UEAxMQdGVzdC1jZXJ0aWZpY2F0ZTCBnzANBgkqhkiG9w0BAQEFAAOBjQAw
@@ -20,8 +21,10 @@ joZp2JHYvNlTPkRJ/J4TcXxBTJmArcQgTIuNoBtC+0A/SwdK4MfTCUY4vNWNdese
5A4K65Nb7Oh1AdQieTBHNXXCdyFsva9/ScfQGEl7p55a52jOPs0StPd7g64uvjlg
YHi2yesCrOvVXt+lgPTd
-----END CERTIFICATE-----'
+ end
- key '-----BEGIN PRIVATE KEY-----
+ key do
+ '-----BEGIN PRIVATE KEY-----
MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAKS+CfS9GcRSdYSN
SzyH5QJQBr5umRL6E+KilOV39iYFO/9oHjUdapTRWkrwnNPCp7qaeck4Jr8iv14t
PVNDfNr76eGb6/3YknOAP0QOjLWunoC8kjU+N/JHU52NrUeX3qEy8EKV9LeCDJcB
@@ -37,10 +40,11 @@ EPjGlXIT+aW2XiPmK3ZlCDcWIenE+lmtbOpI159Wpk8BGXs/s/xBAkEAlAY3ymgx
63BDJEwvOb2IaP8lDDxNsXx9XJNVvQbv5n15vNsLHbjslHfAhAbxnLQ1fLhUPqSi
nNp/xedE1YxutQ==
-----END PRIVATE KEY-----'
+ end
trait :disabled do
- verified_at nil
- enabled_until nil
+ verified_at { nil }
+ enabled_until { nil }
end
trait :scheduled_for_removal do
@@ -52,7 +56,7 @@ nNp/xedE1YxutQ==
end
trait :unverified do
- verified_at nil
+ verified_at { nil }
end
trait :reverify do
@@ -64,17 +68,18 @@ nNp/xedE1YxutQ==
end
trait :without_certificate do
- certificate nil
+ certificate { nil }
end
trait :without_key do
- key nil
+ key { nil }
end
trait :with_missing_chain do
# This certificate is signed with different key
# And misses the CA to build trust chain
- certificate '-----BEGIN CERTIFICATE-----
+ certificate do
+ '-----BEGIN CERTIFICATE-----
MIIDGTCCAgGgAwIBAgIBAjANBgkqhkiG9w0BAQUFADASMRAwDgYDVQQDEwdUZXN0
IENBMB4XDTE2MDIxMjE0MjMwMFoXDTE3MDIxMTE0MjMwMFowHTEbMBkGA1UEAxMS
dGVzdC1jZXJ0aWZpY2F0ZS0yMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC
@@ -93,13 +98,15 @@ WlU8raZoPIqf6H/8JA97pM/nk/3CqCoHsouSQv+jGY4pSL22RqsO0ylIM0LDBbmF
m4AEaojTljX1tMJAF9Rbiw/omam5bDPq2JWtosrz/zB69y5FaQjc6FnCk0M4oN/+
VM+d42lQAgoq318A84Xu5vRh1KCAJuztkhNbM+w=
-----END CERTIFICATE-----'
+ end
end
trait :with_trusted_chain do
# This contains
# [Intermediate #2 (SHA-2)] 'Comodo RSA Domain Validation Secure Server CA'
# [Intermediate #1 (SHA-2)] 'COMODO RSA Certification Authority'
- certificate '-----BEGIN CERTIFICATE-----
+ certificate do
+ '-----BEGIN CERTIFICATE-----
MIIGCDCCA/CgAwIBAgIQKy5u6tl1NmwUim7bo3yMBzANBgkqhkiG9w0BAQwFADCB
hTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G
A1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNV
@@ -166,13 +173,15 @@ B5a6SE2Q8pTIqXOi6wZ7I53eovNNVZ96YUWYGGjHXkBrI/V5eu+MtWuLt29G9Hvx
PUsE2JOAWVrgQSQdso8VYFhH2+9uRv0V9dlfmrPb2LjkQLPNlzmuhbsdjrzch5vR
pu/xO28QOG8=
-----END CERTIFICATE-----'
+ end
end
trait :with_trusted_expired_chain do
# This contains
# Let's Encrypt Authority X3
# DST Root CA X3
- certificate '-----BEGIN CERTIFICATE-----
+ certificate do
+ '-----BEGIN CERTIFICATE-----
MIIFSjCCBDKgAwIBAgISAw24xGWrFotvTBa6AZI/pzq1MA0GCSqGSIb3DQEBCwUA
MEoxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MSMwIQYDVQQD
ExpMZXQncyBFbmNyeXB0IEF1dGhvcml0eSBYMzAeFw0xOTAzMDcxNzU5NTZaFw0x
@@ -250,10 +259,12 @@ R8srzJmwN0jP41ZL9c8PDHIyh8bwRLtTcm1D9SZImlJnt1ir/md2cXjbDaJWFBM5
JDGFoqgCWjBH4d1QB7wCCZAA62RjYJsWvIjJEubSfZGL+T0yjWW06XyxV3bqxbYo
Ob8VZRzI9neWagqNdwvYkQsEjgfbKbYK7p2CNTUQ
-----END CERTIFICATE-----'
+ end
end
trait :with_expired_certificate do
- certificate '-----BEGIN CERTIFICATE-----
+ certificate do
+ '-----BEGIN CERTIFICATE-----
MIIBsDCCARmgAwIBAgIBATANBgkqhkiG9w0BAQUFADAeMRwwGgYDVQQDExNleHBp
cmVkLWNlcnRpZmljYXRlMB4XDTE1MDIxMjE0MzMwMFoXDTE2MDIwMTE0MzMwMFow
HjEcMBoGA1UEAxMTZXhwaXJlZC1jZXJ0aWZpY2F0ZTCBnzANBgkqhkiG9w0BAQEF
@@ -265,6 +276,7 @@ Hq9LkWn6WP4EHsesHyslgTQZF8C7kVLTbLn2noLnOE+Mp3vcWlZxl3Yk6aZMhKS+
Iy6oRpHaCF/2obZdIdgf9rlyz0fkqyHJc9GkioSoOhJZxEV2SgAkap8yS0sX2tJ9
ZDXgrA==
-----END CERTIFICATE-----'
+ end
end
trait :letsencrypt do
@@ -273,7 +285,8 @@ ZDXgrA==
end
trait :explicit_ecdsa do
- certificate '-----BEGIN CERTIFICATE-----
+ certificate do
+ '-----BEGIN CERTIFICATE-----
MIID1zCCAzkCCQDatOIwBlktwjAKBggqhkjOPQQDAjBPMQswCQYDVQQGEwJVUzEL
MAkGA1UECAwCTlkxCzAJBgNVBAcMAk5ZMQswCQYDVQQLDAJJVDEZMBcGA1UEAwwQ
dGVzdC1jZXJ0aWZpY2F0ZTAeFw0xOTA4MjkxMTE1NDBaFw0yMTA4MjgxMTE1NDBa
@@ -296,8 +309,10 @@ OjSJpIDdFWGVYJHyMDI5WgQyhm4hAioXJ0T22Zab8Wmq+hBYRJNcHoaV894blfqR
V3ZJgam8EQJCAcnPpJQ0IqoT1pAQkaL3+Ka8ZaaCd6/8RnoDtGvWljisuyH65SRu
kmYv87bZe1KqOZDoaDBdfVsoxcGbik19lBPV
-----END CERTIFICATE-----'
+ end
- key '-----BEGIN EC PARAMETERS-----
+ key do
+ '-----BEGIN EC PARAMETERS-----
MIIBwgIBATBNBgcqhkjOPQEBAkIB////////////////////////////////////
//////////////////////////////////////////////////8wgZ4EQgH/////
////////////////////////////////////////////////////////////////
@@ -326,10 +341,12 @@ ZAkCAQGhgYkDgYYABAFUb/hz+GCXfq2geP54Yvimq/uXsz5kpAni/PRj0TrEjH6C
+Iu6YSMSWFwExlVeJeFvm3F/XW5cBafmfpCF7Llgo8w2MsuoOpobX158IsJ3bUDR
Nw==
-----END EC PRIVATE KEY-----'
+ end
end
trait :ecdsa do
- certificate '-----BEGIN CERTIFICATE-----
+ certificate do
+ '-----BEGIN CERTIFICATE-----
MIIB8zCCAVUCCQCGKuPQ6SBxUTAKBggqhkjOPQQDAjA+MQswCQYDVQQGEwJVUzEL
MAkGA1UECAwCVVMxCzAJBgNVBAcMAlVTMRUwEwYDVQQDDAxzaHVzaGxpbi5kZXYw
HhcNMTkwOTAyMDkyMDUxWhcNMjEwOTAxMDkyMDUxWjA+MQswCQYDVQQGEwJVUzEL
@@ -342,8 +359,10 @@ R9WYUg5+PQMg7kS+4K/5+5gonWCvaMcP+2P7hltUcvq41l3uMKKCZRU/x60/FMHc
1ZXdAkIBuVtm9RJXziNOKS4TcpH9os/FuREW8YQlpec58LDZdlivcHnikHZ4LCri
T7zu3VY6Rq+V/IKpsQwQjmoTJ0IpCM8=
-----END CERTIFICATE-----'
+ end
- key '-----BEGIN EC PARAMETERS-----
+ key do
+ '-----BEGIN EC PARAMETERS-----
BgUrgQQAIw==
-----END EC PARAMETERS-----
-----BEGIN EC PRIVATE KEY-----
@@ -353,6 +372,7 @@ dNqyCW1lFsgyt6KHJw6H8bhNemSny1O0Sy4P6yHxq7cnLCyLz1FJnq+BWgJjv/8N
Da9qZifIFmpsv5xNAGxDvANQRpLI/RHc/hvo7HfjuljNfBJ1I6tr0KbhMxB76mtU
x6zG6WoibsbsJMj70nwseUnPTBQNDP+j61RJjC/r
-----END EC PRIVATE KEY-----'
+ end
end
end
end
diff --git a/spec/factories/personal_access_tokens.rb b/spec/factories/personal_access_tokens.rb
index cc9b2328705..d8ff2e08657 100644
--- a/spec/factories/personal_access_tokens.rb
+++ b/spec/factories/personal_access_tokens.rb
@@ -4,19 +4,19 @@ FactoryBot.define do
factory :personal_access_token do
user
sequence(:name) { |n| "PAT #{n}" }
- revoked false
+ revoked { false }
expires_at { 5.days.from_now }
- scopes ['api']
- impersonation false
+ scopes { ['api'] }
+ impersonation { false }
after(:build) { |personal_access_token| personal_access_token.ensure_token }
trait :impersonation do
- impersonation true
+ impersonation { true }
end
trait :revoked do
- revoked true
+ revoked { true }
end
trait :expired do
@@ -24,7 +24,7 @@ FactoryBot.define do
end
trait :invalid do
- token_digest nil
+ token_digest { nil }
end
end
end
diff --git a/spec/factories/pool_repositories.rb b/spec/factories/pool_repositories.rb
index 7a8946d47b0..f0905d28c70 100644
--- a/spec/factories/pool_repositories.rb
+++ b/spec/factories/pool_repositories.rb
@@ -3,7 +3,7 @@
FactoryBot.define do
factory :pool_repository do
shard { Shard.by_name("default") }
- state :none
+ state { :none }
before(:create) do |pool|
pool.source_project = create(:project, :repository)
@@ -11,19 +11,19 @@ FactoryBot.define do
end
trait :scheduled do
- state :scheduled
+ state { :scheduled }
end
trait :failed do
- state :failed
+ state { :failed }
end
trait :obsolete do
- state :obsolete
+ state { :obsolete }
end
trait :ready do
- state :ready
+ state { :ready }
after(:create) do |pool|
pool.create_object_pool
diff --git a/spec/factories/programming_languages.rb b/spec/factories/programming_languages.rb
index ee8e7765ec9..7baa63cd4f8 100644
--- a/spec/factories/programming_languages.rb
+++ b/spec/factories/programming_languages.rb
@@ -2,7 +2,7 @@
FactoryBot.define do
factory :programming_language do
- name 'Ruby'
- color '#123456'
+ name { 'Ruby' }
+ color { '#123456' }
end
end
diff --git a/spec/factories/project_auto_devops.rb b/spec/factories/project_auto_devops.rb
index 4cc59c7095c..c87de30211c 100644
--- a/spec/factories/project_auto_devops.rb
+++ b/spec/factories/project_auto_devops.rb
@@ -3,23 +3,23 @@
FactoryBot.define do
factory :project_auto_devops do
project
- enabled true
- deploy_strategy :continuous
+ enabled { true }
+ deploy_strategy { :continuous }
trait :continuous_deployment do
- deploy_strategy ProjectAutoDevops.deploy_strategies[:continuous] # rubocop:disable FactoryBot/DynamicAttributeDefinedStatically
+ deploy_strategy { ProjectAutoDevops.deploy_strategies[:continuous] }
end
trait :manual_deployment do
- deploy_strategy ProjectAutoDevops.deploy_strategies[:manual] # rubocop:disable FactoryBot/DynamicAttributeDefinedStatically
+ deploy_strategy { ProjectAutoDevops.deploy_strategies[:manual] }
end
trait :timed_incremental_deployment do
- deploy_strategy ProjectAutoDevops.deploy_strategies[:timed_incremental] # rubocop:disable FactoryBot/DynamicAttributeDefinedStatically
+ deploy_strategy { ProjectAutoDevops.deploy_strategies[:timed_incremental] }
end
trait :disabled do
- enabled false
+ enabled { false }
end
end
end
diff --git a/spec/factories/project_daily_statistics.rb b/spec/factories/project_daily_statistics.rb
index 7e4142fa401..7aeee55c327 100644
--- a/spec/factories/project_daily_statistics.rb
+++ b/spec/factories/project_daily_statistics.rb
@@ -3,6 +3,6 @@
FactoryBot.define do
factory :project_daily_statistic do
project
- fetch_count 1
+ fetch_count { 1 }
end
end
diff --git a/spec/factories/project_error_tracking_settings.rb b/spec/factories/project_error_tracking_settings.rb
index be30bd0260a..f90a2d17846 100644
--- a/spec/factories/project_error_tracking_settings.rb
+++ b/spec/factories/project_error_tracking_settings.rb
@@ -3,10 +3,10 @@
FactoryBot.define do
factory :project_error_tracking_setting, class: ErrorTracking::ProjectErrorTrackingSetting do
project
- api_url 'https://gitlab.com/api/0/projects/sentry-org/sentry-project'
- enabled true
- token 'access_token_123'
- project_name 'Sentry Project'
- organization_name 'Sentry Org'
+ api_url { 'https://gitlab.com/api/0/projects/sentry-org/sentry-project' }
+ enabled { true }
+ token { 'access_token_123' }
+ project_name { 'Sentry Project' }
+ organization_name { 'Sentry Org' }
end
end
diff --git a/spec/factories/project_group_links.rb b/spec/factories/project_group_links.rb
index b02d167a950..3c8c7a34680 100644
--- a/spec/factories/project_group_links.rb
+++ b/spec/factories/project_group_links.rb
@@ -4,6 +4,6 @@ FactoryBot.define do
factory :project_group_link do
project
group
- expires_at nil
+ expires_at { nil }
end
end
diff --git a/spec/factories/project_hooks.rb b/spec/factories/project_hooks.rb
index 96c9742c7d0..6592141e26d 100644
--- a/spec/factories/project_hooks.rb
+++ b/spec/factories/project_hooks.rb
@@ -3,7 +3,7 @@
FactoryBot.define do
factory :project_hook do
url { generate(:url) }
- enable_ssl_verification false
+ enable_ssl_verification { false }
project
trait :token do
@@ -11,16 +11,16 @@ FactoryBot.define do
end
trait :all_events_enabled do
- push_events true
- merge_requests_events true
- tag_push_events true
- issues_events true
- confidential_issues_events true
- note_events true
- confidential_note_events true
- job_events true
- pipeline_events true
- wiki_page_events true
+ push_events { true }
+ merge_requests_events { true }
+ tag_push_events { true }
+ issues_events { true }
+ confidential_issues_events { true }
+ note_events { true }
+ confidential_note_events { true }
+ job_events { true }
+ pipeline_events { true }
+ wiki_page_events { true }
end
end
end
diff --git a/spec/factories/project_members.rb b/spec/factories/project_members.rb
index 723fa6058fe..e7004937be3 100644
--- a/spec/factories/project_members.rb
+++ b/spec/factories/project_members.rb
@@ -6,16 +6,16 @@ FactoryBot.define do
project
maintainer
- trait(:guest) { access_level ProjectMember::GUEST }
- trait(:reporter) { access_level ProjectMember::REPORTER }
- trait(:developer) { access_level ProjectMember::DEVELOPER }
- trait(:maintainer) { access_level ProjectMember::MAINTAINER }
+ trait(:guest) { access_level { ProjectMember::GUEST } }
+ trait(:reporter) { access_level { ProjectMember::REPORTER } }
+ trait(:developer) { access_level { ProjectMember::DEVELOPER } }
+ trait(:maintainer) { access_level { ProjectMember::MAINTAINER } }
trait(:access_request) { requested_at { Time.now } }
trait(:invited) do
- user_id nil
- invite_token 'xxx'
- invite_email 'email@email.com'
+ user_id { nil }
+ invite_token { 'xxx' }
+ invite_email { 'email@email.com' }
end
trait :blocked do
diff --git a/spec/factories/project_metrics_settings.rb b/spec/factories/project_metrics_settings.rb
index 234753f9b87..51b2ce0e0e9 100644
--- a/spec/factories/project_metrics_settings.rb
+++ b/spec/factories/project_metrics_settings.rb
@@ -3,6 +3,6 @@
FactoryBot.define do
factory :project_metrics_setting, class: ProjectMetricsSetting do
project
- external_dashboard_url 'https://grafana.com'
+ external_dashboard_url { 'https://grafana.com' }
end
end
diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb
index ea89555b0d5..9477eeb18d4 100644
--- a/spec/factories/projects.rb
+++ b/spec/factories/projects.rb
@@ -13,7 +13,7 @@ FactoryBot.define do
sequence(:name) { |n| "project#{n}" }
path { name.downcase.gsub(/\s/, '_') }
# Behaves differently to nil due to cache_has_external_issue_tracker
- has_external_issue_tracker false
+ has_external_issue_tracker { false }
# Associations
namespace
@@ -21,21 +21,21 @@ FactoryBot.define do
transient do
# Nest Project Feature attributes
- wiki_access_level ProjectFeature::ENABLED
- builds_access_level ProjectFeature::ENABLED
- snippets_access_level ProjectFeature::ENABLED
- issues_access_level ProjectFeature::ENABLED
- merge_requests_access_level ProjectFeature::ENABLED
- repository_access_level ProjectFeature::ENABLED
+ wiki_access_level { ProjectFeature::ENABLED }
+ builds_access_level { ProjectFeature::ENABLED }
+ snippets_access_level { ProjectFeature::ENABLED }
+ issues_access_level { ProjectFeature::ENABLED }
+ merge_requests_access_level { ProjectFeature::ENABLED }
+ repository_access_level { ProjectFeature::ENABLED }
pages_access_level do
visibility_level == Gitlab::VisibilityLevel::PUBLIC ? ProjectFeature::ENABLED : ProjectFeature::PRIVATE
end
# we can't assign the delegated `#ci_cd_settings` attributes directly, as the
# `#ci_cd_settings` relation needs to be created first
- group_runners_enabled nil
- import_status nil
- import_jid nil
+ group_runners_enabled { nil }
+ import_status { nil }
+ import_jid { nil }
end
after(:create) do |project, evaluator|
@@ -80,45 +80,45 @@ FactoryBot.define do
end
trait :public do
- visibility_level Gitlab::VisibilityLevel::PUBLIC
+ visibility_level { Gitlab::VisibilityLevel::PUBLIC }
end
trait :internal do
- visibility_level Gitlab::VisibilityLevel::INTERNAL
+ visibility_level { Gitlab::VisibilityLevel::INTERNAL }
end
trait :private do
- visibility_level Gitlab::VisibilityLevel::PRIVATE
+ visibility_level { Gitlab::VisibilityLevel::PRIVATE }
end
trait :import_scheduled do
- import_status :scheduled
+ import_status { :scheduled }
end
trait :import_started do
- import_status :started
+ import_status { :started }
end
trait :import_finished do
- import_status :finished
+ import_status { :finished }
end
trait :import_failed do
- import_status :failed
+ import_status { :failed }
end
trait :archived do
- archived true
+ archived { true }
end
- storage_version Project::LATEST_STORAGE_VERSION
+ storage_version { Project::LATEST_STORAGE_VERSION }
trait :legacy_storage do
- storage_version nil
+ storage_version { nil }
end
- trait :access_requestable do
- request_access_enabled true
+ trait :request_access_disabled do
+ request_access_enabled { false }
end
trait :with_avatar do
@@ -146,7 +146,7 @@ FactoryBot.define do
# will create a repository containing two files, and two commits, in master
trait :custom_repo do
transient do
- files {}
+ files { {} }
end
after :create do |project, evaluator|
@@ -169,7 +169,7 @@ FactoryBot.define do
test_repo
transient do
- create_templates nil
+ create_templates { nil }
end
after :create do |project, evaluator|
@@ -206,9 +206,9 @@ FactoryBot.define do
trait :remote_mirror do
transient do
- remote_name "remote_mirror_#{SecureRandom.hex}"
- url "http://foo.com"
- enabled true
+ remote_name { "remote_mirror_#{SecureRandom.hex}" }
+ url { "http://foo.com" }
+ enabled { true }
end
after(:create) do |project, evaluator|
project.remote_mirrors.create!(url: evaluator.url, enabled: evaluator.enabled)
@@ -229,7 +229,7 @@ FactoryBot.define do
end
trait :read_only do
- repository_read_only true
+ repository_read_only { true }
end
trait :broken_repo do
@@ -249,29 +249,29 @@ FactoryBot.define do
end
end
- trait(:wiki_enabled) { wiki_access_level ProjectFeature::ENABLED }
- trait(:wiki_disabled) { wiki_access_level ProjectFeature::DISABLED }
- trait(:wiki_private) { wiki_access_level ProjectFeature::PRIVATE }
- trait(:builds_enabled) { builds_access_level ProjectFeature::ENABLED }
- trait(:builds_disabled) { builds_access_level ProjectFeature::DISABLED }
- trait(:builds_private) { builds_access_level ProjectFeature::PRIVATE }
- trait(:snippets_enabled) { snippets_access_level ProjectFeature::ENABLED }
- trait(:snippets_disabled) { snippets_access_level ProjectFeature::DISABLED }
- trait(:snippets_private) { snippets_access_level ProjectFeature::PRIVATE }
- trait(:issues_disabled) { issues_access_level ProjectFeature::DISABLED }
- trait(:issues_enabled) { issues_access_level ProjectFeature::ENABLED }
- trait(:issues_private) { issues_access_level ProjectFeature::PRIVATE }
- trait(:merge_requests_enabled) { merge_requests_access_level ProjectFeature::ENABLED }
- trait(:merge_requests_disabled) { merge_requests_access_level ProjectFeature::DISABLED }
- trait(:merge_requests_private) { merge_requests_access_level ProjectFeature::PRIVATE }
- trait(:merge_requests_public) { merge_requests_access_level ProjectFeature::PUBLIC }
- trait(:repository_enabled) { repository_access_level ProjectFeature::ENABLED }
- trait(:repository_disabled) { repository_access_level ProjectFeature::DISABLED }
- trait(:repository_private) { repository_access_level ProjectFeature::PRIVATE }
- trait(:pages_public) { pages_access_level ProjectFeature::PUBLIC }
- trait(:pages_enabled) { pages_access_level ProjectFeature::ENABLED }
- trait(:pages_disabled) { pages_access_level ProjectFeature::DISABLED }
- trait(:pages_private) { pages_access_level ProjectFeature::PRIVATE }
+ trait(:wiki_enabled) { wiki_access_level { ProjectFeature::ENABLED } }
+ trait(:wiki_disabled) { wiki_access_level { ProjectFeature::DISABLED } }
+ trait(:wiki_private) { wiki_access_level { ProjectFeature::PRIVATE } }
+ trait(:builds_enabled) { builds_access_level { ProjectFeature::ENABLED } }
+ trait(:builds_disabled) { builds_access_level { ProjectFeature::DISABLED } }
+ trait(:builds_private) { builds_access_level { ProjectFeature::PRIVATE } }
+ trait(:snippets_enabled) { snippets_access_level { ProjectFeature::ENABLED } }
+ trait(:snippets_disabled) { snippets_access_level { ProjectFeature::DISABLED } }
+ trait(:snippets_private) { snippets_access_level { ProjectFeature::PRIVATE } }
+ trait(:issues_disabled) { issues_access_level { ProjectFeature::DISABLED } }
+ trait(:issues_enabled) { issues_access_level { ProjectFeature::ENABLED } }
+ trait(:issues_private) { issues_access_level { ProjectFeature::PRIVATE } }
+ trait(:merge_requests_enabled) { merge_requests_access_level { ProjectFeature::ENABLED } }
+ trait(:merge_requests_disabled) { merge_requests_access_level { ProjectFeature::DISABLED } }
+ trait(:merge_requests_private) { merge_requests_access_level { ProjectFeature::PRIVATE } }
+ trait(:merge_requests_public) { merge_requests_access_level { ProjectFeature::PUBLIC } }
+ trait(:repository_enabled) { repository_access_level { ProjectFeature::ENABLED } }
+ trait(:repository_disabled) { repository_access_level { ProjectFeature::DISABLED } }
+ trait(:repository_private) { repository_access_level { ProjectFeature::PRIVATE } }
+ trait(:pages_public) { pages_access_level { ProjectFeature::PUBLIC } }
+ trait(:pages_enabled) { pages_access_level { ProjectFeature::ENABLED } }
+ trait(:pages_disabled) { pages_access_level { ProjectFeature::DISABLED } }
+ trait(:pages_private) { pages_access_level { ProjectFeature::PRIVATE } }
trait :auto_devops do
association :auto_devops, factory: :project_auto_devops
@@ -308,19 +308,19 @@ FactoryBot.define do
end
factory :redmine_project, parent: :project do
- has_external_issue_tracker true
+ has_external_issue_tracker { true }
redmine_service
end
factory :youtrack_project, parent: :project do
- has_external_issue_tracker true
+ has_external_issue_tracker { true }
youtrack_service
end
factory :jira_project, parent: :project do
- has_external_issue_tracker true
+ has_external_issue_tracker { true }
jira_service
end
diff --git a/spec/factories/prometheus_metrics.rb b/spec/factories/prometheus_metrics.rb
index c56644bfb96..f6b58cf84c3 100644
--- a/spec/factories/prometheus_metrics.rb
+++ b/spec/factories/prometheus_metrics.rb
@@ -2,17 +2,17 @@
FactoryBot.define do
factory :prometheus_metric, class: PrometheusMetric do
- title 'title'
- query 'avg(metric)'
- y_label 'y_label'
- unit 'm/s'
- group :business
+ title { 'title' }
+ query { 'avg(metric)' }
+ y_label { 'y_label' }
+ unit { 'm/s' }
+ group { :business }
project
- legend 'legend'
+ legend { 'legend' }
trait :common do
- common true
- project nil
+ common { true }
+ project { nil }
end
end
end
diff --git a/spec/factories/protected_branches.rb b/spec/factories/protected_branches.rb
index 741615bc0d3..2d3abc77350 100644
--- a/spec/factories/protected_branches.rb
+++ b/spec/factories/protected_branches.rb
@@ -6,14 +6,14 @@ FactoryBot.define do
project
transient do
- default_push_level true
- default_merge_level true
- default_access_level true
+ default_push_level { true }
+ default_merge_level { true }
+ default_access_level { true }
end
trait :developers_can_push do
transient do
- default_push_level false
+ default_push_level { false }
end
after(:build) do |protected_branch|
@@ -23,7 +23,7 @@ FactoryBot.define do
trait :developers_can_merge do
transient do
- default_merge_level false
+ default_merge_level { false }
end
after(:build) do |protected_branch|
@@ -33,7 +33,7 @@ FactoryBot.define do
trait :no_one_can_push do
transient do
- default_push_level false
+ default_push_level { false }
end
after(:build) do |protected_branch|
@@ -43,7 +43,7 @@ FactoryBot.define do
trait :maintainers_can_push do
transient do
- default_push_level false
+ default_push_level { false }
end
after(:build) do |protected_branch|
diff --git a/spec/factories/protected_tags.rb b/spec/factories/protected_tags.rb
index 6ff2a245b58..3859267ecaf 100644
--- a/spec/factories/protected_tags.rb
+++ b/spec/factories/protected_tags.rb
@@ -6,12 +6,12 @@ FactoryBot.define do
project
transient do
- default_access_level true
+ default_access_level { true }
end
trait :developers_can_create do
transient do
- default_access_level false
+ default_access_level { false }
end
after(:build) do |protected_tag|
@@ -21,7 +21,7 @@ FactoryBot.define do
trait :no_one_can_create do
transient do
- default_access_level false
+ default_access_level { false }
end
after(:build) do |protected_tag|
@@ -31,7 +31,7 @@ FactoryBot.define do
trait :maintainers_can_create do
transient do
- default_access_level false
+ default_access_level { false }
end
after(:build) do |protected_tag|
diff --git a/spec/factories/releases.rb b/spec/factories/releases.rb
index 34794f57284..2f77bb95ea3 100644
--- a/spec/factories/releases.rb
+++ b/spec/factories/releases.rb
@@ -2,17 +2,17 @@
FactoryBot.define do
factory :release do
- tag "v1.1.0"
- sha 'b83d6e391c22777fca1ed3012fce84f633d7fed0'
+ tag { "v1.1.0" }
+ sha { 'b83d6e391c22777fca1ed3012fce84f633d7fed0' }
name { tag }
- description "Awesome release"
+ description { "Awesome release" }
project
author
released_at { Time.zone.parse('2018-10-20T18:00:00Z') }
trait :legacy do
- sha nil
- author nil
+ sha { nil }
+ author { nil }
end
end
end
diff --git a/spec/factories/remote_mirrors.rb b/spec/factories/remote_mirrors.rb
index ff1d751c86c..124c0510cab 100644
--- a/spec/factories/remote_mirrors.rb
+++ b/spec/factories/remote_mirrors.rb
@@ -3,6 +3,6 @@
FactoryBot.define do
factory :remote_mirror, class: 'RemoteMirror' do
association :project, :repository
- url "http://foo:bar@test.com"
+ url { "http://foo:bar@test.com" }
end
end
diff --git a/spec/factories/repository_languages.rb b/spec/factories/repository_languages.rb
index b2b17ebbce2..884298033ac 100644
--- a/spec/factories/repository_languages.rb
+++ b/spec/factories/repository_languages.rb
@@ -4,6 +4,6 @@ FactoryBot.define do
factory :repository_language do
project
programming_language
- share 98.5
+ share { 98.5 }
end
end
diff --git a/spec/factories/resource_label_events.rb b/spec/factories/resource_label_events.rb
index 739ba901052..b59da465fc3 100644
--- a/spec/factories/resource_label_events.rb
+++ b/spec/factories/resource_label_events.rb
@@ -2,7 +2,7 @@
FactoryBot.define do
factory :resource_label_event do
- action :add
+ action { :add }
label
user { issuable&.author || create(:user) }
diff --git a/spec/factories/services.rb b/spec/factories/services.rb
index c4cb3f07fe7..f9c77dbf87f 100644
--- a/spec/factories/services.rb
+++ b/spec/factories/services.rb
@@ -3,54 +3,58 @@
FactoryBot.define do
factory :service do
project
- type 'Service'
+ type { 'Service' }
end
factory :custom_issue_tracker_service, class: CustomIssueTrackerService do
project
- active true
+ active { true }
issue_tracker
end
factory :emails_on_push_service do
project
- type 'EmailsOnPushService'
- active true
- push_events true
- tag_push_events true
- properties(
- recipients: 'test@example.com',
- disable_diffs: true,
- send_from_committer_email: true
- )
+ type { 'EmailsOnPushService' }
+ active { true }
+ push_events { true }
+ tag_push_events { true }
+ properties do
+ {
+ recipients: 'test@example.com',
+ disable_diffs: true,
+ send_from_committer_email: true
+ }
+ end
end
factory :mock_deployment_service do
project
- type 'MockDeploymentService'
- active true
+ type { 'MockDeploymentService' }
+ active { true }
end
factory :prometheus_service do
project
- active true
- properties({
- api_url: 'https://prometheus.example.com/',
- manual_configuration: true
- })
+ active { true }
+ properties do
+ {
+ api_url: 'https://prometheus.example.com/',
+ manual_configuration: true
+ }
+ end
end
factory :jira_service do
project
- active true
+ active { true }
transient do
- create_data true
- url 'https://jira.example.com'
- api_url nil
- username 'jira_username'
- password 'jira_password'
- jira_issue_transition_id '56-1'
+ create_data { true }
+ url { 'https://jira.example.com' }
+ api_url { nil }
+ username { 'jira_username' }
+ password { 'jira_password' }
+ jira_issue_transition_id { '56-1' }
end
after(:build) do |service, evaluator|
@@ -65,34 +69,34 @@ FactoryBot.define do
factory :bugzilla_service do
project
- active true
+ active { true }
issue_tracker
end
factory :redmine_service do
project
- active true
+ active { true }
issue_tracker
end
factory :youtrack_service do
project
- active true
+ active { true }
issue_tracker
end
factory :gitlab_issue_tracker_service do
project
- active true
+ active { true }
issue_tracker
end
trait :issue_tracker do
transient do
- create_data true
- project_url 'http://issuetracker.example.com'
- issues_url 'http://issues.example.com/issues/:id'
- new_issue_url 'http://new-issue.example.com'
+ create_data { true }
+ project_url { 'http://issuetracker.example.com' }
+ issues_url { 'http://issues.example.com/issues/:id' }
+ new_issue_url { 'http://new-issue.example.com' }
end
after(:build) do |service, evaluator|
@@ -105,29 +109,29 @@ FactoryBot.define do
end
trait :jira_cloud_service do
- url 'https://mysite.atlassian.net'
- username 'jira_user'
- password 'my-secret-password'
+ url { 'https://mysite.atlassian.net' }
+ username { 'jira_user' }
+ password { 'my-secret-password' }
end
factory :hipchat_service do
project
- type 'HipchatService'
- token 'test_token'
+ type { 'HipchatService' }
+ token { 'test_token' }
end
# this is for testing storing values inside properties, which is deprecated and will be removed in
- # https://gitlab.com/gitlab-org/gitlab-ce/issues/63084
+ # https://gitlab.com/gitlab-org/gitlab/issues/29404
trait :without_properties_callback do
- jira_tracker_data nil
- issue_tracker_data nil
- create_data false
+ jira_tracker_data { nil }
+ issue_tracker_data { nil }
+ create_data { false }
after(:build) do |service|
IssueTrackerService.skip_callback(:validation, :before, :handle_properties)
end
- to_create { |instance| instance.save(validate: false)}
+ to_create { |instance| instance.save(validate: false) }
after(:create) do
IssueTrackerService.set_callback(:validation, :before, :handle_properties)
diff --git a/spec/factories/shards.rb b/spec/factories/shards.rb
index c30a38180e8..357d3bfbfa1 100644
--- a/spec/factories/shards.rb
+++ b/spec/factories/shards.rb
@@ -2,6 +2,6 @@
FactoryBot.define do
factory :shard do
- name "default"
+ name { "default" }
end
end
diff --git a/spec/factories/snippets.rb b/spec/factories/snippets.rb
index 9c3a0fbe9b3..ede071ae70c 100644
--- a/spec/factories/snippets.rb
+++ b/spec/factories/snippets.rb
@@ -9,15 +9,15 @@ FactoryBot.define do
file_name { generate(:filename) }
trait :public do
- visibility_level Snippet::PUBLIC
+ visibility_level { Snippet::PUBLIC }
end
trait :internal do
- visibility_level Snippet::INTERNAL
+ visibility_level { Snippet::INTERNAL }
end
trait :private do
- visibility_level Snippet::PRIVATE
+ visibility_level { Snippet::PRIVATE }
end
end
diff --git a/spec/factories/spam_logs.rb b/spec/factories/spam_logs.rb
index 42a856832e7..ec4454cac57 100644
--- a/spec/factories/spam_logs.rb
+++ b/spec/factories/spam_logs.rb
@@ -4,7 +4,7 @@ FactoryBot.define do
factory :spam_log do
user
sequence(:source_ip) { |n| "42.42.42.#{n % 255}" }
- noteable_type 'Issue'
+ noteable_type { 'Issue' }
sequence(:title) { |n| "Spam title #{n}" }
description { "Spam description\nwith\nmultiple\nlines" }
end
diff --git a/spec/factories/suggestions.rb b/spec/factories/suggestions.rb
index b1427e0211f..420c59df40e 100644
--- a/spec/factories/suggestions.rb
+++ b/spec/factories/suggestions.rb
@@ -2,18 +2,18 @@
FactoryBot.define do
factory :suggestion do
- relative_order 0
+ relative_order { 0 }
association :note, factory: :diff_note_on_merge_request
- from_content " vars = {\n"
- to_content " vars = [\n"
+ from_content { " vars = {\n" }
+ to_content { " vars = [\n" }
trait :unappliable do
- from_content "foo"
- to_content "foo"
+ from_content { "foo" }
+ to_content { "foo" }
end
trait :applied do
- applied true
+ applied { true }
commit_id { RepoHelpers.sample_commit.id }
end
diff --git a/spec/factories/system_note_metadata.rb b/spec/factories/system_note_metadata.rb
index 8cd4b77799c..56941edba1f 100644
--- a/spec/factories/system_note_metadata.rb
+++ b/spec/factories/system_note_metadata.rb
@@ -3,6 +3,6 @@
FactoryBot.define do
factory :system_note_metadata do
note
- action 'merge'
+ action { 'merge' }
end
end
diff --git a/spec/factories/term_agreements.rb b/spec/factories/term_agreements.rb
index b7e259bd44b..150948df044 100644
--- a/spec/factories/term_agreements.rb
+++ b/spec/factories/term_agreements.rb
@@ -7,10 +7,10 @@ FactoryBot.define do
end
trait :declined do
- accepted false
+ accepted { false }
end
trait :accepted do
- accepted true
+ accepted { true }
end
end
diff --git a/spec/factories/terms.rb b/spec/factories/terms.rb
index b890261d293..b98a2453f7e 100644
--- a/spec/factories/terms.rb
+++ b/spec/factories/terms.rb
@@ -2,6 +2,6 @@
FactoryBot.define do
factory :term, class: ApplicationSetting::Term do
- terms "Lorem ipsum dolor sit amet, consectetur adipiscing elit."
+ terms { "Lorem ipsum dolor sit amet, consectetur adipiscing elit." }
end
end
diff --git a/spec/factories/timelogs.rb b/spec/factories/timelogs.rb
index 056a8833c46..5d34acc635d 100644
--- a/spec/factories/timelogs.rb
+++ b/spec/factories/timelogs.rb
@@ -4,7 +4,7 @@
FactoryBot.define do
factory :timelog do
- time_spent 3600
+ time_spent { 3600 }
issue
user { issue.project.creator }
end
diff --git a/spec/factories/todos.rb b/spec/factories/todos.rb
index 2ff024112a4..bb91fc9ac8e 100644
--- a/spec/factories/todos.rb
+++ b/spec/factories/todos.rb
@@ -38,11 +38,11 @@ FactoryBot.define do
end
trait :pending do
- state :pending
+ state { :pending }
end
trait :done do
- state :done
+ state { :done }
end
end
@@ -52,6 +52,6 @@ FactoryBot.define do
user
action { Todo::ASSIGNED }
commit_id { RepoHelpers.sample_commit.id }
- target_type "Commit"
+ target_type { "Commit" }
end
end
diff --git a/spec/factories/u2f_registrations.rb b/spec/factories/u2f_registrations.rb
index c968468834b..7017b0ee9e7 100644
--- a/spec/factories/u2f_registrations.rb
+++ b/spec/factories/u2f_registrations.rb
@@ -5,6 +5,6 @@ FactoryBot.define do
certificate { FFaker::BaconIpsum.characters(728) }
key_handle { FFaker::BaconIpsum.characters(86) }
public_key { FFaker::BaconIpsum.characters(88) }
- counter 0
+ counter { 0 }
end
end
diff --git a/spec/factories/uploads.rb b/spec/factories/uploads.rb
index 3f6326114c9..a060cd7d6f8 100644
--- a/spec/factories/uploads.rb
+++ b/spec/factories/uploads.rb
@@ -2,32 +2,36 @@
FactoryBot.define do
factory :upload do
- model { build(:project) }
+ model { create(:project) }
size { 100.kilobytes }
- uploader "AvatarUploader"
- mount_point :avatar
- secret nil
- store ObjectStorage::Store::LOCAL
+ uploader { "AvatarUploader" }
+ mount_point { :avatar }
+ secret { nil }
+ store { ObjectStorage::Store::LOCAL }
# we should build a mount agnostic upload by default
transient do
- filename 'myfile.jpg'
+ filename { 'avatar.jpg' }
end
- # this needs to comply with RecordsUpload::Concern#upload_path
- path { File.join("uploads/-/system", model.class.underscore, mount_point.to_s, 'avatar.jpg') }
+ path do
+ uploader_instance = Object.const_get(uploader.to_s, false).new(model, mount_point)
+ File.join(uploader_instance.store_dir, filename)
+ end
trait :personal_snippet_upload do
- uploader "PersonalFileUploader"
+ model { create(:personal_snippet) }
path { File.join(secret, filename) }
- model { build(:personal_snippet) }
+ uploader { "PersonalFileUploader" }
secret { SecureRandom.hex }
+ mount_point { nil }
end
trait :issuable_upload do
- uploader "FileUploader"
+ uploader { "FileUploader" }
path { File.join(secret, filename) }
secret { SecureRandom.hex }
+ mount_point { nil }
end
trait :with_file do
@@ -38,27 +42,28 @@ FactoryBot.define do
end
trait :object_storage do
- store ObjectStorage::Store::REMOTE
+ store { ObjectStorage::Store::REMOTE }
end
trait :namespace_upload do
- model { build(:group) }
+ model { create(:group) }
path { File.join(secret, filename) }
- uploader "NamespaceFileUploader"
+ uploader { "NamespaceFileUploader" }
secret { SecureRandom.hex }
+ mount_point { nil }
end
trait :favicon_upload do
- model { build(:appearance) }
- path { File.join(secret, filename) }
- uploader "FaviconUploader"
+ model { create(:appearance) }
+ uploader { "FaviconUploader" }
secret { SecureRandom.hex }
+ mount_point { :favicon }
end
trait :attachment_upload do
- mount_point :attachment
- model { build(:note) }
- uploader "AttachmentUploader"
+ mount_point { :attachment }
+ model { create(:note) }
+ uploader { "AttachmentUploader" }
end
end
end
diff --git a/spec/factories/user_agent_details.rb b/spec/factories/user_agent_details.rb
index 055aea50585..3185ded26ce 100644
--- a/spec/factories/user_agent_details.rb
+++ b/spec/factories/user_agent_details.rb
@@ -2,8 +2,8 @@
FactoryBot.define do
factory :user_agent_detail do
- ip_address '127.0.0.1'
- user_agent 'AppleWebKit/537.36'
+ ip_address { '127.0.0.1' }
+ user_agent { 'AppleWebKit/537.36' }
association :subject, factory: :issue
end
end
diff --git a/spec/factories/user_callouts.rb b/spec/factories/user_callouts.rb
index c4a217fd357..cedc6efd8d7 100644
--- a/spec/factories/user_callouts.rb
+++ b/spec/factories/user_callouts.rb
@@ -2,7 +2,7 @@
FactoryBot.define do
factory :user_callout do
- feature_name :gke_cluster_integration
+ feature_name { :gke_cluster_integration }
user
end
diff --git a/spec/factories/user_statuses.rb b/spec/factories/user_statuses.rb
index 9998ae9609c..dbed6031ce1 100644
--- a/spec/factories/user_statuses.rb
+++ b/spec/factories/user_statuses.rb
@@ -3,7 +3,7 @@
FactoryBot.define do
factory :user_status do
user
- emoji 'coffee'
- message 'I crave coffee'
+ emoji { 'coffee' }
+ message { 'I crave coffee' }
end
end
diff --git a/spec/factories/users.rb b/spec/factories/users.rb
index 57e58513529..f83c137b758 100644
--- a/spec/factories/users.rb
+++ b/spec/factories/users.rb
@@ -5,17 +5,18 @@ FactoryBot.define do
email { generate(:email) }
name { generate(:name) }
username { generate(:username) }
- password "12345678"
+ password { "12345678" }
+ role { 'software_developer' }
confirmed_at { Time.now }
confirmation_token { nil }
- can_create_group true
+ can_create_group { true }
after(:stub) do |user|
user.notification_email = user.email
end
trait :admin do
- admin true
+ admin { true }
end
trait :blocked do
@@ -23,7 +24,7 @@ FactoryBot.define do
end
trait :external do
- external true
+ external { true }
end
trait :two_factor do
@@ -31,7 +32,7 @@ FactoryBot.define do
end
trait :ghost do
- ghost true
+ ghost { true }
after(:build) { |user, _| user.block! }
end
@@ -40,11 +41,11 @@ FactoryBot.define do
end
trait :with_sign_ins do
- sign_in_count 3
+ sign_in_count { 3 }
current_sign_in_at { Time.now }
last_sign_in_at { FFaker::Time.between(10.days.ago, 1.day.ago) }
- current_sign_in_ip '127.0.0.1'
- last_sign_in_ip '127.0.0.1'
+ current_sign_in_ip { '127.0.0.1' }
+ last_sign_in_ip { '127.0.0.1' }
end
trait :two_factor_via_otp do
@@ -57,7 +58,7 @@ FactoryBot.define do
end
trait :two_factor_via_u2f do
- transient { registrations_count 5 }
+ transient { registrations_count { 5 } }
after(:create) do |user, evaluator|
create_list(:u2f_registration, evaluator.registrations_count, user: user)
@@ -65,7 +66,7 @@ FactoryBot.define do
end
trait :readme do
- project_view :readme
+ project_view { :readme }
end
trait :commit_email do
@@ -77,7 +78,7 @@ FactoryBot.define do
end
transient do
- developer_projects []
+ developer_projects { [] }
end
after(:create) do |user, evaluator|
@@ -88,8 +89,8 @@ FactoryBot.define do
factory :omniauth_user do
transient do
- extern_uid '123456'
- provider 'ldapmain'
+ extern_uid { '123456' }
+ provider { 'ldapmain' }
end
after(:create) do |user, evaluator|
diff --git a/spec/factories/web_hook_log.rb b/spec/factories/web_hook_log.rb
index 5750af85662..65b91b7183a 100644
--- a/spec/factories/web_hook_log.rb
+++ b/spec/factories/web_hook_log.rb
@@ -3,14 +3,20 @@
FactoryBot.define do
factory :web_hook_log do
web_hook factory: :project_hook
- trigger 'push_hooks'
+ trigger { 'push_hooks' }
url { generate(:url) }
- request_headers {}
- request_data {}
- response_headers {}
- response_body ''
- response_status '200'
- execution_duration 2.0
- internal_error_message nil
+ request_headers do
+ {}
+ end
+ request_data do
+ {}
+ end
+ response_headers do
+ {}
+ end
+ response_body { '' }
+ response_status { '200' }
+ execution_duration { 2.0 }
+ internal_error_message { nil }
end
end
diff --git a/spec/factories/wiki_directories.rb b/spec/factories/wiki_directories.rb
index de23cf110b5..afa003617c4 100644
--- a/spec/factories/wiki_directories.rb
+++ b/spec/factories/wiki_directories.rb
@@ -4,7 +4,7 @@ FactoryBot.define do
factory :wiki_directory do
skip_create
- slug '/path_up_to/dir'
+ slug { '/path_up_to/dir' }
initialize_with { new(slug) }
end
end
diff --git a/spec/factories_spec.rb b/spec/factories_spec.rb
index 66b71d0f556..aa72a116be2 100644
--- a/spec/factories_spec.rb
+++ b/spec/factories_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe 'factories' do
diff --git a/spec/fast_spec_helper.rb b/spec/fast_spec_helper.rb
index f5a487b4d57..1a8af335244 100644
--- a/spec/fast_spec_helper.rb
+++ b/spec/fast_spec_helper.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'bundler/setup'
ENV['GITLAB_ENV'] = 'test'
diff --git a/spec/features/admin/admin_settings_spec.rb b/spec/features/admin/admin_settings_spec.rb
index ab58d9fb9d5..e1c9364067a 100644
--- a/spec/features/admin/admin_settings_spec.rb
+++ b/spec/features/admin/admin_settings_spec.rb
@@ -2,450 +2,529 @@
require 'spec_helper'
-describe 'Admin updates settings' do
+describe 'Admin updates settings', :clean_gitlab_redis_shared_state, :do_not_mock_admin_mode do
include StubENV
include TermsHelper
let(:admin) { create(:admin) }
- before do
- stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false')
- sign_in(admin)
- end
-
- context 'General page' do
+ context 'feature flag :user_mode_in_session is enabled' do
before do
- visit general_admin_application_settings_path
+ stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false')
+ sign_in(admin)
+ gitlab_enable_admin_mode_sign_in(admin)
end
- it 'Change visibility settings' do
- page.within('.as-visibility-access') do
- choose "application_setting_default_project_visibility_20"
- click_button 'Save changes'
+ context 'General page' do
+ before do
+ visit general_admin_application_settings_path
end
- expect(page).to have_content "Application settings saved successfully"
- end
+ it 'Change visibility settings' do
+ page.within('.as-visibility-access') do
+ choose "application_setting_default_project_visibility_20"
+ click_button 'Save changes'
+ end
- it 'Uncheck all restricted visibility levels' do
- page.within('.as-visibility-access') do
- find('#application_setting_visibility_level_0').set(false)
- find('#application_setting_visibility_level_10').set(false)
- find('#application_setting_visibility_level_20').set(false)
- click_button 'Save changes'
+ expect(page).to have_content "Application settings saved successfully"
end
- expect(page).to have_content "Application settings saved successfully"
- expect(find('#application_setting_visibility_level_0')).not_to be_checked
- expect(find('#application_setting_visibility_level_10')).not_to be_checked
- expect(find('#application_setting_visibility_level_20')).not_to be_checked
- end
+ it 'Uncheck all restricted visibility levels' do
+ page.within('.as-visibility-access') do
+ find('#application_setting_visibility_level_0').set(false)
+ find('#application_setting_visibility_level_10').set(false)
+ find('#application_setting_visibility_level_20').set(false)
+ click_button 'Save changes'
+ end
+
+ expect(page).to have_content "Application settings saved successfully"
+ expect(find('#application_setting_visibility_level_0')).not_to be_checked
+ expect(find('#application_setting_visibility_level_10')).not_to be_checked
+ expect(find('#application_setting_visibility_level_20')).not_to be_checked
+ end
- it 'Modify import sources' do
- expect(current_settings.import_sources).not_to be_empty
+ it 'Modify import sources' do
+ expect(current_settings.import_sources).not_to be_empty
- page.within('.as-visibility-access') do
- Gitlab::ImportSources.options.map do |name, _|
- uncheck name
+ page.within('.as-visibility-access') do
+ Gitlab::ImportSources.options.map do |name, _|
+ uncheck name
+ end
+
+ click_button 'Save changes'
end
- click_button 'Save changes'
- end
+ expect(page).to have_content "Application settings saved successfully"
+ expect(current_settings.import_sources).to be_empty
- expect(page).to have_content "Application settings saved successfully"
- expect(current_settings.import_sources).to be_empty
+ page.within('.as-visibility-access') do
+ check "Repo by URL"
+ click_button 'Save changes'
+ end
- page.within('.as-visibility-access') do
- check "Repo by URL"
- click_button 'Save changes'
+ expect(page).to have_content "Application settings saved successfully"
+ expect(current_settings.import_sources).to eq(['git'])
end
- expect(page).to have_content "Application settings saved successfully"
- expect(current_settings.import_sources).to eq(['git'])
- end
+ it 'Change Visibility and Access Controls' do
+ page.within('.as-visibility-access') do
+ uncheck 'Project export enabled'
+ click_button 'Save changes'
+ end
- it 'Change Visibility and Access Controls' do
- page.within('.as-visibility-access') do
- uncheck 'Project export enabled'
- click_button 'Save changes'
+ expect(current_settings.project_export_enabled).to be_falsey
+ expect(page).to have_content "Application settings saved successfully"
end
- expect(current_settings.project_export_enabled).to be_falsey
- expect(page).to have_content "Application settings saved successfully"
- end
+ it 'Change Keys settings' do
+ page.within('.as-visibility-access') do
+ select 'Are forbidden', from: 'RSA SSH keys'
+ select 'Are allowed', from: 'DSA SSH keys'
+ select 'Must be at least 384 bits', from: 'ECDSA SSH keys'
+ select 'Are forbidden', from: 'ED25519 SSH keys'
+ click_on 'Save changes'
+ end
- it 'Change Keys settings' do
- page.within('.as-visibility-access') do
- select 'Are forbidden', from: 'RSA SSH keys'
- select 'Are allowed', from: 'DSA SSH keys'
- select 'Must be at least 384 bits', from: 'ECDSA SSH keys'
- select 'Are forbidden', from: 'ED25519 SSH keys'
- click_on 'Save changes'
- end
+ forbidden = ApplicationSetting::FORBIDDEN_KEY_VALUE.to_s
- forbidden = ApplicationSetting::FORBIDDEN_KEY_VALUE.to_s
+ expect(page).to have_content 'Application settings saved successfully'
+ expect(find_field('RSA SSH keys').value).to eq(forbidden)
+ expect(find_field('DSA SSH keys').value).to eq('0')
+ expect(find_field('ECDSA SSH keys').value).to eq('384')
+ expect(find_field('ED25519 SSH keys').value).to eq(forbidden)
+ end
- expect(page).to have_content 'Application settings saved successfully'
- expect(find_field('RSA SSH keys').value).to eq(forbidden)
- expect(find_field('DSA SSH keys').value).to eq('0')
- expect(find_field('ECDSA SSH keys').value).to eq('384')
- expect(find_field('ED25519 SSH keys').value).to eq(forbidden)
- end
+ it 'Change Account and Limit Settings' do
+ page.within('.as-account-limit') do
+ uncheck 'Gravatar enabled'
+ click_button 'Save changes'
+ end
- it 'Change Account and Limit Settings' do
- page.within('.as-account-limit') do
- uncheck 'Gravatar enabled'
- click_button 'Save changes'
+ expect(current_settings.gravatar_enabled).to be_falsey
+ expect(page).to have_content "Application settings saved successfully"
end
- expect(current_settings.gravatar_enabled).to be_falsey
- expect(page).to have_content "Application settings saved successfully"
- end
+ it 'Change New users set to external', :js do
+ user_internal_regex = find('#application_setting_user_default_internal_regex', visible: :all)
- it 'Change New users set to external', :js do
- user_internal_regex = find('#application_setting_user_default_internal_regex', visible: :all)
+ expect(user_internal_regex).to be_readonly
+ expect(user_internal_regex['placeholder']).to eq 'To define internal users, first enable new users set to external'
- expect(user_internal_regex).to be_readonly
- expect(user_internal_regex['placeholder']).to eq 'To define internal users, first enable new users set to external'
+ check 'application_setting_user_default_external'
- check 'application_setting_user_default_external'
+ expect(user_internal_regex).not_to be_readonly
+ expect(user_internal_regex['placeholder']).to eq 'Regex pattern'
+ end
- expect(user_internal_regex).not_to be_readonly
- expect(user_internal_regex['placeholder']).to eq 'Regex pattern'
- end
+ it 'Change Sign-in restrictions' do
+ page.within('.as-signin') do
+ fill_in 'Home page URL', with: 'https://about.gitlab.com/'
+ click_button 'Save changes'
+ end
- it 'Change Sign-in restrictions' do
- page.within('.as-signin') do
- fill_in 'Home page URL', with: 'https://about.gitlab.com/'
- click_button 'Save changes'
+ expect(current_settings.home_page_url).to eq "https://about.gitlab.com/"
+ expect(page).to have_content "Application settings saved successfully"
end
- expect(current_settings.home_page_url).to eq "https://about.gitlab.com/"
- expect(page).to have_content "Application settings saved successfully"
- end
+ it 'Terms of Service' do
+ # Already have the admin accept terms, so they don't need to accept in this spec.
+ _existing_terms = create(:term)
+ accept_terms(admin)
- it 'Terms of Service' do
- # Already have the admin accept terms, so they don't need to accept in this spec.
- _existing_terms = create(:term)
- accept_terms(admin)
+ page.within('.as-terms') do
+ check 'Require all users to accept Terms of Service and Privacy Policy when they access GitLab.'
+ fill_in 'Terms of Service Agreement', with: 'Be nice!'
+ click_button 'Save changes'
+ end
- page.within('.as-terms') do
- check 'Require all users to accept Terms of Service and Privacy Policy when they access GitLab.'
- fill_in 'Terms of Service Agreement', with: 'Be nice!'
- click_button 'Save changes'
+ expect(current_settings.enforce_terms).to be(true)
+ expect(current_settings.terms).to eq 'Be nice!'
+ expect(page).to have_content 'Application settings saved successfully'
end
- expect(current_settings.enforce_terms).to be(true)
- expect(current_settings.terms).to eq 'Be nice!'
- expect(page).to have_content 'Application settings saved successfully'
- end
+ it 'Modify oauth providers' do
+ expect(current_settings.disabled_oauth_sign_in_sources).to be_empty
- it 'Modify oauth providers' do
- expect(current_settings.disabled_oauth_sign_in_sources).to be_empty
+ page.within('.as-signin') do
+ uncheck 'Google'
+ click_button 'Save changes'
+ end
- page.within('.as-signin') do
- uncheck 'Google'
- click_button 'Save changes'
- end
+ expect(page).to have_content "Application settings saved successfully"
+ expect(current_settings.disabled_oauth_sign_in_sources).to include('google_oauth2')
- expect(page).to have_content "Application settings saved successfully"
- expect(current_settings.disabled_oauth_sign_in_sources).to include('google_oauth2')
+ page.within('.as-signin') do
+ check "Google"
+ click_button 'Save changes'
+ end
- page.within('.as-signin') do
- check "Google"
- click_button 'Save changes'
+ expect(page).to have_content "Application settings saved successfully"
+ expect(current_settings.disabled_oauth_sign_in_sources).not_to include('google_oauth2')
end
- expect(page).to have_content "Application settings saved successfully"
- expect(current_settings.disabled_oauth_sign_in_sources).not_to include('google_oauth2')
- end
+ it 'Oauth providers do not raise validation errors when saving unrelated changes' do
+ expect(current_settings.disabled_oauth_sign_in_sources).to be_empty
- it 'Oauth providers do not raise validation errors when saving unrelated changes' do
- expect(current_settings.disabled_oauth_sign_in_sources).to be_empty
+ page.within('.as-signin') do
+ uncheck 'Google'
+ click_button 'Save changes'
+ end
- page.within('.as-signin') do
- uncheck 'Google'
- click_button 'Save changes'
- end
+ expect(page).to have_content "Application settings saved successfully"
+ expect(current_settings.disabled_oauth_sign_in_sources).to include('google_oauth2')
- expect(page).to have_content "Application settings saved successfully"
- expect(current_settings.disabled_oauth_sign_in_sources).to include('google_oauth2')
+ # Remove google_oauth2 from the Omniauth strategies
+ allow(Devise).to receive(:omniauth_providers).and_return([])
- # Remove google_oauth2 from the Omniauth strategies
- allow(Devise).to receive(:omniauth_providers).and_return([])
+ # Save an unrelated setting
+ page.within('.as-terms') do
+ click_button 'Save changes'
+ end
- # Save an unrelated setting
- page.within('.as-terms') do
- click_button 'Save changes'
+ expect(page).to have_content "Application settings saved successfully"
+ expect(current_settings.disabled_oauth_sign_in_sources).to include('google_oauth2')
end
- expect(page).to have_content "Application settings saved successfully"
- expect(current_settings.disabled_oauth_sign_in_sources).to include('google_oauth2')
+ it 'Configure web terminal' do
+ page.within('.as-terminal') do
+ fill_in 'Max session time', with: 15
+ click_button 'Save changes'
+ end
+
+ expect(page).to have_content "Application settings saved successfully"
+ expect(current_settings.terminal_max_session_time).to eq(15)
+ end
end
- it 'Configure web terminal' do
- page.within('.as-terminal') do
- fill_in 'Max session time', with: 15
- click_button 'Save changes'
+ context 'Integrations page' do
+ before do
+ visit integrations_admin_application_settings_path
end
- expect(page).to have_content "Application settings saved successfully"
- expect(current_settings.terminal_max_session_time).to eq(15)
- end
- end
+ it 'Enable hiding third party offers' do
+ page.within('.as-third-party-offers') do
+ check 'Do not display offers from third parties within GitLab'
+ click_button 'Save changes'
+ end
- context 'Integrations page' do
- before do
- visit integrations_admin_application_settings_path
- end
+ expect(page).to have_content "Application settings saved successfully"
+ expect(current_settings.hide_third_party_offers).to be true
+ end
+
+ it 'Change Slack Notifications Service template settings' do
+ first(:link, 'Service Templates').click
+ click_link 'Slack notifications'
+ fill_in 'Webhook', with: 'http://localhost'
+ fill_in 'Username', with: 'test_user'
+ fill_in 'service_push_channel', with: '#test_channel'
+ page.check('Notify only broken pipelines')
+ page.select 'All branches', from: 'Branches to be notified'
+
+ check_all_events
+ click_on 'Save'
- it 'Enable hiding third party offers' do
- page.within('.as-third-party-offers') do
- check 'Do not display offers from third parties within GitLab'
- click_button 'Save changes'
+ expect(page).to have_content 'Application settings saved successfully'
+
+ click_link 'Slack notifications'
+
+ page.all('input[type=checkbox]').each do |checkbox|
+ expect(checkbox).to be_checked
+ end
+ expect(find_field('Webhook').value).to eq 'http://localhost'
+ expect(find_field('Username').value).to eq 'test_user'
+ expect(find('#service_push_channel').value).to eq '#test_channel'
end
- expect(page).to have_content "Application settings saved successfully"
- expect(current_settings.hide_third_party_offers).to be true
+ it 'defaults Deployment events to false for chat notification template settings' do
+ first(:link, 'Service Templates').click
+ click_link 'Slack notifications'
+
+ expect(find_field('Deployment')).not_to be_checked
+ end
end
- it 'Change Slack Notifications Service template settings' do
- first(:link, 'Service Templates').click
- click_link 'Slack notifications'
- fill_in 'Webhook', with: 'http://localhost'
- fill_in 'Username', with: 'test_user'
- fill_in 'service_push_channel', with: '#test_channel'
- page.check('Notify only broken pipelines')
- page.select 'All branches', from: 'Branches to be notified'
+ context 'CI/CD page' do
+ it 'Change CI/CD settings' do
+ visit ci_cd_admin_application_settings_path
+
+ page.within('.as-ci-cd') do
+ check 'Default to Auto DevOps pipeline for all projects'
+ fill_in 'application_setting_auto_devops_domain', with: 'domain.com'
+ click_button 'Save changes'
+ end
- check_all_events
- click_on 'Save'
+ expect(current_settings.auto_devops_enabled?).to be true
+ expect(current_settings.auto_devops_domain).to eq('domain.com')
+ expect(page).to have_content "Application settings saved successfully"
+ end
+ end
- expect(page).to have_content 'Application settings saved successfully'
+ context 'Reporting page' do
+ it 'Change Spam settings' do
+ visit reporting_admin_application_settings_path
- click_link 'Slack notifications'
+ page.within('.as-spam') do
+ check 'Enable reCAPTCHA'
+ check 'Enable reCAPTCHA for login'
+ fill_in 'reCAPTCHA Site Key', with: 'key'
+ fill_in 'reCAPTCHA Private Key', with: 'key'
+ fill_in 'IPs per user', with: 15
+ click_button 'Save changes'
+ end
- page.all('input[type=checkbox]').each do |checkbox|
- expect(checkbox).to be_checked
+ expect(page).to have_content "Application settings saved successfully"
+ expect(current_settings.recaptcha_enabled).to be true
+ expect(current_settings.login_recaptcha_protection_enabled).to be true
+ expect(current_settings.unique_ips_limit_per_user).to eq(15)
end
- expect(find_field('Webhook').value).to eq 'http://localhost'
- expect(find_field('Username').value).to eq 'test_user'
- expect(find('#service_push_channel').value).to eq '#test_channel'
end
- it 'defaults Deployment events to false for chat notification template settings' do
- first(:link, 'Service Templates').click
- click_link 'Slack notifications'
+ context 'Metrics and profiling page' do
+ before do
+ visit metrics_and_profiling_admin_application_settings_path
+ end
- expect(find_field('Deployment')).not_to be_checked
- end
- end
+ it 'Change Influx settings' do
+ page.within('.as-influx') do
+ check 'Enable InfluxDB Metrics'
+ click_button 'Save changes'
+ end
- context 'CI/CD page' do
- it 'Change CI/CD settings' do
- visit ci_cd_admin_application_settings_path
+ expect(current_settings.metrics_enabled?).to be true
+ expect(page).to have_content "Application settings saved successfully"
+ end
+
+ it 'Change Prometheus settings' do
+ page.within('.as-prometheus') do
+ check 'Enable Prometheus Metrics'
+ click_button 'Save changes'
+ end
- page.within('.as-ci-cd') do
- check 'Default to Auto DevOps pipeline for all projects'
- fill_in 'application_setting_auto_devops_domain', with: 'domain.com'
- click_button 'Save changes'
+ expect(current_settings.prometheus_metrics_enabled?).to be true
+ expect(page).to have_content "Application settings saved successfully"
end
- expect(current_settings.auto_devops_enabled?).to be true
- expect(current_settings.auto_devops_domain).to eq('domain.com')
- expect(page).to have_content "Application settings saved successfully"
- end
- end
+ it 'Change Performance bar settings' do
+ group = create(:group)
+
+ page.within('.as-performance-bar') do
+ check 'Enable access to the Performance Bar'
+ fill_in 'Allowed group', with: group.path
+ click_on 'Save changes'
+ end
- context 'Reporting page' do
- it 'Change Spam settings' do
- visit reporting_admin_application_settings_path
+ expect(page).to have_content "Application settings saved successfully"
+ expect(find_field('Enable access to the Performance Bar')).to be_checked
+ expect(find_field('Allowed group').value).to eq group.path
- page.within('.as-spam') do
- check 'Enable reCAPTCHA'
- check 'Enable reCAPTCHA for login'
- fill_in 'reCAPTCHA Site Key', with: 'key'
- fill_in 'reCAPTCHA Private Key', with: 'key'
- fill_in 'IPs per user', with: 15
- click_button 'Save changes'
+ page.within('.as-performance-bar') do
+ uncheck 'Enable access to the Performance Bar'
+ click_on 'Save changes'
+ end
+
+ expect(page).to have_content 'Application settings saved successfully'
+ expect(find_field('Enable access to the Performance Bar')).not_to be_checked
+ expect(find_field('Allowed group').value).to be_nil
end
- expect(page).to have_content "Application settings saved successfully"
- expect(current_settings.recaptcha_enabled).to be true
- expect(current_settings.login_recaptcha_protection_enabled).to be true
- expect(current_settings.unique_ips_limit_per_user).to eq(15)
- end
- end
+ it 'loads usage ping payload on click', :js do
+ expect(page).to have_button 'Preview payload'
- context 'Metrics and profiling page' do
- before do
- visit metrics_and_profiling_admin_application_settings_path
- end
+ find('.js-usage-ping-payload-trigger').click
- it 'Change Influx settings' do
- page.within('.as-influx') do
- check 'Enable InfluxDB Metrics'
- click_button 'Save changes'
+ expect(page).to have_selector '.js-usage-ping-payload'
+ expect(page).to have_button 'Hide payload'
end
+ end
+
+ context 'Network page' do
+ it 'Changes Outbound requests settings' do
+ visit network_admin_application_settings_path
- expect(current_settings.metrics_enabled?).to be true
- expect(page).to have_content "Application settings saved successfully"
+ page.within('.as-outbound') do
+ check 'Allow requests to the local network from web hooks and services'
+ # Enabled by default
+ uncheck 'Allow requests to the local network from system hooks'
+ # Enabled by default
+ uncheck 'Enforce DNS rebinding attack protection'
+ click_button 'Save changes'
+ end
+
+ expect(page).to have_content "Application settings saved successfully"
+ expect(current_settings.allow_local_requests_from_web_hooks_and_services).to be true
+ expect(current_settings.allow_local_requests_from_system_hooks).to be false
+ expect(current_settings.dns_rebinding_protection_enabled).to be false
+ end
end
- it 'Change Prometheus settings' do
- page.within('.as-prometheus') do
- check 'Enable Prometheus Metrics'
- click_button 'Save changes'
+ context 'Preferences page' do
+ before do
+ visit preferences_admin_application_settings_path
end
- expect(current_settings.prometheus_metrics_enabled?).to be true
- expect(page).to have_content "Application settings saved successfully"
- end
+ it 'Change Help page' do
+ new_support_url = 'http://example.com/help'
- it 'Change Performance bar settings' do
- group = create(:group)
+ page.within('.as-help-page') do
+ fill_in 'Help page text', with: 'Example text'
+ check 'Hide marketing-related entries from help'
+ fill_in 'Support page URL', with: new_support_url
+ click_button 'Save changes'
+ end
- page.within('.as-performance-bar') do
- check 'Enable access to the Performance Bar'
- fill_in 'Allowed group', with: group.path
- click_on 'Save changes'
+ expect(current_settings.help_page_text).to eq "Example text"
+ expect(current_settings.help_page_hide_commercial_content).to be_truthy
+ expect(current_settings.help_page_support_url).to eq new_support_url
+ expect(page).to have_content "Application settings saved successfully"
end
- expect(page).to have_content "Application settings saved successfully"
- expect(find_field('Enable access to the Performance Bar')).to be_checked
- expect(find_field('Allowed group').value).to eq group.path
+ it 'Change Pages settings' do
+ page.within('.as-pages') do
+ fill_in 'Maximum size of pages (MB)', with: 15
+ check 'Require users to prove ownership of custom domains'
+ click_button 'Save changes'
+ end
- page.within('.as-performance-bar') do
- uncheck 'Enable access to the Performance Bar'
- click_on 'Save changes'
+ expect(current_settings.max_pages_size).to eq 15
+ expect(current_settings.pages_domain_verification_enabled?).to be_truthy
+ expect(page).to have_content "Application settings saved successfully"
end
- expect(page).to have_content 'Application settings saved successfully'
- expect(find_field('Enable access to the Performance Bar')).not_to be_checked
- expect(find_field('Allowed group').value).to be_nil
- end
+ it 'Change Real-time features settings' do
+ page.within('.as-realtime') do
+ fill_in 'Polling interval multiplier', with: 5.0
+ click_button 'Save changes'
+ end
- it 'loads usage ping payload on click', :js do
- expect(page).to have_button 'Preview payload'
+ expect(current_settings.polling_interval_multiplier).to eq 5.0
+ expect(page).to have_content "Application settings saved successfully"
+ end
- find('.js-usage-ping-payload-trigger').click
+ it 'shows an error when validation fails' do
+ page.within('.as-realtime') do
+ fill_in 'Polling interval multiplier', with: -1.0
+ click_button 'Save changes'
+ end
- expect(page).to have_selector '.js-usage-ping-payload'
- expect(page).to have_button 'Hide payload'
- end
- end
+ expect(current_settings.polling_interval_multiplier).not_to eq(-1.0)
+ expect(page)
+ .to have_content "The form contains the following error: Polling interval multiplier must be greater than or equal to 0"
+ end
- context 'Network page' do
- it 'Changes Outbound requests settings' do
- visit network_admin_application_settings_path
+ it "Change Pages Let's Encrypt settings" do
+ visit preferences_admin_application_settings_path
+ page.within('.as-pages') do
+ fill_in 'Email', with: 'my@test.example.com'
+ check "I have read and agree to the Let's Encrypt Terms of Service"
+ click_button 'Save changes'
+ end
- page.within('.as-outbound') do
- check 'Allow requests to the local network from web hooks and services'
- # Enabled by default
- uncheck 'Allow requests to the local network from system hooks'
- # Enabled by default
- uncheck 'Enforce DNS rebinding attack protection'
- click_button 'Save changes'
+ expect(current_settings.lets_encrypt_notification_email).to eq 'my@test.example.com'
+ expect(current_settings.lets_encrypt_terms_of_service_accepted).to eq true
end
-
- expect(page).to have_content "Application settings saved successfully"
- expect(current_settings.allow_local_requests_from_web_hooks_and_services).to be true
- expect(current_settings.allow_local_requests_from_system_hooks).to be false
- expect(current_settings.dns_rebinding_protection_enabled).to be false
end
- end
- context 'Preferences page' do
- before do
- visit preferences_admin_application_settings_path
- end
+ context 'Nav bar' do
+ it 'Shows default help links in nav' do
+ default_support_url = 'https://about.gitlab.com/getting-help/'
- it 'Change Help page' do
- new_support_url = 'http://example.com/help'
+ visit root_dashboard_path
- page.within('.as-help-page') do
- fill_in 'Help page text', with: 'Example text'
- check 'Hide marketing-related entries from help'
- fill_in 'Support page URL', with: new_support_url
- click_button 'Save changes'
+ find('.header-help-dropdown-toggle').click
+
+ page.within '.header-help' do
+ expect(page).to have_link(text: 'Help', href: help_path)
+ expect(page).to have_link(text: 'Support', href: default_support_url)
+ end
end
- expect(current_settings.help_page_text).to eq "Example text"
- expect(current_settings.help_page_hide_commercial_content).to be_truthy
- expect(current_settings.help_page_support_url).to eq new_support_url
- expect(page).to have_content "Application settings saved successfully"
- end
+ it 'Shows custom support url in nav when set' do
+ new_support_url = 'http://example.com/help'
+ stub_application_setting(help_page_support_url: new_support_url)
- it 'Change Pages settings' do
- page.within('.as-pages') do
- fill_in 'Maximum size of pages (MB)', with: 15
- check 'Require users to prove ownership of custom domains'
- click_button 'Save changes'
- end
+ visit root_dashboard_path
- expect(current_settings.max_pages_size).to eq 15
- expect(current_settings.pages_domain_verification_enabled?).to be_truthy
- expect(page).to have_content "Application settings saved successfully"
+ find('.header-help-dropdown-toggle').click
+
+ page.within '.header-help' do
+ expect(page).to have_link(text: 'Support', href: new_support_url)
+ end
+ end
end
- it 'Change Real-time features settings' do
- page.within('.as-realtime') do
- fill_in 'Polling interval multiplier', with: 5.0
- click_button 'Save changes'
+ context 'when in admin_mode' do
+ it 'contains link to leave admin mode' do
+ page.within('.navbar-sub-nav') do
+ expect(page).to have_link(href: destroy_admin_session_path)
+ end
end
- expect(current_settings.polling_interval_multiplier).to eq 5.0
- expect(page).to have_content "Application settings saved successfully"
- end
+ it 'can leave admin mode' do
+ page.within('.navbar-sub-nav') do
+ # Select first, link is also included in mobile view list
+ click_on 'Leave admin mode', match: :first
- it 'shows an error when validation fails' do
- page.within('.as-realtime') do
- fill_in 'Polling interval multiplier', with: -1.0
- click_button 'Save changes'
+ expect(page).to have_link(href: new_admin_session_path)
+ end
end
- expect(current_settings.polling_interval_multiplier).not_to eq(-1.0)
- expect(page)
- .to have_content "The form contains the following error: Polling interval multiplier must be greater than or equal to 0"
+ it 'can open pages not in admin scope' do
+ page.within('.navbar-sub-nav') do
+ find_all('a', text: 'Projects').first.click
+
+ expect(page).to have_current_path(dashboard_projects_path)
+ end
+ end
end
- it "Change Pages Let's Encrypt settings" do
- visit preferences_admin_application_settings_path
- page.within('.as-pages') do
- fill_in 'Email', with: 'my@test.example.com'
- check "I have read and agree to the Let's Encrypt Terms of Service"
- click_button 'Save changes'
+ context 'when not in admin mode' do
+ before do
+ page.within('.navbar-sub-nav') do
+ # Select first, link is also included in mobile view list
+ click_on 'Leave admin mode', match: :first
+ end
end
- expect(current_settings.lets_encrypt_notification_email).to eq 'my@test.example.com'
- expect(current_settings.lets_encrypt_terms_of_service_accepted).to eq true
- end
- end
+ it 'has no leave admin mode button' do
+ page.within('.navbar-sub-nav') do
+ expect(page).not_to have_link(href: destroy_admin_session_path)
+ end
+ end
- context 'Nav bar' do
- it 'Shows default help links in nav' do
- default_support_url = 'https://about.gitlab.com/getting-help/'
+ it 'is necessary to provide credentials again before opening admin settings' do
+ visit admin_application_settings_path # admin logged out because not in admin_mode
- visit root_dashboard_path
+ expect(page).to have_current_path(new_admin_session_path)
+ end
- find('.header-help-dropdown-toggle').click
+ it 'can open pages not in admin scope' do
+ page.within('.navbar-sub-nav') do
+ find_all('a', text: 'Projects').first.click
+ end
- page.within '.header-help' do
- expect(page).to have_link(text: 'Help', href: help_path)
- expect(page).to have_link(text: 'Support', href: default_support_url)
+ expect(page).to have_current_path(dashboard_projects_path)
end
end
+ end
- it 'Shows custom support url in nav when set' do
- new_support_url = 'http://example.com/help'
- stub_application_setting(help_page_support_url: new_support_url)
+ context 'feature flag :user_mode_in_session is disabled' do
+ before do
+ stub_feature_flags(user_mode_in_session: false)
- visit root_dashboard_path
+ stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false')
- find('.header-help-dropdown-toggle').click
+ sign_in(admin)
+ visit admin_application_settings_path
+ end
+
+ it 'loads admin settings page without redirect for reauthentication' do
+ expect(current_path).to eq admin_application_settings_path
+ end
- page.within '.header-help' do
- expect(page).to have_link(text: 'Support', href: new_support_url)
+ it 'shows no admin mode buttons in navbar' do
+ page.within('.navbar-sub-nav') do
+ expect(page).not_to have_link(href: new_admin_session_path)
+ expect(page).not_to have_link(href: destroy_admin_session_path)
end
end
end
diff --git a/spec/features/admin/admin_users_spec.rb b/spec/features/admin/admin_users_spec.rb
index ebf71d8c9da..29f29e58917 100644
--- a/spec/features/admin/admin_users_spec.rb
+++ b/spec/features/admin/admin_users_spec.rb
@@ -31,7 +31,8 @@ describe "Admin::Users" do
expect(page).to have_content(current_user.last_activity_on.strftime("%e %b, %Y"))
expect(page).to have_content(user.email)
expect(page).to have_content(user.name)
- expect(page).to have_link('Block', href: block_admin_user_path(user))
+ expect(page).to have_button('Block')
+ expect(page).to have_button('Deactivate')
expect(page).to have_button('Delete user')
expect(page).to have_button('Delete user and contributions')
end
@@ -277,7 +278,8 @@ describe "Admin::Users" do
expect(page).to have_content(user.email)
expect(page).to have_content(user.name)
expect(page).to have_content(user.id)
- expect(page).to have_link('Block user', href: block_admin_user_path(user))
+ expect(page).to have_button('Deactivate user')
+ expect(page).to have_button('Block user')
expect(page).to have_button('Delete user')
expect(page).to have_button('Delete user and contributions')
end
diff --git a/spec/features/boards/boards_spec.rb b/spec/features/boards/boards_spec.rb
index 57dc9de62fb..e26582d3444 100644
--- a/spec/features/boards/boards_spec.rb
+++ b/spec/features/boards/boards_spec.rb
@@ -234,6 +234,12 @@ describe 'Issue Boards', :js do
expect(find('.board:nth-child(2)')).to have_content(development.title)
expect(find('.board:nth-child(2)')).to have_content(planning.title)
+
+ # Make sure list positions are preserved after a reload
+ visit project_board_path(project, board)
+
+ expect(find('.board:nth-child(2)')).to have_content(development.title)
+ expect(find('.board:nth-child(2)')).to have_content(planning.title)
end
it 'dragging does not duplicate list' do
@@ -245,7 +251,7 @@ describe 'Issue Boards', :js do
expect(page).to have_selector(selector, text: development.title, count: 1)
end
- it 'issue moves between lists' do
+ it 'issue moves between lists and does not show the "Development" label since the card is in the "Development" list label' do
drag(list_from_index: 1, from_index: 1, list_to_index: 2)
wait_for_board_cards(2, 7)
@@ -253,10 +259,10 @@ describe 'Issue Boards', :js do
wait_for_board_cards(4, 1)
expect(find('.board:nth-child(3)')).to have_content(issue6.title)
- expect(find('.board:nth-child(3)').all('.board-card').last).to have_content(development.title)
+ expect(find('.board:nth-child(3)').all('.board-card').last).not_to have_content(development.title)
end
- it 'issue moves between lists' do
+ it 'issue moves between lists and does not show the "Planning" label since the card is in the "Planning" list label' do
drag(list_from_index: 2, list_to_index: 1)
wait_for_board_cards(2, 9)
@@ -264,7 +270,7 @@ describe 'Issue Boards', :js do
wait_for_board_cards(4, 1)
expect(find('.board:nth-child(2)')).to have_content(issue7.title)
- expect(find('.board:nth-child(2)').all('.board-card').first).to have_content(planning.title)
+ expect(find('.board:nth-child(2)').all('.board-card').first).not_to have_content(planning.title)
end
it 'issue moves from closed' do
diff --git a/spec/features/boards/multi_select_spec.rb b/spec/features/boards/multi_select_spec.rb
new file mode 100644
index 00000000000..885dc08e38d
--- /dev/null
+++ b/spec/features/boards/multi_select_spec.rb
@@ -0,0 +1,110 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'Multi Select Issue', :js do
+ include DragTo
+
+ let(:group) { create(:group, :nested) }
+ let(:project) { create(:project, :public, namespace: group) }
+ let(:board) { create(:board, project: project) }
+ let(:user) { create(:user) }
+
+ def drag(selector: '.board-list', list_from_index: 1, from_index: 0, to_index: 0, list_to_index: 1, duration: 1000)
+ drag_to(selector: selector,
+ scrollable: '#board-app',
+ list_from_index: list_from_index,
+ from_index: from_index,
+ to_index: to_index,
+ list_to_index: list_to_index,
+ duration: duration)
+ end
+
+ def wait_for_board_cards(board_number, expected_cards)
+ page.within(find(".board:nth-child(#{board_number})")) do
+ expect(page.find('.board-header')).to have_content(expected_cards.to_s)
+ expect(page).to have_selector('.board-card', count: expected_cards)
+ end
+ end
+
+ def multi_select(selector, action = 'select')
+ element = page.find(selector)
+ script = "var el = document.querySelector('#{selector}');"
+ script += "var mousedown = new MouseEvent('mousedown', { button: 0, bubbles: true });"
+ script += "var mouseup = new MouseEvent('mouseup', { ctrlKey: true, button: 0, bubbles:true });"
+ script += "el.dispatchEvent(mousedown); el.dispatchEvent(mouseup);"
+ script += "Sortable.utils.#{action}(el);"
+
+ page.execute_script(script, element)
+ end
+
+ before do
+ project.add_maintainer(user)
+
+ sign_in(user)
+ end
+
+ context 'with lists' do
+ let(:label1) { create(:label, project: project, name: 'Label 1', description: 'Test') }
+ let(:label2) { create(:label, project: project, name: 'Label 2', description: 'Test') }
+ let!(:list1) { create(:list, board: board, label: label1, position: 0) }
+ let!(:list2) { create(:list, board: board, label: label2, position: 1) }
+ let!(:issue1) { create(:labeled_issue, project: project, title: 'Issue 1', description: '', assignees: [user], labels: [label1], relative_position: 1) }
+ let!(:issue2) { create(:labeled_issue, project: project, title: 'Issue 2', description: '', author: user, labels: [label1], relative_position: 2) }
+ let!(:issue3) { create(:labeled_issue, project: project, title: 'Issue 3', description: '', labels: [label1], relative_position: 3) }
+ let!(:issue4) { create(:labeled_issue, project: project, title: 'Issue 4', description: '', labels: [label1], relative_position: 4) }
+ let!(:issue5) { create(:labeled_issue, project: project, title: 'Issue 5', description: '', labels: [label1], relative_position: 5) }
+
+ before do
+ visit project_board_path(project, board)
+
+ wait_for_requests
+ end
+
+ it 'moves multiple issues to another list', :js do
+ multi_select('.board-card:nth-child(1)')
+ multi_select('.board-card:nth-child(2)')
+ drag(list_from_index: 1, list_to_index: 2)
+
+ wait_for_requests
+
+ page.within(all('.js-board-list')[2]) do
+ expect(find('.board-card:nth-child(1)')).to have_content(issue1.title)
+ expect(find('.board-card:nth-child(2)')).to have_content(issue2.title)
+ end
+ end
+
+ it 'maintains order when moved', :js do
+ multi_select('.board-card:nth-child(3)')
+ multi_select('.board-card:nth-child(2)')
+ multi_select('.board-card:nth-child(1)')
+
+ drag(list_from_index: 1, list_to_index: 2)
+
+ wait_for_requests
+
+ page.within(all('.js-board-list')[2]) do
+ expect(find('.board-card:nth-child(1)')).to have_content(issue1.title)
+ expect(find('.board-card:nth-child(2)')).to have_content(issue2.title)
+ expect(find('.board-card:nth-child(3)')).to have_content(issue3.title)
+ end
+ end
+
+ it 'move issues in the same list', :js do
+ multi_select('.board-card:nth-child(3)')
+ multi_select('.board-card:nth-child(4)')
+
+ drag(list_from_index: 1, list_to_index: 1, from_index: 2, to_index: 4)
+
+ wait_for_requests
+
+ page.within(all('.js-board-list')[1]) do
+ expect(find('.board-card:nth-child(1)')).to have_content(issue1.title)
+ expect(find('.board-card:nth-child(2)')).to have_content(issue2.title)
+ expect(find('.board-card:nth-child(3)')).to have_content(issue5.title)
+ expect(find('.board-card:nth-child(4)')).to have_content(issue3.title)
+ expect(find('.board-card:nth-child(5)')).to have_content(issue4.title)
+ end
+ end
+ end
+end
diff --git a/spec/features/boards/sidebar_spec.rb b/spec/features/boards/sidebar_spec.rb
index 8497eaf102f..2b923df40c5 100644
--- a/spec/features/boards/sidebar_spec.rb
+++ b/spec/features/boards/sidebar_spec.rb
@@ -304,7 +304,8 @@ describe 'Issue Boards', :js do
end
end
- expect(card).to have_selector('.badge', count: 3)
+ # 'Development' label does not show since the card is in a 'Development' list label
+ expect(card).to have_selector('.badge', count: 2)
expect(card).to have_content(bug.title)
end
@@ -330,7 +331,8 @@ describe 'Issue Boards', :js do
end
end
- expect(card).to have_selector('.badge', count: 4)
+ # 'Development' label does not show since the card is in a 'Development' list label
+ expect(card).to have_selector('.badge', count: 3)
expect(card).to have_content(bug.title)
expect(card).to have_content(regression.title)
end
@@ -357,7 +359,8 @@ describe 'Issue Boards', :js do
end
end
- expect(card).to have_selector('.badge', count: 1)
+ # 'Development' label does not show since the card is in a 'Development' list label
+ expect(card).to have_selector('.badge', count: 0)
expect(card).not_to have_content(stretch.title)
end
diff --git a/spec/features/clusters/cluster_detail_page_spec.rb b/spec/features/clusters/cluster_detail_page_spec.rb
index 927862689c1..437e7f18c48 100644
--- a/spec/features/clusters/cluster_detail_page_spec.rb
+++ b/spec/features/clusters/cluster_detail_page_spec.rb
@@ -14,10 +14,6 @@ describe 'Clusterable > Show page' do
end
shared_examples 'show page' do
- before do
- clusterable.add_maintainer(current_user)
- end
-
it 'allow the user to set domain' do
visit cluster_path
@@ -63,7 +59,6 @@ describe 'Clusterable > Show page' do
shared_examples 'editing a GCP cluster' do
before do
- clusterable.add_maintainer(current_user)
visit cluster_path
end
@@ -92,7 +87,6 @@ describe 'Clusterable > Show page' do
shared_examples 'editing a user-provided cluster' do
before do
stub_kubeclient_discover(cluster.platform.api_url)
- clusterable.add_maintainer(current_user)
visit cluster_path
end
@@ -123,6 +117,10 @@ describe 'Clusterable > Show page' do
let(:cluster_path) { project_cluster_path(clusterable, cluster) }
let(:cluster) { create(:cluster, :provided_by_gcp, :project, projects: [clusterable]) }
+ before do
+ clusterable.add_maintainer(current_user)
+ end
+
it_behaves_like 'show page'
it_behaves_like 'editing a GCP cluster'
@@ -137,6 +135,10 @@ describe 'Clusterable > Show page' do
let(:cluster_path) { group_cluster_path(clusterable, cluster) }
let(:cluster) { create(:cluster, :provided_by_gcp, :group, groups: [clusterable]) }
+ before do
+ clusterable.add_maintainer(current_user)
+ end
+
it_behaves_like 'show page'
it_behaves_like 'editing a GCP cluster'
@@ -145,4 +147,18 @@ describe 'Clusterable > Show page' do
let(:cluster) { create(:cluster, :provided_by_user, :group, groups: [clusterable]) }
end
end
+
+ context 'when clusterable is an instance' do
+ let(:current_user) { create(:admin) }
+ let(:cluster_path) { admin_cluster_path(cluster) }
+ let(:cluster) { create(:cluster, :provided_by_gcp, :instance) }
+
+ it_behaves_like 'show page'
+
+ it_behaves_like 'editing a GCP cluster'
+
+ it_behaves_like 'editing a user-provided cluster' do
+ let(:cluster) { create(:cluster, :provided_by_user, :instance) }
+ end
+ end
end
diff --git a/spec/features/container_registry_spec.rb b/spec/features/container_registry_spec.rb
index aefdc4d6d4f..03a2402a2d6 100644
--- a/spec/features/container_registry_spec.rb
+++ b/spec/features/container_registry_spec.rb
@@ -17,6 +17,11 @@ describe 'Container Registry', :js do
stub_container_registry_tags(repository: :any, tags: [])
end
+ it 'has a page title set' do
+ visit_container_registry
+ expect(page).to have_title(_('Container Registry'))
+ end
+
context 'when there are no image repositories' do
it 'user visits container registry main page' do
visit_container_registry
@@ -53,10 +58,12 @@ describe 'Container Registry', :js do
find('.js-toggle-repo').click
wait_for_requests
- expect_any_instance_of(ContainerRegistry::Tag).to receive(:delete).and_return(true)
+ service = double('service')
+ expect(service).to receive(:execute).with(container_repository) { { status: :success } }
+ expect(Projects::ContainerRepository::DeleteTagsService).to receive(:new).with(container_repository.project, user, tags: ['latest']) { service }
click_on(class: 'js-delete-registry-row', visible: false)
- expect(find('.modal .modal-title')).to have_content 'Remove image'
+ expect(find('.modal .modal-title')).to have_content 'Remove tag'
find('.modal .modal-footer .btn-danger').click
end
end
diff --git a/spec/features/dashboard/active_tab_spec.rb b/spec/features/dashboard/active_tab_spec.rb
index 92d0c0c9260..ee85c136190 100644
--- a/spec/features/dashboard/active_tab_spec.rb
+++ b/spec/features/dashboard/active_tab_spec.rb
@@ -27,20 +27,4 @@ RSpec.describe 'Dashboard Active Tab', :js do
subject { visit dashboard_groups_path }
end
end
-
- context 'on activity projects' do
- it_behaves_like 'page has active tab', 'Activity' do
- subject { visit activity_dashboard_path }
- end
- end
-
- context 'on instance statistics' do
- subject { visit instance_statistics_root_path }
-
- it 'shows Instance Statistics` as active' do
- subject
-
- expect(find('.navbar-sub-nav li.active')).to have_link('Instance Statistics')
- end
- end
end
diff --git a/spec/features/groups/issues_spec.rb b/spec/features/groups/issues_spec.rb
index 0cb24ef856b..5d87c9d7be8 100644
--- a/spec/features/groups/issues_spec.rb
+++ b/spec/features/groups/issues_spec.rb
@@ -93,6 +93,7 @@ describe 'Group issues page' do
end
it 'shows projects only with issues feature enabled', :js do
+ find('.empty-state .js-lazy-loaded')
find('.new-project-item-link').click
page.within('.select2-results') do
diff --git a/spec/features/groups/members/master_manages_access_requests_spec.rb b/spec/features/groups/members/master_manages_access_requests_spec.rb
index 454da126c81..1c13bd3d59e 100644
--- a/spec/features/groups/members/master_manages_access_requests_spec.rb
+++ b/spec/features/groups/members/master_manages_access_requests_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
describe 'Groups > Members > Maintainer manages access requests' do
it_behaves_like 'Maintainer manages access requests' do
- let(:entity) { create(:group, :public, :access_requestable) }
+ let(:entity) { create(:group, :public) }
let(:members_page_path) { group_group_members_path(entity) }
end
end
diff --git a/spec/features/groups/members/request_access_spec.rb b/spec/features/groups/members/request_access_spec.rb
index 0d5321709ae..5f22af3529c 100644
--- a/spec/features/groups/members/request_access_spec.rb
+++ b/spec/features/groups/members/request_access_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
describe 'Groups > Members > Request access' do
let(:user) { create(:user) }
let(:owner) { create(:user) }
- let(:group) { create(:group, :public, :access_requestable) }
+ let(:group) { create(:group, :public) }
let!(:project) { create(:project, :private, namespace: group) }
before do
diff --git a/spec/features/groups/user_sees_package_sidebar_spec.rb b/spec/features/groups/user_sees_package_sidebar_spec.rb
new file mode 100644
index 00000000000..f85b6841636
--- /dev/null
+++ b/spec/features/groups/user_sees_package_sidebar_spec.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'Groups > sidebar' do
+ let(:user) { create(:user) }
+ let(:group) { create(:group) }
+
+ before do
+ group.add_developer(user)
+ sign_in(user)
+ end
+
+ context 'Package menu' do
+ context 'when container registry is enabled' do
+ before do
+ stub_container_registry_config(enabled: true)
+ visit group_path(group)
+ end
+
+ it 'shows main menu' do
+ within '.nav-sidebar' do
+ expect(page).to have_link(_('Packages'))
+ end
+ end
+
+ it 'has container registry link' do
+ within '.nav-sidebar' do
+ expect(page).to have_link(_('Container Registry'))
+ end
+ end
+ end
+
+ context 'when container registry is disabled' do
+ before do
+ stub_container_registry_config(enabled: false)
+ visit group_path(group)
+ end
+
+ it 'does not have container registry link' do
+ within '.nav-sidebar' do
+ expect(page).not_to have_link(_('Container Registry'))
+ end
+ end
+ end
+ end
+end
diff --git a/spec/features/invites_spec.rb b/spec/features/invites_spec.rb
index 1e054a7b358..2a1980346e9 100644
--- a/spec/features/invites_spec.rb
+++ b/spec/features/invites_spec.rb
@@ -10,7 +10,6 @@ describe 'Invites' do
let(:group_invite) { group.group_members.invite.last }
before do
- stub_feature_flags(invisible_captcha: false)
project.add_maintainer(owner)
group.add_user(owner, Gitlab::Access::OWNER)
group.add_developer('user@example.com', owner)
diff --git a/spec/features/issues/filtered_search/dropdown_milestone_spec.rb b/spec/features/issues/filtered_search/dropdown_milestone_spec.rb
index 9a782950bf6..5272a970a60 100644
--- a/spec/features/issues/filtered_search/dropdown_milestone_spec.rb
+++ b/spec/features/issues/filtered_search/dropdown_milestone_spec.rb
@@ -39,16 +39,28 @@ describe 'Dropdown milestone', :js do
end
describe 'behavior' do
- it 'opens when the search bar has milestone:' do
- filtered_search.set('milestone:')
+ context 'filters by "milestone:"' do
+ before do
+ filtered_search.set('milestone:')
+ end
- expect(page).to have_css(js_dropdown_milestone, visible: true)
- end
+ it 'opens when the search bar has milestone:' do
+ expect(page).to have_css(js_dropdown_milestone, visible: true)
+ end
- it 'closes when the search bar is unfocused' do
- find('body').click
+ it 'closes when the search bar is unfocused' do
+ find('body').click
- expect(page).to have_css(js_dropdown_milestone, visible: false)
+ expect(page).to have_css(js_dropdown_milestone, visible: false)
+ end
+
+ it 'hides loading indicator when loaded' do
+ expect(find(js_dropdown_milestone)).not_to have_css('.filter-dropdown-loading')
+ end
+
+ it 'loads all the milestones when opened' do
+ expect(filter_dropdown).to have_selector('.filter-dropdown .filter-dropdown-item', count: 6)
+ end
end
it 'shows loading indicator when opened' do
@@ -58,18 +70,6 @@ describe 'Dropdown milestone', :js do
expect(page).to have_css('#js-dropdown-milestone .filter-dropdown-loading', visible: true)
end
end
-
- it 'hides loading indicator when loaded' do
- filtered_search.set('milestone:')
-
- expect(find(js_dropdown_milestone)).not_to have_css('.filter-dropdown-loading')
- end
-
- it 'loads all the milestones when opened' do
- filtered_search.set('milestone:')
-
- expect(filter_dropdown).to have_selector('.filter-dropdown .filter-dropdown-item', count: 6)
- end
end
describe 'filtering' do
diff --git a/spec/features/issues/gfm_autocomplete_spec.rb b/spec/features/issues/gfm_autocomplete_spec.rb
index cc834df367b..0ff3809a915 100644
--- a/spec/features/issues/gfm_autocomplete_spec.rb
+++ b/spec/features/issues/gfm_autocomplete_spec.rb
@@ -27,6 +27,8 @@ describe 'GFM autocomplete', :js do
it 'updates issue description with GFM reference' do
find('.js-issuable-edit').click
+ wait_for_requests
+
simulate_input('#issue-description', "@#{user.name[0...3]}")
wait_for_requests
diff --git a/spec/features/issues/spam_issues_spec.rb b/spec/features/issues/spam_issues_spec.rb
index 0d009f47fff..18245f249fd 100644
--- a/spec/features/issues/spam_issues_spec.rb
+++ b/spec/features/issues/spam_issues_spec.rb
@@ -30,21 +30,47 @@ describe 'New issue', :js do
visit new_project_issue_path(project)
end
- it 'creates an issue after solving reCaptcha' do
- fill_in 'issue_title', with: 'issue title'
- fill_in 'issue_description', with: 'issue description'
+ context 'when allow_possible_spam feature flag is false' do
+ before do
+ stub_feature_flags(allow_possible_spam: false)
+ end
- click_button 'Submit issue'
+ it 'creates an issue after solving reCaptcha' do
+ fill_in 'issue_title', with: 'issue title'
+ fill_in 'issue_description', with: 'issue description'
- # it is impossible to test recaptcha automatically and there is no possibility to fill in recaptcha
- # recaptcha verification is skipped in test environment and it always returns true
- expect(page).not_to have_content('issue title')
- expect(page).to have_css('.recaptcha')
+ click_button 'Submit issue'
- click_button 'Submit issue'
+ # it is impossible to test recaptcha automatically and there is no possibility to fill in recaptcha
+ # recaptcha verification is skipped in test environment and it always returns true
+ expect(page).not_to have_content('issue title')
+ expect(page).to have_css('.recaptcha')
- expect(page.find('.issue-details h2.title')).to have_content('issue title')
- expect(page.find('.issue-details .description')).to have_content('issue description')
+ click_button 'Submit issue'
+
+ expect(page.find('.issue-details h2.title')).to have_content('issue title')
+ expect(page.find('.issue-details .description')).to have_content('issue description')
+ end
+ end
+
+ context 'when allow_possible_spam feature flag is true' do
+ before do
+ fill_in 'issue_title', with: 'issue title'
+ fill_in 'issue_description', with: 'issue description'
+ end
+
+ it 'creates an issue without a need to solve reCaptcha' do
+ click_button 'Submit issue'
+
+ expect(page).not_to have_css('.recaptcha')
+ expect(page.find('.issue-details h2.title')).to have_content('issue title')
+ expect(page.find('.issue-details .description')).to have_content('issue description')
+ end
+
+ it 'creates a spam log record' do
+ expect { click_button 'Submit issue' }
+ .to log_spam(title: 'issue title', description: 'issue description', user_id: user.id, noteable_type: 'Issue')
+ end
end
end
diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb
index 5bdd9113b06..ef9daf70b0c 100644
--- a/spec/features/issues_spec.rb
+++ b/spec/features/issues_spec.rb
@@ -470,8 +470,7 @@ describe 'Issues' do
expect(page).to have_content 'None'
end
- # wait_for_requests does not work with vue-resource at the moment
- sleep 1
+ wait_for_requests
expect(issue.reload.assignees).to be_empty
end
diff --git a/spec/features/markdown/copy_as_gfm_spec.rb b/spec/features/markdown/copy_as_gfm_spec.rb
index 9f26321a1dc..84221f5555a 100644
--- a/spec/features/markdown/copy_as_gfm_spec.rb
+++ b/spec/features/markdown/copy_as_gfm_spec.rb
@@ -179,6 +179,12 @@ describe 'Copy as GFM', :js do
)
verify(
+ 'AudioLinkFilter',
+
+ '![Audio](https://example.com/audio.wav)'
+ )
+
+ verify(
'MathFilter: math as converted from GFM to HTML',
'$`c = \pm\sqrt{a^2 + b^2}`$',
diff --git a/spec/features/markdown/markdown_spec.rb b/spec/features/markdown/markdown_spec.rb
index 0efeffe3232..a45fa67ce9e 100644
--- a/spec/features/markdown/markdown_spec.rb
+++ b/spec/features/markdown/markdown_spec.rb
@@ -320,6 +320,10 @@ describe 'GitLab Markdown', :aggregate_failures do
expect(doc).to parse_video_links
end
+ aggregate_failures 'AudioLinkFilter' do
+ expect(doc).to parse_audio_links
+ end
+
aggregate_failures 'ColorFilter' do
expect(doc).to parse_colors
end
diff --git a/spec/features/markdown/mermaid_spec.rb b/spec/features/markdown/mermaid_spec.rb
index b2a31ff2275..4520d1bb2da 100644
--- a/spec/features/markdown/mermaid_spec.rb
+++ b/spec/features/markdown/mermaid_spec.rb
@@ -56,18 +56,6 @@ describe 'Mermaid rendering', :js do
graph LR
A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;
```
- ```mermaid
- graph LR
- A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;
- ```
- ```mermaid
- graph LR
- A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;
- ```
- ```mermaid
- graph LR
- A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;
- ```
MERMAID
project = create(:project, :public)
diff --git a/spec/features/merge_request/user_edits_assignees_sidebar_spec.rb b/spec/features/merge_request/user_edits_assignees_sidebar_spec.rb
new file mode 100644
index 00000000000..59c20f4ec6b
--- /dev/null
+++ b/spec/features/merge_request/user_edits_assignees_sidebar_spec.rb
@@ -0,0 +1,68 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'Merge request > User edits assignees sidebar', :js do
+ let(:project) { create(:project, :public, :repository) }
+ let(:protected_branch) { create(:protected_branch, :maintainers_can_push, name: 'master', project: project) }
+ let(:merge_request) { create(:merge_request, :simple, source_project: project, target_branch: protected_branch.name) }
+
+ let(:users_find_limit) { 5 }
+
+ # Insert more than limit so that response doesn't include assigned user
+ let(:project_developers) { Array.new(users_find_limit + 1) { create(:user).tap { |u| project.add_developer(u) } } }
+ let(:project_maintainers) { Array.new(users_find_limit + 1) { create(:user).tap { |u| project.add_maintainer(u) } } }
+
+ # DOM finders to simplify and improve readability
+ let(:sidebar_assignee_block) { page.find('.js-issuable-sidebar .assignee') }
+ let(:sidebar_assignee_avatar_link) { sidebar_assignee_block.find_all('a').find { |a| a['href'].include? assignee.username } }
+ let(:sidebar_assignee_tooltip) { sidebar_assignee_avatar_link['data-original-title'] || '' }
+ let(:sidebar_assignee_dropdown_item) { sidebar_assignee_block.find(".dropdown-menu li[data-user-id=\"#{assignee.id}\"]") }
+ let(:sidebar_assignee_dropdown_tooltip) { sidebar_assignee_dropdown_item.find('a')['data-title'] || '' }
+
+ before do
+ stub_const('Autocomplete::UsersFinder::LIMIT', users_find_limit)
+
+ sign_in(project.owner)
+
+ merge_request.assignees << assignee
+
+ visit project_merge_request_path(project, merge_request)
+
+ wait_for_requests
+ end
+
+ shared_examples 'when assigned' do |expected_tooltip: ''|
+ it 'shows assignee name' do
+ expect(sidebar_assignee_block).to have_text(assignee.name)
+ end
+
+ it "shows assignee tooltip '#{expected_tooltip}'" do
+ expect(sidebar_assignee_tooltip).to eql(expected_tooltip)
+ end
+
+ context 'when edit is clicked' do
+ before do
+ sidebar_assignee_block.click_link('Edit')
+
+ wait_for_requests
+ end
+
+ it "shows assignee tooltip '#{expected_tooltip}" do
+ expect(sidebar_assignee_dropdown_tooltip).to eql(expected_tooltip)
+ end
+ end
+ end
+
+ context 'when assigned to maintainer' do
+ let(:assignee) { project_maintainers.last }
+
+ it_behaves_like 'when assigned', expected_tooltip: ''
+ end
+
+ context 'when assigned to developer' do
+ let(:assignee) { project_developers.last }
+
+ it_behaves_like 'when assigned', expected_tooltip: 'Cannot merge'
+ end
+end
diff --git a/spec/features/merge_request/user_sees_merge_widget_spec.rb b/spec/features/merge_request/user_sees_merge_widget_spec.rb
index d19835741e3..6b6226ad1c5 100644
--- a/spec/features/merge_request/user_sees_merge_widget_spec.rb
+++ b/spec/features/merge_request/user_sees_merge_widget_spec.rb
@@ -536,10 +536,10 @@ describe 'Merge request > User sees merge widget', :js do
within(".js-reports-container") do
click_button 'Expand'
- expect(page).to have_content('Test summary contained 1 failed test result out of 2 total tests')
+ expect(page).to have_content('Test summary contained 1 failed/error test result out of 2 total tests')
within(".js-report-section-container") do
expect(page).to have_content('rspec found no changed test results out of 1 total test')
- expect(page).to have_content('junit found 1 failed test result out of 1 total test')
+ expect(page).to have_content('junit found 1 failed/error test result out of 1 total test')
expect(page).to have_content('New')
expect(page).to have_content('addTest')
end
@@ -581,9 +581,9 @@ describe 'Merge request > User sees merge widget', :js do
within(".js-reports-container") do
click_button 'Expand'
- expect(page).to have_content('Test summary contained 1 failed test result out of 2 total tests')
+ expect(page).to have_content('Test summary contained 1 failed/error test result out of 2 total tests')
within(".js-report-section-container") do
- expect(page).to have_content('rspec found 1 failed test result out of 1 total test')
+ expect(page).to have_content('rspec found 1 failed/error test result out of 1 total test')
expect(page).to have_content('junit found no changed test results out of 1 total test')
expect(page).not_to have_content('New')
expect(page).to have_content('Test#sum when a is 1 and b is 3 returns summary')
@@ -677,10 +677,10 @@ describe 'Merge request > User sees merge widget', :js do
within(".js-reports-container") do
click_button 'Expand'
- expect(page).to have_content('Test summary contained 20 failed test results out of 20 total tests')
+ expect(page).to have_content('Test summary contained 20 failed/error test results out of 20 total tests')
within(".js-report-section-container") do
- expect(page).to have_content('rspec found 10 failed test results out of 10 total tests')
- expect(page).to have_content('junit found 10 failed test results out of 10 total tests')
+ expect(page).to have_content('rspec found 10 failed/error test results out of 10 total tests')
+ expect(page).to have_content('junit found 10 failed/error test results out of 10 total tests')
expect(page).to have_content('Test#sum when a is 1 and b is 3 returns summary', count: 2)
end
diff --git a/spec/features/merge_request/user_suggests_changes_on_diff_spec.rb b/spec/features/merge_request/user_suggests_changes_on_diff_spec.rb
index 4363b359038..3d26ff3ed94 100644
--- a/spec/features/merge_request/user_suggests_changes_on_diff_spec.rb
+++ b/spec/features/merge_request/user_suggests_changes_on_diff_spec.rb
@@ -14,6 +14,10 @@ describe 'User comments on a diff', :js do
expect(suggested_content).to eq(expected_suggested_content)
end
+ def expect_appliable_suggestions(amount)
+ expect(all('button', text: 'Apply suggestion').size).to eq(amount)
+ end
+
let(:project) { create(:project, :repository) }
let(:merge_request) do
create(:merge_request_with_diffs, source_project: project, target_project: project, source_branch: 'merge-test')
@@ -89,6 +93,60 @@ describe 'User comments on a diff', :js do
end
end
+ context 'multiple suggestions in expanded lines' do
+ it 'suggestions are appliable' do
+ diff_file = merge_request.diffs(paths: ['files/ruby/popen.rb']).diff_files.first
+ hash = Digest::SHA1.hexdigest(diff_file.file_path)
+
+ expanded_changes = [
+ {
+ line_code: "#{hash}_1_1",
+ file_path: diff_file.file_path
+ },
+ {
+ line_code: "#{hash}_5_5",
+ file_path: diff_file.file_path
+ }
+ ]
+ changes = sample_compare(expanded_changes).changes.last(expanded_changes.size)
+
+ page.within("[id='#{hash}']") do
+ find("button[data-original-title='Show full file']").click
+ wait_for_requests
+
+ click_diff_line(find("[id='#{changes.first[:line_code]}']"))
+
+ page.within('.js-discussion-note-form') do
+ fill_in('note_note', with: "```suggestion\n# change to a comment\n```")
+ click_button('Comment')
+ wait_for_requests
+ end
+
+ click_diff_line(find("[id='#{changes.last[:line_code]}']"))
+
+ page.within('.js-discussion-note-form') do
+ fill_in('note_note', with: "```suggestion\n# 2nd change to a comment\n```")
+ click_button('Comment')
+ wait_for_requests
+ end
+
+ expect_appliable_suggestions(2)
+ end
+
+ # Making sure it's not a Front-end cache.
+ visit(diffs_project_merge_request_path(project, merge_request))
+
+ expect_appliable_suggestions(2)
+
+ page.within("[id='#{hash}']") do
+ all('button', text: 'Apply suggestion').last.click
+ wait_for_requests
+
+ expect(page).to have_content('Applied')
+ end
+ end
+ end
+
context 'multiple suggestions in a single note' do
it 'suggestions are presented' do
click_diff_line(find("[id='#{sample_compare.changes[1][:line_code]}']"))
diff --git a/spec/features/merge_request/user_views_merge_request_from_deleted_fork_spec.rb b/spec/features/merge_request/user_views_merge_request_from_deleted_fork_spec.rb
new file mode 100644
index 00000000000..26984a1fb5e
--- /dev/null
+++ b/spec/features/merge_request/user_views_merge_request_from_deleted_fork_spec.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+# This is a feature spec because the problems arrise when rendering the view for
+# an actual project for which the repository is removed but the cached not
+# updated.
+# This can occur when the fork a merge request is created from is in the process
+# of being destroyed.
+describe 'User views merged merge request from deleted fork' do
+ include ProjectForksHelper
+
+ let(:project) { create(:project, :repository) }
+ let(:source_project) { fork_project(project, nil, repository: true) }
+ let(:user) { project.owner }
+ let!(:merge_request) { create(:merge_request, :merged, source_project: source_project, target_project: project) }
+
+ before do
+ sign_in user
+
+ fork_owner = source_project.namespace.owners.first
+ # Place the source_project in the weird in between state
+ source_project.update_attribute(:pending_delete, true)
+ Projects::DestroyService.new(source_project, fork_owner, {}).__send__(:trash_repositories!)
+ end
+
+ it 'correctly shows the merge request' do
+ visit(merge_request_path(merge_request))
+
+ expect(page).to have_content(merge_request.title)
+ end
+end
diff --git a/spec/features/milestones/user_deletes_milestone_spec.rb b/spec/features/milestones/user_deletes_milestone_spec.rb
index 7c1d88f7798..fd72f2dfefa 100644
--- a/spec/features/milestones/user_deletes_milestone_spec.rb
+++ b/spec/features/milestones/user_deletes_milestone_spec.rb
@@ -12,7 +12,7 @@ describe "User deletes milestone", :js do
end
context "when milestone belongs to project" do
- let!(:milestone) { create(:milestone, parent: project, title: "project milestone") }
+ let!(:milestone) { create(:milestone, resource_parent: project, title: "project milestone") }
it "deletes milestone" do
project.add_developer(user)
@@ -30,8 +30,8 @@ describe "User deletes milestone", :js do
end
context "when milestone belongs to group" do
- let!(:milestone_to_be_deleted) { create(:milestone, parent: group, title: "group milestone 1") }
- let!(:milestone) { create(:milestone, parent: group, title: "group milestone 2") }
+ let!(:milestone_to_be_deleted) { create(:milestone, resource_parent: group, title: "group milestone 1") }
+ let!(:milestone) { create(:milestone, resource_parent: group, title: "group milestone 2") }
it "deletes milestone" do
group.add_developer(user)
diff --git a/spec/features/projects/artifacts/user_browses_artifacts_spec.rb b/spec/features/projects/artifacts/user_browses_artifacts_spec.rb
index ecc07181d09..d8c6ef4755d 100644
--- a/spec/features/projects/artifacts/user_browses_artifacts_spec.rb
+++ b/spec/features/projects/artifacts/user_browses_artifacts_spec.rb
@@ -114,5 +114,24 @@ describe "User browses artifacts" do
it { expect(page).to have_link("doc_sample.txt").and have_no_selector(".js-artifact-tree-external-icon") }
end
+
+ context "when the project is private and pages access control is enabled" do
+ let!(:private_project) { create(:project, :private) }
+ let(:pipeline) { create(:ci_empty_pipeline, project: private_project) }
+ let(:job) { create(:ci_build, :artifacts, pipeline: pipeline) }
+ let(:user) { create(:user) }
+
+ before do
+ private_project.add_developer(user)
+
+ allow(Gitlab.config.pages).to receive(:access_control).and_return(true)
+
+ sign_in(user)
+
+ visit(browse_project_job_artifacts_path(private_project, job, "other_artifacts_0.1.2"))
+ end
+
+ it { expect(page).to have_link("doc_sample.txt").and have_selector(".js-artifact-tree-external-icon") }
+ end
end
end
diff --git a/spec/features/projects/branches_spec.rb b/spec/features/projects/branches_spec.rb
index b35067d0f4d..9dc0f7c90c2 100644
--- a/spec/features/projects/branches_spec.rb
+++ b/spec/features/projects/branches_spec.rb
@@ -101,7 +101,7 @@ describe 'Branches' do
visit project_branches_filtered_path(project, state: 'all')
expect(all('.all-branches').last).to have_selector('li', count: 20)
- accept_confirm { find('.js-branch-add-pdf-text-binary .btn-remove').click }
+ accept_confirm { first('.js-branch-item .btn-remove').click }
expect(all('.all-branches').last).to have_selector('li', count: 19)
end
@@ -246,7 +246,6 @@ describe 'Branches' do
end
expect(page).to have_content 'Commits'
- expect(page).to have_link 'Create merge request'
end
end
diff --git a/spec/features/projects/clusters/eks_spec.rb b/spec/features/projects/clusters/eks_spec.rb
new file mode 100644
index 00000000000..758dccd6e49
--- /dev/null
+++ b/spec/features/projects/clusters/eks_spec.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'AWS EKS Cluster', :js do
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+
+ before do
+ project.add_maintainer(user)
+ gitlab_sign_in(user)
+ allow(Projects::ClustersController).to receive(:STATUS_POLLING_INTERVAL) { 100 }
+ end
+
+ context 'when user does not have a cluster and visits cluster index page' do
+ let(:project_id) { 'test-project-1234' }
+
+ before do
+ visit project_clusters_path(project)
+
+ click_link 'Add Kubernetes cluster'
+ end
+
+ context 'when user creates a cluster on AWS EKS' do
+ before do
+ click_link 'Amazon EKS'
+ end
+
+ it 'user sees a form to create an EKS cluster' do
+ expect(page).to have_selector(:css, '.js-create-eks-cluster')
+ end
+ end
+ end
+end
diff --git a/spec/features/projects/clusters/gcp_spec.rb b/spec/features/projects/clusters/gcp_spec.rb
index a11237db508..b5ab9faa14b 100644
--- a/spec/features/projects/clusters/gcp_spec.rb
+++ b/spec/features/projects/clusters/gcp_spec.rb
@@ -177,6 +177,7 @@ describe 'Gcp Cluster', :js do
context 'when user has not dismissed GCP signup offer' do
before do
+ stub_feature_flags(create_eks_clusters: false)
visit project_clusters_path(project)
end
@@ -200,6 +201,7 @@ describe 'Gcp Cluster', :js do
context 'when user has dismissed GCP signup offer' do
before do
+ stub_feature_flags(create_eks_clusters: false)
visit project_clusters_path(project)
end
diff --git a/spec/features/projects/clusters_spec.rb b/spec/features/projects/clusters_spec.rb
index d1cd19dff2d..67d14d0a58a 100644
--- a/spec/features/projects/clusters_spec.rb
+++ b/spec/features/projects/clusters_spec.rb
@@ -74,7 +74,7 @@ describe 'Clusters', :js do
visit project_clusters_path(project)
click_link 'Add Kubernetes cluster'
- click_link 'Create new Cluster on GKE'
+ click_link 'Create new Cluster'
end
it 'user sees a link to create a GKE cluster' do
diff --git a/spec/features/projects/commits/user_browses_commits_spec.rb b/spec/features/projects/commits/user_browses_commits_spec.rb
index 085d8d63d52..131d9097f48 100644
--- a/spec/features/projects/commits/user_browses_commits_spec.rb
+++ b/spec/features/projects/commits/user_browses_commits_spec.rb
@@ -93,13 +93,13 @@ describe 'User browses commits' do
context 'when the blob does not exist' do
let(:commit) { create(:commit, project: project) }
- it 'shows a blank label' do
+ it 'renders successfully' do
allow_any_instance_of(Gitlab::Diff::File).to receive(:blob).and_return(nil)
allow_any_instance_of(Gitlab::Diff::File).to receive(:binary?).and_return(true)
visit(project_commit_path(project, commit))
- expect(find('.diff-file-changes', visible: false)).to have_content('No file name available')
+ expect(find('.diff-file-changes', visible: false)).to have_content('files/ruby/popen.rb')
end
end
diff --git a/spec/features/projects/compare_spec.rb b/spec/features/projects/compare_spec.rb
index 2674617bcfc..34bde29c8da 100644
--- a/spec/features/projects/compare_spec.rb
+++ b/spec/features/projects/compare_spec.rb
@@ -12,6 +12,23 @@ describe "Compare", :js do
end
describe "branches" do
+ shared_examples 'compares branches' do
+ it 'compares branches' do
+ visit project_compare_index_path(project, from: 'master', to: 'master')
+
+ select_using_dropdown 'from', 'feature'
+ expect(find('.js-compare-from-dropdown .dropdown-toggle-text')).to have_content('feature')
+
+ select_using_dropdown 'to', 'binary-encoding'
+ expect(find('.js-compare-to-dropdown .dropdown-toggle-text')).to have_content('binary-encoding')
+
+ click_button 'Compare'
+
+ expect(page).to have_content 'Commits'
+ expect(page).to have_link 'Create merge request'
+ end
+ end
+
it "pre-populates fields" do
visit project_compare_index_path(project, from: "master", to: "master")
@@ -19,19 +36,14 @@ describe "Compare", :js do
expect(find(".js-compare-to-dropdown .dropdown-toggle-text")).to have_content("master")
end
- it "compares branches" do
- visit project_compare_index_path(project, from: "master", to: "master")
-
- select_using_dropdown "from", "feature"
- expect(find(".js-compare-from-dropdown .dropdown-toggle-text")).to have_content("feature")
-
- select_using_dropdown "to", "binary-encoding"
- expect(find(".js-compare-to-dropdown .dropdown-toggle-text")).to have_content("binary-encoding")
+ it_behaves_like 'compares branches'
- click_button "Compare"
+ context 'on a read-only instance' do
+ before do
+ allow(Gitlab::Database).to receive(:read_only?).and_return(true)
+ end
- expect(page).to have_content "Commits"
- expect(page).to have_link 'Create merge request'
+ it_behaves_like 'compares branches'
end
it 'renders additions info when click unfold diff' do
diff --git a/spec/features/projects/environments/environment_spec.rb b/spec/features/projects/environments/environment_spec.rb
index 497880a7835..dd690699ff6 100644
--- a/spec/features/projects/environments/environment_spec.rb
+++ b/spec/features/projects/environments/environment_spec.rb
@@ -66,8 +66,8 @@ describe 'Environment' do
create(:deployment, :running, environment: environment, deployable: build)
end
- it 'does not show deployments' do
- expect(page).to have_content('You don\'t have any deployments right now.')
+ it 'does show deployments' do
+ expect(page).to have_link("#{build.name} (##{build.id})")
end
end
@@ -79,8 +79,8 @@ describe 'Environment' do
create(:deployment, :failed, environment: environment, deployable: build)
end
- it 'does not show deployments' do
- expect(page).to have_content('You don\'t have any deployments right now.')
+ it 'does show deployments' do
+ expect(page).to have_link("#{build.name} (##{build.id})")
end
end
@@ -175,7 +175,7 @@ describe 'Environment' do
#
# In EE we have to stub EE::Environment since it overwrites
# the "terminals" method.
- allow_any_instance_of(defined?(EE) ? EE::Environment : Environment)
+ allow_any_instance_of(Gitlab.ee? ? EE::Environment : Environment)
.to receive(:terminals) { nil }
visit terminal_project_environment_path(project, environment)
@@ -304,9 +304,11 @@ describe 'Environment' do
#
def remove_branch_with_hooks(project, user, branch)
params = {
- oldrev: project.commit(branch).id,
- newrev: Gitlab::Git::BLANK_SHA,
- ref: "refs/heads/#{branch}"
+ change: {
+ oldrev: project.commit(branch).id,
+ newrev: Gitlab::Git::BLANK_SHA,
+ ref: "refs/heads/#{branch}"
+ }
}
yield
diff --git a/spec/features/projects/features_visibility_spec.rb b/spec/features/projects/features_visibility_spec.rb
index 531592ffd65..9ec61743a11 100644
--- a/spec/features/projects/features_visibility_spec.rb
+++ b/spec/features/projects/features_visibility_spec.rb
@@ -211,7 +211,7 @@ describe 'Edit Project Settings' do
visit activity_project_path(project)
page.within(".event-filter") do
- expect(page).to have_selector("a", count: 2)
+ expect(page).to have_content("All")
expect(page).not_to have_content("Push events")
expect(page).not_to have_content("Merge events")
expect(page).not_to have_content("Comments")
diff --git a/spec/features/projects/files/dockerfile_dropdown_spec.rb b/spec/features/projects/files/dockerfile_dropdown_spec.rb
index 2e0c589e168..756f2f2d493 100644
--- a/spec/features/projects/files/dockerfile_dropdown_spec.rb
+++ b/spec/features/projects/files/dockerfile_dropdown_spec.rb
@@ -23,7 +23,7 @@ describe 'Projects > Files > User wants to add a Dockerfile file' do
wait_for_requests
- expect(page).to have_css('.dockerfile-selector .dropdown-toggle-text', text: 'HTTPd')
+ expect(page).to have_css('.dockerfile-selector .dropdown-toggle-text', text: 'Apply a template')
expect(page).to have_content('COPY ./ /usr/local/apache2/htdocs/')
end
end
diff --git a/spec/features/projects/files/gitignore_dropdown_spec.rb b/spec/features/projects/files/gitignore_dropdown_spec.rb
index dcb960b880a..a8c6e780d47 100644
--- a/spec/features/projects/files/gitignore_dropdown_spec.rb
+++ b/spec/features/projects/files/gitignore_dropdown_spec.rb
@@ -23,7 +23,7 @@ describe 'Projects > Files > User wants to add a .gitignore file' do
wait_for_requests
- expect(page).to have_css('.gitignore-selector .dropdown-toggle-text', text: 'Rails')
+ expect(page).to have_css('.gitignore-selector .dropdown-toggle-text', text: 'Apply a template')
expect(page).to have_content('/.bundle')
expect(page).to have_content('# Gemfile.lock, .ruby-version, .ruby-gemset')
end
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 875ae5d34d1..107d426a893 100644
--- a/spec/features/projects/files/gitlab_ci_yml_dropdown_spec.rb
+++ b/spec/features/projects/files/gitlab_ci_yml_dropdown_spec.rb
@@ -23,7 +23,7 @@ describe 'Projects > Files > User wants to add a .gitlab-ci.yml file' do
wait_for_requests
- expect(page).to have_css('.gitlab-ci-yml-selector .dropdown-toggle-text', text: 'Jekyll')
+ expect(page).to have_css('.gitlab-ci-yml-selector .dropdown-toggle-text', text: 'Apply a template')
expect(page).to have_content('This file is a template, and might need editing before it works on your project')
expect(page).to have_content('jekyll build -d test')
end
diff --git a/spec/features/projects/files/project_owner_creates_license_file_spec.rb b/spec/features/projects/files/project_owner_creates_license_file_spec.rb
index 2944089358f..943c6e0e959 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
@@ -64,7 +64,7 @@ describe 'Projects > Files > Project owner creates a license file', :js do
def select_template(template)
page.within('.js-license-selector-wrap') do
- click_button 'Apply a license template'
+ click_button 'Apply a template'
click_link template
wait_for_requests
end
diff --git a/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb b/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb
index 556b7227403..9f63b312146 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
@@ -37,7 +37,7 @@ describe 'Projects > Files > Project owner sees a link to create a license file
def select_template(template)
page.within('.js-license-selector-wrap') do
- click_button 'Apply a license template'
+ click_button 'Apply a template'
click_link template
wait_for_requests
end
diff --git a/spec/features/projects/files/template_type_dropdown_spec.rb b/spec/features/projects/files/template_type_dropdown_spec.rb
index 8b385185e2e..ba52a7e7deb 100644
--- a/spec/features/projects/files/template_type_dropdown_spec.rb
+++ b/spec/features/projects/files/template_type_dropdown_spec.rb
@@ -24,8 +24,9 @@ describe 'Projects > Files > Template type dropdown selector', :js do
try_selecting_all_types
end
- it 'updates toggle value when input matches' do
+ it 'updates template type toggle value when template is chosen' do
fill_in 'file_path', with: '.gitignore'
+ select_template('gitignore', 'Actionscript')
check_type_selector_toggle_text('.gitignore')
end
end
@@ -70,6 +71,7 @@ describe 'Projects > Files > Template type dropdown selector', :js do
end
it 'toggle is set to the correct value' do
+ select_template('gitignore', 'Actionscript')
check_type_selector_toggle_text('.gitignore')
end
@@ -88,7 +90,7 @@ describe 'Projects > Files > Template type dropdown selector', :js do
end
it 'toggle is set to the proper value' do
- check_type_selector_toggle_text('Choose type')
+ check_type_selector_toggle_text('Select a template type')
end
it 'selects every template type correctly' do
@@ -103,16 +105,15 @@ def check_type_selector_display(is_visible)
end
def try_selecting_all_types
- try_selecting_template_type('LICENSE', 'Apply a license template')
- try_selecting_template_type('Dockerfile', 'Apply a Dockerfile template')
- try_selecting_template_type('.gitlab-ci.yml', 'Apply a GitLab CI Yaml template')
- try_selecting_template_type('.gitignore', 'Apply a .gitignore template')
+ try_selecting_template_type('LICENSE', 'Apply a template')
+ try_selecting_template_type('Dockerfile', 'Apply a template')
+ try_selecting_template_type('.gitlab-ci.yml', 'Apply a template')
+ try_selecting_template_type('.gitignore', 'Apply a template')
end
def try_selecting_template_type(template_type, selector_label)
select_template_type(template_type)
check_template_selector_display(selector_label)
- check_type_selector_toggle_text(template_type)
end
def select_template_type(template_type)
@@ -120,6 +121,11 @@ def select_template_type(template_type)
find('.dropdown-content li', text: template_type).click
end
+def select_template(type, template)
+ find(".js-#{type}-selector-wrap").click
+ find('.dropdown-content li', text: template).click
+end
+
def check_template_selector_display(content)
expect(page).to have_content(content)
end
diff --git a/spec/features/projects/files/undo_template_spec.rb b/spec/features/projects/files/undo_template_spec.rb
index d3f8d36a0a9..887214e1dbf 100644
--- a/spec/features/projects/files/undo_template_spec.rb
+++ b/spec/features/projects/files/undo_template_spec.rb
@@ -13,11 +13,12 @@ describe 'Projects > Files > Template Undo Button', :js do
context 'editing a matching file and applying a template' do
before do
visit project_edit_blob_path(project, File.join(project.default_branch, "LICENSE"))
+ select_file_template_type('LICENSE')
select_file_template('.js-license-selector', 'Apache License 2.0')
end
it 'reverts template application' do
- try_template_undo('http://www.apache.org/licenses/', 'Apply a license template')
+ try_template_undo('http://www.apache.org/licenses/', 'Apply a template')
end
end
@@ -29,7 +30,7 @@ describe 'Projects > Files > Template Undo Button', :js do
end
it 'reverts template application' do
- try_template_undo('http://www.apache.org/licenses/', 'Apply a license template')
+ try_template_undo('http://www.apache.org/licenses/', 'Apply a template')
end
end
end
@@ -45,12 +46,12 @@ def check_toggle_text_set(neutral_toggle_text)
end
def check_undo_button_display
- expect(page).to have_content('Template applied')
- expect(page).to have_css('.template-selectors-undo-menu .btn-info')
+ expect(page).to have_content('template applied')
+ expect(page).to have_css('.toasted-container')
end
def check_content_reverted(template_content)
- find('.template-selectors-undo-menu .btn-info').click
+ find('.toasted-container a', text: 'Undo').click
expect(page).not_to have_content(template_content)
expect(page).to have_css('.template-type-selector .dropdown-toggle-text')
end
diff --git a/spec/features/projects/import_export/export_file_spec.rb b/spec/features/projects/import_export/export_file_spec.rb
index a1002f38936..7618a2bdea3 100644
--- a/spec/features/projects/import_export/export_file_spec.rb
+++ b/spec/features/projects/import_export/export_file_spec.rb
@@ -49,8 +49,7 @@ describe 'Import/Export - project export integration test', :js do
expect(page).to have_content('Download export')
- expect(file_permissions(project.export_path)).to eq(0700)
-
+ expect(project.export_status).to eq(:finished)
expect(project.export_file.path).to include('tar.gz')
in_directory_with_expanded_export(project) do |exit_status, tmpdir|
diff --git a/spec/features/projects/import_export/import_file_spec.rb b/spec/features/projects/import_export/import_file_spec.rb
index a12fc8b18ed..6f96da60a31 100644
--- a/spec/features/projects/import_export/import_file_spec.rb
+++ b/spec/features/projects/import_export/import_file_spec.rb
@@ -3,7 +3,6 @@
require 'spec_helper'
describe 'Import/Export - project import integration test', :js do
- include Select2Helper
include GitHelpers
let(:user) { create(:user) }
@@ -31,7 +30,6 @@ describe 'Import/Export - project import integration test', :js do
it 'user imports an exported project successfully' do
visit new_project_path
- select2(namespace.id, from: '#project_namespace_id')
fill_in :project_name, with: project_name, visible: true
click_import_project_tab
click_link 'GitLab export'
@@ -78,7 +76,6 @@ describe 'Import/Export - project import integration test', :js do
visit new_project_path
- select2(user.namespace.id, from: '#project_namespace_id')
fill_in :project_name, with: project.name, visible: true
click_import_project_tab
click_link 'GitLab export'
diff --git a/spec/features/projects/issuable_templates_spec.rb b/spec/features/projects/issuable_templates_spec.rb
index 18eadb7c4a3..4b6f1672f08 100644
--- a/spec/features/projects/issuable_templates_spec.rb
+++ b/spec/features/projects/issuable_templates_spec.rb
@@ -92,6 +92,9 @@ describe 'issuable templates', :js do
context 'user creates a merge request using templates' do
let(:template_content) { 'this is a test "feature-proposal" template' }
+ let(:bug_template_content) { 'this is merge request bug template' }
+ let(:template_override_warning) { 'Applying a template will replace the existing issue description.' }
+ let(:updated_description) { 'updated merge request description' }
let(:merge_request) { create(:merge_request, :with_diffs, source_project: project) }
before do
@@ -101,6 +104,12 @@ describe 'issuable templates', :js do
template_content,
message: 'added merge request template',
branch_name: 'master')
+ project.repository.create_file(
+ user,
+ '.gitlab/merge_request_templates/bug.md',
+ bug_template_content,
+ message: 'added merge request bug template',
+ branch_name: 'master')
visit edit_project_merge_request_path project, merge_request
fill_in :'merge_request[title]', with: 'test merge request title'
end
@@ -111,6 +120,34 @@ describe 'issuable templates', :js do
assert_template
save_changes
end
+
+ context 'changes template' do
+ before do
+ select_template 'bug'
+ wait_for_requests
+ fill_in :'merge_request[description]', with: updated_description
+ select_template 'feature-proposal'
+ expect(page).to have_content template_override_warning
+ end
+
+ it 'user selects "bug" template, then updates description, then selects "feature-proposal" template, then cancels template change' do
+ page.find('.js-template-warning .js-close-btn.js-cancel-btn').click
+ expect(find('textarea')['value']).to eq(updated_description)
+ expect(page).not_to have_content template_override_warning
+ end
+
+ it 'user selects "bug" template, then updates description, then selects "feature-proposal" template, then dismiss the template warning' do
+ page.find('.js-template-warning .js-close-btn.js-dismiss-btn').click
+ expect(find('textarea')['value']).to eq(updated_description)
+ expect(page).not_to have_content template_override_warning
+ end
+
+ it 'user selects "bug" template, then updates description, then selects "feature-proposal" template, then applies template change' do
+ page.find('.js-template-warning .js-override-template').click
+ wait_for_requests
+ assert_template
+ end
+ end
end
context 'user creates a merge request from a forked project using templates' do
diff --git a/spec/features/projects/jobs/permissions_spec.rb b/spec/features/projects/jobs/permissions_spec.rb
index 44309a9c4bf..ae506b66a86 100644
--- a/spec/features/projects/jobs/permissions_spec.rb
+++ b/spec/features/projects/jobs/permissions_spec.rb
@@ -10,6 +10,7 @@ describe 'Project Jobs Permissions' do
let!(:job) { create(:ci_build, :running, :coverage, :trace_artifact, pipeline: pipeline) }
before do
+ stub_feature_flags(job_log_json: true)
sign_in(user)
project.enable_ci
@@ -69,7 +70,7 @@ describe 'Project Jobs Permissions' do
it_behaves_like 'recent job page details responds with status', 200 do
it 'renders job details', :js do
expect(page).to have_content "Job ##{job.id}"
- expect(page).to have_css '.js-build-trace'
+ expect(page).to have_css '.log-line'
end
end
diff --git a/spec/features/projects/jobs/user_browses_job_spec.rb b/spec/features/projects/jobs/user_browses_job_spec.rb
index 4d8a4812123..856c39df8b3 100644
--- a/spec/features/projects/jobs/user_browses_job_spec.rb
+++ b/spec/features/projects/jobs/user_browses_job_spec.rb
@@ -38,86 +38,26 @@ describe 'User browses a job', :js do
expect(page).to have_content('Job has been erased')
end
- shared_examples 'has collapsible sections' do
- it 'collapses the section clicked' do
- wait_for_requests
- text_to_hide = "Cloning into '/nolith/ci-tests'"
- text_to_show = 'Waiting for pod'
-
- expect(page).to have_content(text_to_hide)
- expect(page).to have_content(text_to_show)
-
- first('.js-section-start[data-section="get-sources"]').click
-
- expect(page).not_to have_content(text_to_hide)
- expect(page).to have_content(text_to_show)
- end
-
- it 'collapses the section header clicked' do
- wait_for_requests
- text_to_hide = "Cloning into '/nolith/ci-tests'"
- text_to_show = 'Waiting for pod'
-
- expect(page).to have_content(text_to_hide)
- expect(page).to have_content(text_to_show)
-
- first('.js-section-header.js-s-get-sources').click
-
- expect(page).not_to have_content(text_to_hide)
- expect(page).to have_content(text_to_show)
- end
- end
-
- context 'when job trace contains sections' do
- let!(:build) { create(:ci_build, :success, :trace_with_sections, :coverage, pipeline: pipeline) }
-
- it_behaves_like 'has collapsible sections'
- end
-
- context 'when job trace contains duplicate sections' do
- let!(:build) { create(:ci_build, :success, :trace_with_duplicate_sections, :coverage, pipeline: pipeline) }
-
- it_behaves_like 'has collapsible sections'
- end
-
- context 'when job trace contains sections' do
- let!(:build) { create(:ci_build, :success, :trace_with_duplicate_sections, :coverage, pipeline: pipeline) }
-
- it 'collapses a section' do
- wait_for_requests
- text_to_hide = "Cloning into '/nolith/ci-tests'"
- text_to_show = 'Waiting for pod'
-
- expect(page).to have_content(text_to_hide)
- expect(page).to have_content(text_to_show)
-
- first('.js-section-start[data-section="get-sources"]').click
-
- expect(page).not_to have_content(text_to_hide)
- expect(page).to have_content(text_to_show)
- end
- end
-
context 'with a failed job' do
let!(:build) { create(:ci_build, :failed, :trace_artifact, pipeline: pipeline) }
it 'displays the failure reason' do
wait_for_all_requests
within('.builds-container') do
- build_link = first('.build-job > a')
- expect(build_link['data-original-title']).to eq('test - failed - (unknown failure)')
+ expect(page).to have_selector(
+ ".build-job > a[data-original-title='test - failed - (unknown failure)']")
end
end
end
context 'when a failed job has been retried' do
- let!(:build) { create(:ci_build, :failed, :retried, :trace_artifact, pipeline: pipeline) }
+ let!(:build_retried) { create(:ci_build, :failed, :retried, :trace_artifact, pipeline: pipeline) }
it 'displays the failure reason and retried label' do
wait_for_all_requests
within('.builds-container') do
- build_link = first('.build-job > a')
- expect(build_link['data-original-title']).to eq('test - failed - (unknown failure) (retried)')
+ expect(page).to have_selector(
+ ".build-job > a[data-original-title='test - failed - (unknown failure) (retried)']")
end
end
end
diff --git a/spec/features/projects/jobs_spec.rb b/spec/features/projects/jobs_spec.rb
index cebca338f33..f5d5bc7f5b9 100644
--- a/spec/features/projects/jobs_spec.rb
+++ b/spec/features/projects/jobs_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
require 'tempfile'
@@ -424,8 +426,8 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do
it 'loads job trace' do
expect(page).to have_content 'BUILD TRACE'
- job.trace.write('a+b') do |stream|
- stream.append(' and more trace', 11)
+ job.trace.write(+'a+b') do |stream|
+ stream.append(+' and more trace', 11)
end
expect(page).to have_content 'BUILD TRACE and more trace'
@@ -534,7 +536,7 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do
end
it 'shows deployment message' do
- expect(page).to have_content 'This job is the most recent deployment to production'
+ expect(page).to have_content 'This job is deployed to production'
expect(find('.js-environment-link')['href']).to match("environments/#{environment.id}")
end
@@ -548,14 +550,14 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do
end
it 'shows the name of the cluster' do
- expect(page).to have_content 'Cluster the-cluster was used'
+ expect(page).to have_content 'using cluster the-cluster'
end
context 'when the user is not able to view the cluster' do
let(:user_access_level) { :developer }
it 'includes only the name of the cluster without a link' do
- expect(page).to have_content 'Cluster the-cluster was used'
+ expect(page).to have_content 'using cluster the-cluster'
expect(page).not_to have_link 'the-cluster'
end
end
@@ -623,8 +625,7 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do
let!(:second_deployment) { create(:deployment, :success, environment: environment, deployable: second_build) }
it 'shows deployment message' do
- expected_text = 'This job is an out-of-date deployment ' \
- "to staging. View the most recent deployment ##{second_deployment.iid}."
+ expected_text = 'This job is an out-of-date deployment to staging. View the most recent deployment.'
expect(page).to have_css('.environment-information', text: expected_text)
end
diff --git a/spec/features/projects/members/group_members_spec.rb b/spec/features/projects/members/group_members_spec.rb
index dd5fc82e058..4ecc3db78b3 100644
--- a/spec/features/projects/members/group_members_spec.rb
+++ b/spec/features/projects/members/group_members_spec.rb
@@ -5,8 +5,8 @@ require 'spec_helper'
describe 'Projects members' do
let(:user) { create(:user) }
let(:developer) { create(:user) }
- let(:group) { create(:group, :public, :access_requestable) }
- let(:project) { create(:project, :public, :access_requestable, creator: user, group: group) }
+ let(:group) { create(:group, :public) }
+ let(:project) { create(:project, :public, creator: user, group: group) }
let(:project_invitee) { create(:project_member, project: project, invite_token: '123', invite_email: 'test1@abc.com', user: nil) }
let(:group_invitee) { create(:group_member, group: group, invite_token: '123', invite_email: 'test2@abc.com', user: nil) }
let(:project_requester) { create(:user) }
diff --git a/spec/features/projects/members/group_requester_cannot_request_access_to_project_spec.rb b/spec/features/projects/members/group_requester_cannot_request_access_to_project_spec.rb
index fb4238f0a1f..ecd55f71c84 100644
--- a/spec/features/projects/members/group_requester_cannot_request_access_to_project_spec.rb
+++ b/spec/features/projects/members/group_requester_cannot_request_access_to_project_spec.rb
@@ -5,8 +5,8 @@ require 'spec_helper'
describe 'Projects > Members > Group requester cannot request access to project', :js do
let(:user) { create(:user) }
let(:owner) { create(:user) }
- let(:group) { create(:group, :public, :access_requestable) }
- let(:project) { create(:project, :public, :access_requestable, namespace: group) }
+ let(:group) { create(:group, :public) }
+ let(:project) { create(:project, :public, namespace: group) }
before do
group.add_owner(owner)
diff --git a/spec/features/projects/members/master_manages_access_requests_spec.rb b/spec/features/projects/members/master_manages_access_requests_spec.rb
index 17d6efbcaa5..f113fb643f8 100644
--- a/spec/features/projects/members/master_manages_access_requests_spec.rb
+++ b/spec/features/projects/members/master_manages_access_requests_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
describe 'Projects > Members > Maintainer manages access requests' do
it_behaves_like 'Maintainer manages access requests' do
- let(:entity) { create(:project, :public, :access_requestable) }
+ let(:entity) { create(:project, :public) }
let(:members_page_path) { project_project_members_path(entity) }
end
end
diff --git a/spec/features/projects/members/user_requests_access_spec.rb b/spec/features/projects/members/user_requests_access_spec.rb
index 9f7327cd6e4..a77f0bdcbe9 100644
--- a/spec/features/projects/members/user_requests_access_spec.rb
+++ b/spec/features/projects/members/user_requests_access_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
describe 'Projects > Members > User requests access', :js do
let(:user) { create(:user) }
- let(:project) { create(:project, :public, :access_requestable, :repository) }
+ let(:project) { create(:project, :public, :repository) }
let(:maintainer) { project.owner }
before do
diff --git a/spec/features/projects/pipelines/pipeline_spec.rb b/spec/features/projects/pipelines/pipeline_spec.rb
index 9759fd04ad2..04adb1ec6af 100644
--- a/spec/features/projects/pipelines/pipeline_spec.rb
+++ b/spec/features/projects/pipelines/pipeline_spec.rb
@@ -98,6 +98,16 @@ describe 'Pipeline', :js do
end
end
+ it 'shows links to the related merge requests' do
+ visit_pipeline
+
+ within '.related-merge-request-info' do
+ pipeline.all_merge_requests.map do |merge_request|
+ expect(page).to have_link(project_merge_request_path(project, merge_request))
+ end
+ end
+ end
+
it_behaves_like 'showing user status' do
let(:user_with_status) { pipeline.user }
diff --git a/spec/features/projects/settings/repository_settings_spec.rb b/spec/features/projects/settings/repository_settings_spec.rb
index 1294c8822b6..18031a40f15 100644
--- a/spec/features/projects/settings/repository_settings_spec.rb
+++ b/spec/features/projects/settings/repository_settings_spec.rb
@@ -66,6 +66,19 @@ describe 'Projects > Settings > Repository settings' do
expect(page).to have_content('Write access allowed')
end
+ it 'edit an existing public deploy key to be writable' do
+ project.deploy_keys << public_deploy_key
+ visit project_settings_repository_path(project)
+
+ find('.deploy-key', text: public_deploy_key.title).find('.ic-pencil').click
+
+ check 'deploy_key_deploy_keys_projects_attributes_0_can_push'
+ click_button 'Save changes'
+
+ expect(page).to have_content('public_deploy_key')
+ expect(page).to have_content('Write access allowed')
+ end
+
it 'edit a deploy key from projects user has access to' do
project2 = create(:project_empty_repo)
project2.add_role(user, role)
diff --git a/spec/features/protected_branches_spec.rb b/spec/features/protected_branches_spec.rb
index 80937223016..36c5a116b66 100644
--- a/spec/features/protected_branches_spec.rb
+++ b/spec/features/protected_branches_spec.rb
@@ -92,7 +92,10 @@ describe 'Protected Branches', :js do
set_protected_branch_name('some-branch')
click_on "Protect"
- within(".protected-branches-list") { expect(page).to have_content(commit.id[0..7]) }
+ within(".protected-branches-list") do
+ expect(page).not_to have_content("matching")
+ expect(page).not_to have_content("was deleted")
+ end
end
it "displays an error message if the named branch does not exist" do
@@ -101,7 +104,7 @@ describe 'Protected Branches', :js do
set_protected_branch_name('some-branch')
click_on "Protect"
- within(".protected-branches-list") { expect(page).to have_content('branch was deleted') }
+ within(".protected-branches-list") { expect(page).to have_content('Branch was deleted') }
end
end
@@ -127,7 +130,6 @@ describe 'Protected Branches', :js do
click_on "Protect"
within(".protected-branches-list") do
- expect(page).to have_content("Protected branch (2)")
expect(page).to have_content("2 matching branches")
end
end
diff --git a/spec/features/runners_spec.rb b/spec/features/runners_spec.rb
index 63d21d94b5f..0049d3ca7c9 100644
--- a/spec/features/runners_spec.rb
+++ b/spec/features/runners_spec.rb
@@ -272,6 +272,12 @@ describe 'Runners' do
expect(page).to have_content 'This group does not provide any group Runners yet'
end
+
+ it 'user can see a link to install runners on kubernetes clusters' do
+ visit group_settings_ci_cd_path(group)
+
+ expect(page).to have_link('Install Runner on Kubernetes', href: group_clusters_path(group))
+ end
end
context 'group with a runner' do
diff --git a/spec/features/search/user_searches_for_code_spec.rb b/spec/features/search/user_searches_for_code_spec.rb
index 9451ee6eb15..9949595fddf 100644
--- a/spec/features/search/user_searches_for_code_spec.rb
+++ b/spec/features/search/user_searches_for_code_spec.rb
@@ -94,6 +94,13 @@ describe 'User searches for code' do
expect(page).to have_selector('.results', text: 'path = gitlab-grack')
end
+
+ it 'persist refs over browser tabs' do
+ ref = 'feature'
+ find('.js-project-refs-dropdown').click
+ link = find_link(ref)[:href]
+ expect(link.include?("repository_ref=" + ref)).to be(true)
+ end
end
it 'no ref switcher shown in issue result summary', :js do
diff --git a/spec/features/security/group/internal_access_spec.rb b/spec/features/security/group/internal_access_spec.rb
index d6575ec9de1..a182b6b9d57 100644
--- a/spec/features/security/group/internal_access_spec.rb
+++ b/spec/features/security/group/internal_access_spec.rb
@@ -16,6 +16,7 @@ describe 'Internal Group access' do
describe "Group should be internal" do
describe '#internal?' do
subject { group.internal? }
+
it { is_expected.to be_truthy }
end
end
diff --git a/spec/features/security/group/private_access_spec.rb b/spec/features/security/group/private_access_spec.rb
index 2dc863a6e73..5e3e9824aaa 100644
--- a/spec/features/security/group/private_access_spec.rb
+++ b/spec/features/security/group/private_access_spec.rb
@@ -16,6 +16,7 @@ describe 'Private Group access' do
describe "Group should be private" do
describe '#private?' do
subject { group.private? }
+
it { is_expected.to be_truthy }
end
end
diff --git a/spec/features/security/group/public_access_spec.rb b/spec/features/security/group/public_access_spec.rb
index 4066a19fce2..efc84205980 100644
--- a/spec/features/security/group/public_access_spec.rb
+++ b/spec/features/security/group/public_access_spec.rb
@@ -16,6 +16,7 @@ describe 'Public Group access' do
describe "Group should be public" do
describe '#public?' do
subject { group.public? }
+
it { is_expected.to be_truthy }
end
end
diff --git a/spec/features/security/project/internal_access_spec.rb b/spec/features/security/project/internal_access_spec.rb
index d089fa718d2..768b883a90e 100644
--- a/spec/features/security/project/internal_access_spec.rb
+++ b/spec/features/security/project/internal_access_spec.rb
@@ -14,6 +14,7 @@ describe "Internal Project Access" do
describe "Project should be internal" do
describe '#internal?' do
subject { project.internal? }
+
it { is_expected.to be_truthy }
end
end
diff --git a/spec/features/security/project/private_access_spec.rb b/spec/features/security/project/private_access_spec.rb
index b868cd595cb..c2d44c05a22 100644
--- a/spec/features/security/project/private_access_spec.rb
+++ b/spec/features/security/project/private_access_spec.rb
@@ -14,6 +14,7 @@ describe "Private Project Access" do
describe "Project should be private" do
describe '#private?' do
subject { project.private? }
+
it { is_expected.to be_truthy }
end
end
diff --git a/spec/features/security/project/public_access_spec.rb b/spec/features/security/project/public_access_spec.rb
index 8db2f2d69e5..19f01257713 100644
--- a/spec/features/security/project/public_access_spec.rb
+++ b/spec/features/security/project/public_access_spec.rb
@@ -14,6 +14,7 @@ describe "Public Project Access" do
describe "Project should be public" do
describe '#public?' do
subject { project.public? }
+
it { is_expected.to be_truthy }
end
end
diff --git a/spec/features/snippets/private_snippets_spec.rb b/spec/features/snippets/private_snippets_spec.rb
new file mode 100644
index 00000000000..9df4cd01103
--- /dev/null
+++ b/spec/features/snippets/private_snippets_spec.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'Private Snippets', :js do
+ let(:user) { create(:user) }
+
+ before do
+ sign_in(user)
+ end
+
+ it 'Private Snippet renders for creator' do
+ private_snippet = create(:personal_snippet, :private, author: user)
+
+ visit snippet_path(private_snippet)
+ wait_for_requests
+
+ expect(page).to have_content(private_snippet.content)
+ expect(page).not_to have_css('.js-embed-btn')
+ expect(page).not_to have_css('.js-share-btn')
+ end
+end
diff --git a/spec/features/snippets/public_snippets_spec.rb b/spec/features/snippets/public_snippets_spec.rb
index a0db00cfc67..82edda509c2 100644
--- a/spec/features/snippets/public_snippets_spec.rb
+++ b/spec/features/snippets/public_snippets_spec.rb
@@ -10,6 +10,8 @@ describe 'Public Snippets', :js do
wait_for_requests
expect(page).to have_content(public_snippet.content)
+ expect(page).to have_css('.js-embed-btn', visible: false)
+ expect(page).to have_css('.js-share-btn', visible: false)
end
it 'Unauthenticated user should see raw public snippets' do
diff --git a/spec/features/snippets/spam_snippets_spec.rb b/spec/features/snippets/spam_snippets_spec.rb
new file mode 100644
index 00000000000..3e71a4e7879
--- /dev/null
+++ b/spec/features/snippets/spam_snippets_spec.rb
@@ -0,0 +1,73 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'User creates snippet', :js do
+ let(:user) { create(:user) }
+
+ before do
+ stub_feature_flags(allow_possible_spam: false)
+ stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false')
+
+ Gitlab::CurrentSettings.update!(
+ akismet_enabled: true,
+ akismet_api_key: 'testkey',
+ recaptcha_enabled: true,
+ recaptcha_site_key: 'test site key',
+ recaptcha_private_key: 'test private key'
+ )
+
+ sign_in(user)
+ visit new_snippet_path
+
+ fill_in 'personal_snippet_title', with: 'My Snippet Title'
+ fill_in 'personal_snippet_description', with: 'My Snippet **Description**'
+ find('#personal_snippet_visibility_level_20').set(true)
+ page.within('.file-editor') do
+ find('.ace_text-input', visible: false).send_keys 'Hello World!'
+ end
+ end
+
+ shared_examples 'solve recaptcha' do
+ it 'creates a snippet after solving reCaptcha' do
+ click_button('Create snippet')
+ wait_for_requests
+
+ # it is impossible to test recaptcha automatically and there is no possibility to fill in recaptcha
+ # recaptcha verification is skipped in test environment and it always returns true
+ expect(page).not_to have_content('My Snippet Title')
+ expect(page).to have_css('.recaptcha')
+ click_button('Submit personal snippet')
+
+ expect(page).to have_content('My Snippet Title')
+ end
+ end
+
+ context 'when identified as a spam' do
+ before do
+ WebMock.stub_request(:any, /.*akismet.com.*/).to_return(body: "true", status: 200)
+ end
+
+ context 'when allow_possible_spam feature flag is false' do
+ it_behaves_like 'solve recaptcha'
+ end
+
+ context 'when allow_possible_spam feature flag is true' do
+ it_behaves_like 'solve recaptcha'
+ end
+ end
+
+ context 'when not identified as a spam' do
+ before do
+ WebMock.stub_request(:any, /.*akismet.com.*/).to_return(body: "false", status: 200)
+ end
+
+ it 'creates a snippet' do
+ click_button('Create snippet')
+ wait_for_requests
+
+ expect(page).not_to have_css('.recaptcha')
+ expect(page).to have_content('My Snippet Title')
+ end
+ end
+end
diff --git a/spec/features/snippets/user_creates_snippet_spec.rb b/spec/features/snippets/user_creates_snippet_spec.rb
index 52ec5eddd5c..9a141dd463a 100644
--- a/spec/features/snippets/user_creates_snippet_spec.rb
+++ b/spec/features/snippets/user_creates_snippet_spec.rb
@@ -45,7 +45,9 @@ describe 'User creates snippet', :js do
link = find('a.no-attachment-icon img[alt="banana_sample"]')['src']
expect(link).to match(%r{/uploads/-/system/user/#{user.id}/\h{32}/banana_sample\.gif\z})
- reqs = inspect_requests { visit(link) }
+ # Adds a cache buster for checking if the image exists as Selenium is now handling the cached regquests
+ # not anymore as requests when they come straight from memory cache.
+ reqs = inspect_requests { visit("#{link}?ran=#{SecureRandom.base64(20)}") }
expect(reqs.first.status_code).to eq(200)
end
end
@@ -63,7 +65,7 @@ describe 'User creates snippet', :js do
link = find('a.no-attachment-icon img[alt="banana_sample"]')['src']
expect(link).to match(%r{/uploads/-/system/personal_snippet/#{Snippet.last.id}/\h{32}/banana_sample\.gif\z})
- reqs = inspect_requests { visit(link) }
+ reqs = inspect_requests { visit("#{link}?ran=#{SecureRandom.base64(20)}") }
expect(reqs.first.status_code).to eq(200)
end
@@ -88,7 +90,7 @@ describe 'User creates snippet', :js do
link = find('a.no-attachment-icon img[alt="banana_sample"]')['src']
expect(link).to match(%r{/uploads/-/system/personal_snippet/#{Snippet.last.id}/\h{32}/banana_sample\.gif\z})
- reqs = inspect_requests { visit(link) }
+ reqs = inspect_requests { visit("#{link}?ran=#{SecureRandom.base64(20)}") }
expect(reqs.first.status_code).to eq(200)
end
diff --git a/spec/features/users/signup_spec.rb b/spec/features/users/signup_spec.rb
index fb927a9ca3b..562d6fcab1b 100644
--- a/spec/features/users/signup_spec.rb
+++ b/spec/features/users/signup_spec.rb
@@ -2,19 +2,14 @@
require 'spec_helper'
-describe 'Signup' do
+shared_examples 'Signup' do
include TermsHelper
- before do
- stub_feature_flags(invisible_captcha: false)
- end
-
let(:new_user) { build_stubbed(:user) }
describe 'username validation', :js do
before do
- visit root_path
- click_link 'Register'
+ visit new_user_registration_path
end
it 'does not show an error border if the username is available' do
@@ -130,36 +125,43 @@ describe 'Signup' do
describe 'user\'s full name validation', :js do
before do
- visit root_path
- click_link 'Register'
+ if Gitlab::Experimentation.enabled?(:signup_flow)
+ user = create(:user, role: nil)
+ sign_in(user)
+ visit users_sign_up_welcome_path
+ @user_name_field = 'user_name'
+ else
+ visit new_user_registration_path
+ @user_name_field = 'new_user_name'
+ end
end
it 'does not show an error border if the user\'s fullname length is not longer than 128 characters' do
- fill_in 'new_user_name', with: 'u' * 128
+ fill_in @user_name_field, with: 'u' * 128
expect(find('.name')).not_to have_css '.gl-field-error-outline'
end
it 'shows an error border if the user\'s fullname contains an emoji' do
- simulate_input('#new_user_name', 'Ehsan 🦋')
+ simulate_input("##{@user_name_field}", 'Ehsan 🦋')
expect(find('.name')).to have_css '.gl-field-error-outline'
end
it 'shows an error border if the user\'s fullname is longer than 128 characters' do
- fill_in 'new_user_name', with: 'n' * 129
+ fill_in @user_name_field, with: 'n' * 129
expect(find('.name')).to have_css '.gl-field-error-outline'
end
it 'shows an error message if the user\'s fullname is longer than 128 characters' do
- fill_in 'new_user_name', with: 'n' * 129
+ fill_in @user_name_field, with: 'n' * 129
expect(page).to have_content("Name is too long (maximum is 128 characters).")
end
it 'shows an error message if the username contains emojis' do
- simulate_input('#new_user_name', 'Ehsan 🦋')
+ simulate_input("##{@user_name_field}", 'Ehsan 🦋')
expect(page).to have_content("Invalid input, please avoid emojis")
end
@@ -177,13 +179,17 @@ describe 'Signup' do
end
it 'creates the user account and sends a confirmation email' do
- visit root_path
+ visit new_user_registration_path
+
+ fill_in 'new_user_username', with: new_user.username
+ fill_in 'new_user_email', with: new_user.email
- fill_in 'new_user_name', with: new_user.name
- fill_in 'new_user_username', with: new_user.username
- fill_in 'new_user_email', with: new_user.email
- fill_in 'new_user_email_confirmation', with: new_user.email
- fill_in 'new_user_password', with: new_user.password
+ unless Gitlab::Experimentation.enabled?(:signup_flow)
+ fill_in 'new_user_name', with: new_user.name
+ fill_in 'new_user_email_confirmation', with: new_user.email
+ end
+
+ fill_in 'new_user_password', with: new_user.password
expect { click_button 'Register' }.to change { User.count }.by(1)
@@ -198,35 +204,51 @@ describe 'Signup' do
end
it 'creates the user account and sends a confirmation email' do
- visit root_path
+ visit new_user_registration_path
+
+ fill_in 'new_user_username', with: new_user.username
+ fill_in 'new_user_email', with: new_user.email
+
+ unless Gitlab::Experimentation.enabled?(:signup_flow)
+ fill_in 'new_user_name', with: new_user.name
+ fill_in 'new_user_email_confirmation', with: new_user.email
+ end
- fill_in 'new_user_name', with: new_user.name
- fill_in 'new_user_username', with: new_user.username
- fill_in 'new_user_email', with: new_user.email
- fill_in 'new_user_email_confirmation', with: new_user.email
- fill_in 'new_user_password', with: new_user.password
+ fill_in 'new_user_password', with: new_user.password
expect { click_button 'Register' }.to change { User.count }.by(1)
- expect(current_path).to eq dashboard_projects_path
- expect(page).to have_content("Please check your email (#{new_user.email}) to verify that you own this address.")
+ if Gitlab::Experimentation.enabled?(:signup_flow)
+ expect(current_path).to eq users_sign_up_welcome_path
+ else
+ expect(current_path).to eq dashboard_projects_path
+ expect(page).to have_content("Please check your email (#{new_user.email}) to verify that you own this address.")
+ end
end
end
end
context "when sigining up with different cased emails" do
it "creates the user successfully" do
- visit root_path
+ visit new_user_registration_path
+
+ fill_in 'new_user_username', with: new_user.username
+ fill_in 'new_user_email', with: new_user.email
- fill_in 'new_user_name', with: new_user.name
- fill_in 'new_user_username', with: new_user.username
- fill_in 'new_user_email', with: new_user.email
- fill_in 'new_user_email_confirmation', with: new_user.email.capitalize
- fill_in 'new_user_password', with: new_user.password
+ unless Gitlab::Experimentation.enabled?(:signup_flow)
+ fill_in 'new_user_name', with: new_user.name
+ fill_in 'new_user_email_confirmation', with: new_user.email.capitalize
+ end
+
+ fill_in 'new_user_password', with: new_user.password
click_button "Register"
- expect(current_path).to eq dashboard_projects_path
- expect(page).to have_content("Welcome! You have signed up successfully.")
+ if Gitlab::Experimentation.enabled?(:signup_flow)
+ expect(current_path).to eq users_sign_up_welcome_path
+ else
+ expect(current_path).to eq dashboard_projects_path
+ expect(page).to have_content("Welcome! You have signed up successfully.")
+ end
end
end
@@ -236,17 +258,25 @@ describe 'Signup' do
end
it 'creates the user account and goes to dashboard' do
- visit root_path
+ visit new_user_registration_path
- fill_in 'new_user_name', with: new_user.name
- fill_in 'new_user_username', with: new_user.username
- fill_in 'new_user_email', with: new_user.email
- fill_in 'new_user_email_confirmation', with: new_user.email
- fill_in 'new_user_password', with: new_user.password
+ fill_in 'new_user_username', with: new_user.username
+ fill_in 'new_user_email', with: new_user.email
+
+ unless Gitlab::Experimentation.enabled?(:signup_flow)
+ fill_in 'new_user_name', with: new_user.name
+ fill_in 'new_user_email_confirmation', with: new_user.email
+ end
+
+ fill_in 'new_user_password', with: new_user.password
click_button "Register"
- expect(current_path).to eq dashboard_projects_path
- expect(page).to have_content("Welcome! You have signed up successfully.")
+ if Gitlab::Experimentation.enabled?(:signup_flow)
+ expect(current_path).to eq users_sign_up_welcome_path
+ else
+ expect(current_path).to eq dashboard_projects_path
+ expect(page).to have_content("Welcome! You have signed up successfully.")
+ end
end
end
end
@@ -255,28 +285,40 @@ describe 'Signup' do
it "displays the errors" do
existing_user = create(:user)
- visit root_path
+ visit new_user_registration_path
+
+ unless Gitlab::Experimentation.enabled?(:signup_flow)
+ fill_in 'new_user_name', with: new_user.name
+ end
- fill_in 'new_user_name', with: new_user.name
fill_in 'new_user_username', with: new_user.username
- fill_in 'new_user_email', with: existing_user.email
+ fill_in 'new_user_email', with: existing_user.email
fill_in 'new_user_password', with: new_user.password
click_button "Register"
expect(current_path).to eq user_registration_path
- expect(page).to have_content("errors prohibited this user from being saved")
+
+ if Gitlab::Experimentation.enabled?(:signup_flow)
+ expect(page).to have_content("error prohibited this user from being saved")
+ else
+ expect(page).to have_content("errors prohibited this user from being saved")
+ expect(page).to have_content("Email confirmation doesn't match")
+ end
+
expect(page).to have_content("Email has already been taken")
- expect(page).to have_content("Email confirmation doesn't match")
end
it 'does not redisplay the password' do
existing_user = create(:user)
- visit root_path
+ visit new_user_registration_path
+
+ unless Gitlab::Experimentation.enabled?(:signup_flow)
+ fill_in 'new_user_name', with: new_user.name
+ end
- fill_in 'new_user_name', with: new_user.name
fill_in 'new_user_username', with: new_user.username
- fill_in 'new_user_email', with: existing_user.email
+ fill_in 'new_user_email', with: existing_user.email
fill_in 'new_user_password', with: new_user.password
click_button "Register"
@@ -291,13 +333,17 @@ describe 'Signup' do
end
it 'requires the user to check the checkbox' do
- visit root_path
+ visit new_user_registration_path
- fill_in 'new_user_name', with: new_user.name
- fill_in 'new_user_username', with: new_user.username
- fill_in 'new_user_email', with: new_user.email
- fill_in 'new_user_email_confirmation', with: new_user.email
- fill_in 'new_user_password', with: new_user.password
+ fill_in 'new_user_username', with: new_user.username
+ fill_in 'new_user_email', with: new_user.email
+
+ unless Gitlab::Experimentation.enabled?(:signup_flow)
+ fill_in 'new_user_name', with: new_user.name
+ fill_in 'new_user_email_confirmation', with: new_user.email
+ end
+
+ fill_in 'new_user_password', with: new_user.password
click_button 'Register'
@@ -306,18 +352,99 @@ describe 'Signup' do
end
it 'asks the user to accept terms before going to the dashboard' do
- visit root_path
+ visit new_user_registration_path
- fill_in 'new_user_name', with: new_user.name
- fill_in 'new_user_username', with: new_user.username
- fill_in 'new_user_email', with: new_user.email
- fill_in 'new_user_email_confirmation', with: new_user.email
- fill_in 'new_user_password', with: new_user.password
+ fill_in 'new_user_username', with: new_user.username
+ fill_in 'new_user_email', with: new_user.email
+
+ unless Gitlab::Experimentation.enabled?(:signup_flow)
+ fill_in 'new_user_name', with: new_user.name
+ fill_in 'new_user_email_confirmation', with: new_user.email
+ end
+
+ fill_in 'new_user_password', with: new_user.password
check :terms_opt_in
click_button "Register"
- expect(current_path).to eq dashboard_projects_path
+ if Gitlab::Experimentation.enabled?(:signup_flow)
+ expect(current_path).to eq users_sign_up_welcome_path
+ else
+ expect(current_path).to eq dashboard_projects_path
+ end
+ end
+ end
+
+ context 'when reCAPTCHA and invisible captcha are enabled' do
+ before do
+ InvisibleCaptcha.timestamp_enabled = true
+ stub_application_setting(recaptcha_enabled: true)
+ allow_any_instance_of(RegistrationsController).to receive(:verify_recaptcha).and_return(false)
+ end
+
+ after do
+ InvisibleCaptcha.timestamp_enabled = false
+ end
+
+ it 'prevents from signing up' do
+ visit new_user_registration_path
+
+ fill_in 'new_user_username', with: new_user.username
+ fill_in 'new_user_email', with: new_user.email
+
+ unless Gitlab::Experimentation.enabled?(:signup_flow)
+ fill_in 'new_user_name', with: new_user.name
+ fill_in 'new_user_email_confirmation', with: new_user.email
+ end
+
+ fill_in 'new_user_password', with: new_user.password
+
+ expect { click_button 'Register' }.not_to change { User.count }
+
+ if Gitlab::Experimentation.enabled?(:signup_flow)
+ expect(page).to have_content('That was a bit too quick! Please resubmit.')
+ else
+ expect(page).to have_content('There was an error with the reCAPTCHA. Please solve the reCAPTCHA again.')
+ end
+ end
+ end
+end
+
+describe 'With original flow' do
+ before do
+ stub_experiment(signup_flow: false)
+ end
+
+ it_behaves_like 'Signup'
+end
+
+describe 'With experimental flow' do
+ before do
+ stub_experiment(signup_flow: true)
+ end
+
+ it_behaves_like 'Signup'
+
+ describe 'when role is required' do
+ it 'after registering, it redirects to step 2 of the signup process, sets the name and role and then redirects to the original requested url' do
+ new_user = build_stubbed(:user)
+ visit new_user_registration_path
+ fill_in 'new_user_username', with: new_user.username
+ fill_in 'new_user_email', with: new_user.email
+ fill_in 'new_user_password', with: new_user.password
+ click_button 'Register'
+ visit new_project_path
+
+ expect(page).to have_current_path(users_sign_up_welcome_path)
+
+ fill_in 'user_name', with: 'New name'
+ select 'Software Developer', from: 'user_role'
+ click_button 'Get started!'
+ new_user = User.find_by_username(new_user.username)
+
+ expect(new_user.name).to eq 'New name'
+ expect(new_user.software_developer_role?).to be_truthy
+ expect(page).to have_current_path(new_project_path)
end
end
end
diff --git a/spec/finders/access_requests_finder_spec.rb b/spec/finders/access_requests_finder_spec.rb
index 605777462bb..fbfc8035bcc 100644
--- a/spec/finders/access_requests_finder_spec.rb
+++ b/spec/finders/access_requests_finder_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe AccessRequestsFinder do
@@ -5,13 +7,13 @@ describe AccessRequestsFinder do
let(:access_requester) { create(:user) }
let(:project) do
- create(:project, :public, :access_requestable) do |project|
+ create(:project, :public) do |project|
project.request_access(access_requester)
end
end
let(:group) do
- create(:group, :public, :access_requestable) do |group|
+ create(:group, :public) do |group|
group.request_access(access_requester)
end
end
diff --git a/spec/finders/admin/projects_finder_spec.rb b/spec/finders/admin/projects_finder_spec.rb
index 44cc8debd04..eb5d0bba183 100644
--- a/spec/finders/admin/projects_finder_spec.rb
+++ b/spec/finders/admin/projects_finder_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Admin::ProjectsFinder do
diff --git a/spec/finders/artifacts_finder_spec.rb b/spec/finders/artifacts_finder_spec.rb
new file mode 100644
index 00000000000..b956e2c9515
--- /dev/null
+++ b/spec/finders/artifacts_finder_spec.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe ArtifactsFinder do
+ let(:project) { create(:project) }
+
+ describe '#execute' do
+ before do
+ create(:ci_build, :artifacts, project: project)
+ end
+
+ subject { described_class.new(project, params).execute }
+
+ context 'with empty params' do
+ let(:params) { {} }
+
+ it 'returns all artifacts belonging to the project' do
+ expect(subject).to contain_exactly(*project.job_artifacts)
+ end
+ end
+
+ context 'with sort param' do
+ let(:params) { { sort: 'size_desc' } }
+
+ it 'sorts the artifacts' do
+ expect(subject).to eq(project.job_artifacts.order_by('size_desc'))
+ end
+ end
+ end
+end
diff --git a/spec/finders/autocomplete/move_to_project_finder_spec.rb b/spec/finders/autocomplete/move_to_project_finder_spec.rb
index 4a87b47bd08..f997dd32c40 100644
--- a/spec/finders/autocomplete/move_to_project_finder_spec.rb
+++ b/spec/finders/autocomplete/move_to_project_finder_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Autocomplete::MoveToProjectFinder do
diff --git a/spec/finders/autocomplete/users_finder_spec.rb b/spec/finders/autocomplete/users_finder_spec.rb
index f3b54ca0461..5d340c46114 100644
--- a/spec/finders/autocomplete/users_finder_spec.rb
+++ b/spec/finders/autocomplete/users_finder_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Autocomplete::UsersFinder do
diff --git a/spec/finders/boards/visits_finder_spec.rb b/spec/finders/boards/visits_finder_spec.rb
index 4d40f4826f8..7e3ad8aa9f0 100644
--- a/spec/finders/boards/visits_finder_spec.rb
+++ b/spec/finders/boards/visits_finder_spec.rb
@@ -10,7 +10,7 @@ describe Boards::VisitsFinder do
let(:project) { create(:project) }
let(:project_board) { create(:board, project: project) }
- subject(:finder) { described_class.new(project_board.parent, user) }
+ subject(:finder) { described_class.new(project_board.resource_parent, user) }
it 'returns nil when there is no user' do
finder.current_user = nil
@@ -27,7 +27,7 @@ describe Boards::VisitsFinder do
it 'queries for last N visits' do
expect(BoardProjectRecentVisit).to receive(:latest).with(user, project, count: 5).once
- described_class.new(project_board.parent, user).latest(5)
+ described_class.new(project_board.resource_parent, user).latest(5)
end
end
@@ -35,7 +35,7 @@ describe Boards::VisitsFinder do
let(:group) { create(:group) }
let(:group_board) { create(:board, group: group) }
- subject(:finder) { described_class.new(group_board.parent, user) }
+ subject(:finder) { described_class.new(group_board.resource_parent, user) }
it 'returns nil when there is no user' do
finder.current_user = nil
@@ -52,7 +52,7 @@ describe Boards::VisitsFinder do
it 'queries for last N visits' do
expect(BoardGroupRecentVisit).to receive(:latest).with(user, group, count: 5).once
- described_class.new(group_board.parent, user).latest(5)
+ described_class.new(group_board.resource_parent, user).latest(5)
end
end
end
diff --git a/spec/finders/branches_finder_spec.rb b/spec/finders/branches_finder_spec.rb
index 3fc86f3e408..1a33bdf11d7 100644
--- a/spec/finders/branches_finder_spec.rb
+++ b/spec/finders/branches_finder_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe BranchesFinder do
diff --git a/spec/finders/clusters/knative_services_finder_spec.rb b/spec/finders/clusters/knative_services_finder_spec.rb
index 159724b3c1f..7ad64cc3bca 100644
--- a/spec/finders/clusters/knative_services_finder_spec.rb
+++ b/spec/finders/clusters/knative_services_finder_spec.rb
@@ -77,6 +77,7 @@ describe Clusters::KnativeServicesFinder do
describe '#knative_detected' do
subject { finder.knative_detected }
+
before do
synchronous_reactive_cache(finder)
end
diff --git a/spec/finders/clusters/kubernetes_namespace_finder_spec.rb b/spec/finders/clusters/kubernetes_namespace_finder_spec.rb
index 8beba0b99a4..7d9c4daa0fe 100644
--- a/spec/finders/clusters/kubernetes_namespace_finder_spec.rb
+++ b/spec/finders/clusters/kubernetes_namespace_finder_spec.rb
@@ -7,7 +7,7 @@ RSpec.describe Clusters::KubernetesNamespaceFinder do
described_class.new(
cluster,
project: project,
- environment_slug: 'production',
+ environment_name: 'production',
allow_blank_token: allow_blank_token
)
end
@@ -22,8 +22,8 @@ RSpec.describe Clusters::KubernetesNamespaceFinder do
end
describe '#execute' do
- let(:production) { create(:environment, project: project, slug: 'production') }
- let(:staging) { create(:environment, project: project, slug: 'staging') }
+ let(:production) { create(:environment, project: project, name: 'production') }
+ let(:staging) { create(:environment, project: project, name: 'staging') }
let(:cluster) { create(:cluster, :group, :provided_by_user) }
let(:project) { create(:project) }
diff --git a/spec/finders/clusters_finder_spec.rb b/spec/finders/clusters_finder_spec.rb
index da529e0670f..f6ea8347f67 100644
--- a/spec/finders/clusters_finder_spec.rb
+++ b/spec/finders/clusters_finder_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe ClustersFinder do
diff --git a/spec/finders/concerns/finder_methods_spec.rb b/spec/finders/concerns/finder_methods_spec.rb
index e074e53c2c5..2e44df8b044 100644
--- a/spec/finders/concerns/finder_methods_spec.rb
+++ b/spec/finders/concerns/finder_methods_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe FinderMethods do
diff --git a/spec/finders/concerns/finder_with_cross_project_access_spec.rb b/spec/finders/concerns/finder_with_cross_project_access_spec.rb
index f29acb521a8..6ba98b79176 100644
--- a/spec/finders/concerns/finder_with_cross_project_access_spec.rb
+++ b/spec/finders/concerns/finder_with_cross_project_access_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe FinderWithCrossProjectAccess do
@@ -22,6 +24,7 @@ describe FinderWithCrossProjectAccess do
let(:user) { create(:user) }
subject(:finder) { finder_class.new(user) }
+
let!(:result) { create(:issue) }
before do
diff --git a/spec/finders/contributed_projects_finder_spec.rb b/spec/finders/contributed_projects_finder_spec.rb
index ee84fd067d4..1d907261fe9 100644
--- a/spec/finders/contributed_projects_finder_spec.rb
+++ b/spec/finders/contributed_projects_finder_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe ContributedProjectsFinder do
diff --git a/spec/finders/environments_finder_spec.rb b/spec/finders/environments_finder_spec.rb
index 25835bb4d94..69687eaa99f 100644
--- a/spec/finders/environments_finder_spec.rb
+++ b/spec/finders/environments_finder_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe EnvironmentsFinder do
diff --git a/spec/finders/events_finder_spec.rb b/spec/finders/events_finder_spec.rb
index 3bce46cc4d1..848030262cd 100644
--- a/spec/finders/events_finder_spec.rb
+++ b/spec/finders/events_finder_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe EventsFinder do
diff --git a/spec/finders/fork_projects_finder_spec.rb b/spec/finders/fork_projects_finder_spec.rb
index 98cff37205e..2fba53a74a0 100644
--- a/spec/finders/fork_projects_finder_spec.rb
+++ b/spec/finders/fork_projects_finder_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe ForkProjectsFinder do
diff --git a/spec/finders/group_descendants_finder_spec.rb b/spec/finders/group_descendants_finder_spec.rb
index 5fb6739d6e2..17875a9b9ab 100644
--- a/spec/finders/group_descendants_finder_spec.rb
+++ b/spec/finders/group_descendants_finder_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe GroupDescendantsFinder do
diff --git a/spec/finders/group_members_finder_spec.rb b/spec/finders/group_members_finder_spec.rb
index 49b0e14241e..08f3b4024b3 100644
--- a/spec/finders/group_members_finder_spec.rb
+++ b/spec/finders/group_members_finder_spec.rb
@@ -1,8 +1,10 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe GroupMembersFinder, '#execute' do
let(:group) { create(:group) }
- let(:nested_group) { create(:group, :access_requestable, parent: group) }
+ let(:nested_group) { create(:group, parent: group) }
let(:user1) { create(:user) }
let(:user2) { create(:user) }
let(:user3) { create(:user) }
diff --git a/spec/finders/group_projects_finder_spec.rb b/spec/finders/group_projects_finder_spec.rb
index f4bd8a3f6ba..b291b5d4b90 100644
--- a/spec/finders/group_projects_finder_spec.rb
+++ b/spec/finders/group_projects_finder_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe GroupProjectsFinder do
diff --git a/spec/finders/groups_finder_spec.rb b/spec/finders/groups_finder_spec.rb
index c8875d1f92d..741a89a270b 100644
--- a/spec/finders/groups_finder_spec.rb
+++ b/spec/finders/groups_finder_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe GroupsFinder do
diff --git a/spec/finders/issues_finder_spec.rb b/spec/finders/issues_finder_spec.rb
index a17ff1ad50d..c27ce263bf0 100644
--- a/spec/finders/issues_finder_spec.rb
+++ b/spec/finders/issues_finder_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe IssuesFinder do
diff --git a/spec/finders/joined_groups_finder_spec.rb b/spec/finders/joined_groups_finder_spec.rb
index ae3e55f90f1..b01bd44470a 100644
--- a/spec/finders/joined_groups_finder_spec.rb
+++ b/spec/finders/joined_groups_finder_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe JoinedGroupsFinder do
diff --git a/spec/finders/labels_finder_spec.rb b/spec/finders/labels_finder_spec.rb
index ba41ded112a..2681f098fec 100644
--- a/spec/finders/labels_finder_spec.rb
+++ b/spec/finders/labels_finder_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe LabelsFinder do
diff --git a/spec/finders/license_template_finder_spec.rb b/spec/finders/license_template_finder_spec.rb
index f6f40bf33cc..183ee67d801 100644
--- a/spec/finders/license_template_finder_spec.rb
+++ b/spec/finders/license_template_finder_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe LicenseTemplateFinder do
diff --git a/spec/finders/members_finder_spec.rb b/spec/finders/members_finder_spec.rb
index 6920fb4e572..f9b8fee6f2d 100644
--- a/spec/finders/members_finder_spec.rb
+++ b/spec/finders/members_finder_spec.rb
@@ -1,8 +1,10 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe MembersFinder, '#execute' do
set(:group) { create(:group) }
- set(:nested_group) { create(:group, :access_requestable, parent: group) }
+ set(:nested_group) { create(:group, parent: group) }
set(:project) { create(:project, namespace: nested_group) }
set(:user1) { create(:user) }
set(:user2) { create(:user) }
@@ -55,7 +57,7 @@ describe MembersFinder, '#execute' do
context 'when include_invited_groups_members == true' do
subject { described_class.new(project, user2).execute(include_invited_groups_members: true) }
- set(:linked_group) { create(:group, :public, :access_requestable) }
+ set(:linked_group) { create(:group, :public) }
set(:nested_linked_group) { create(:group, parent: linked_group) }
set(:linked_group_member) { linked_group.add_guest(user1) }
set(:nested_linked_group_member) { nested_linked_group.add_guest(user2) }
@@ -86,10 +88,8 @@ describe MembersFinder, '#execute' do
create(:project_group_link, project: project, group: nested_linked_group, group_access: Gitlab::Access::REPORTER)
nested_linked_group.add_developer(user1)
- result = subject
-
- expect(result).to contain_exactly(linked_group_member, nested_linked_group_member)
- expect(result.first.access_level).to eq(Gitlab::Access::REPORTER)
+ expect(subject.map(&:user)).to contain_exactly(user1, user2)
+ expect(subject.max_by(&:access_level).access_level).to eq(Gitlab::Access::REPORTER)
end
end
end
diff --git a/spec/finders/merge_request_target_project_finder_spec.rb b/spec/finders/merge_request_target_project_finder_spec.rb
index d26a75179de..1d78b7ba4e3 100644
--- a/spec/finders/merge_request_target_project_finder_spec.rb
+++ b/spec/finders/merge_request_target_project_finder_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe MergeRequestTargetProjectFinder do
diff --git a/spec/finders/merge_requests_finder_spec.rb b/spec/finders/merge_requests_finder_spec.rb
index 6c0bbeff4f4..a396284f1e9 100644
--- a/spec/finders/merge_requests_finder_spec.rb
+++ b/spec/finders/merge_requests_finder_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe MergeRequestsFinder do
diff --git a/spec/finders/milestones_finder_spec.rb b/spec/finders/milestones_finder_spec.rb
index 34c7b508c56..3545ff35ed8 100644
--- a/spec/finders/milestones_finder_spec.rb
+++ b/spec/finders/milestones_finder_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe MilestonesFinder do
diff --git a/spec/finders/notes_finder_spec.rb b/spec/finders/notes_finder_spec.rb
index 88906adfeeb..44636a22ef9 100644
--- a/spec/finders/notes_finder_spec.rb
+++ b/spec/finders/notes_finder_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe NotesFinder do
diff --git a/spec/finders/personal_access_tokens_finder_spec.rb b/spec/finders/personal_access_tokens_finder_spec.rb
index 3e849c9a644..a44daf585ba 100644
--- a/spec/finders/personal_access_tokens_finder_spec.rb
+++ b/spec/finders/personal_access_tokens_finder_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe PersonalAccessTokensFinder do
diff --git a/spec/finders/personal_projects_finder_spec.rb b/spec/finders/personal_projects_finder_spec.rb
index ef7dd0cd4a8..7686dd3dc9d 100644
--- a/spec/finders/personal_projects_finder_spec.rb
+++ b/spec/finders/personal_projects_finder_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe PersonalProjectsFinder do
diff --git a/spec/finders/pipeline_schedules_finder_spec.rb b/spec/finders/pipeline_schedules_finder_spec.rb
index 2fefa0280d1..8d0bde15e03 100644
--- a/spec/finders/pipeline_schedules_finder_spec.rb
+++ b/spec/finders/pipeline_schedules_finder_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe PipelineSchedulesFinder do
diff --git a/spec/finders/pipelines_finder_spec.rb b/spec/finders/pipelines_finder_spec.rb
index b23fd8ccdc6..05d13a76e0e 100644
--- a/spec/finders/pipelines_finder_spec.rb
+++ b/spec/finders/pipelines_finder_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe PipelinesFinder do
diff --git a/spec/finders/projects_finder_spec.rb b/spec/finders/projects_finder_spec.rb
index ac866e49fcd..4ec12b5a7f7 100644
--- a/spec/finders/projects_finder_spec.rb
+++ b/spec/finders/projects_finder_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe ProjectsFinder do
diff --git a/spec/finders/runner_jobs_finder_spec.rb b/spec/finders/runner_jobs_finder_spec.rb
index 01f45a37ba8..c11f9182036 100644
--- a/spec/finders/runner_jobs_finder_spec.rb
+++ b/spec/finders/runner_jobs_finder_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe RunnerJobsFinder do
diff --git a/spec/finders/snippets_finder_spec.rb b/spec/finders/snippets_finder_spec.rb
index d367f9015c7..bcb762664f7 100644
--- a/spec/finders/snippets_finder_spec.rb
+++ b/spec/finders/snippets_finder_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe SnippetsFinder do
@@ -15,16 +17,27 @@ describe SnippetsFinder do
end
describe '#execute' do
- set(:user) { create(:user) }
- set(:private_personal_snippet) { create(:personal_snippet, :private, author: user) }
- set(:internal_personal_snippet) { create(:personal_snippet, :internal, author: user) }
- set(:public_personal_snippet) { create(:personal_snippet, :public, author: user) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:admin) { create(:admin) }
+ let_it_be(:group) { create(:group, :public) }
+ let_it_be(:project) { create(:project, :public, group: group) }
+
+ let_it_be(:private_personal_snippet) { create(:personal_snippet, :private, author: user) }
+ let_it_be(:internal_personal_snippet) { create(:personal_snippet, :internal, author: user) }
+ let_it_be(:public_personal_snippet) { create(:personal_snippet, :public, author: user) }
+
+ let_it_be(:private_project_snippet) { create(:project_snippet, :private, project: project) }
+ let_it_be(:internal_project_snippet) { create(:project_snippet, :internal, project: project) }
+ let_it_be(:public_project_snippet) { create(:project_snippet, :public, project: project) }
context 'filter by scope' do
it "returns all snippets for 'all' scope" do
snippets = described_class.new(user, scope: :all).execute
- expect(snippets).to contain_exactly(private_personal_snippet, internal_personal_snippet, public_personal_snippet)
+ expect(snippets).to contain_exactly(
+ private_personal_snippet, internal_personal_snippet, public_personal_snippet,
+ internal_project_snippet, public_project_snippet
+ )
end
it "returns all snippets for 'are_private' scope" do
@@ -36,13 +49,13 @@ describe SnippetsFinder do
it "returns all snippets for 'are_internal' scope" do
snippets = described_class.new(user, scope: :are_internal).execute
- expect(snippets).to contain_exactly(internal_personal_snippet)
+ expect(snippets).to contain_exactly(internal_personal_snippet, internal_project_snippet)
end
- it "returns all snippets for 'are_private' scope" do
+ it "returns all snippets for 'are_public' scope" do
snippets = described_class.new(user, scope: :are_public).execute
- expect(snippets).to contain_exactly(public_personal_snippet)
+ expect(snippets).to contain_exactly(public_personal_snippet, public_project_snippet)
end
end
@@ -84,7 +97,6 @@ describe SnippetsFinder do
end
it 'returns all snippets for an admin' do
- admin = create(:user, :admin)
snippets = described_class.new(admin, author: user).execute
expect(snippets).to contain_exactly(private_personal_snippet, internal_personal_snippet, public_personal_snippet)
@@ -92,12 +104,6 @@ describe SnippetsFinder do
end
context 'project snippets' do
- let(:group) { create(:group, :public) }
- let(:project) { create(:project, :public, group: group) }
- let!(:private_project_snippet) { create(:project_snippet, :private, project: project) }
- let!(:internal_project_snippet) { create(:project_snippet, :internal, project: project) }
- let!(:public_project_snippet) { create(:project_snippet, :public, project: project) }
-
it 'returns public personal and project snippets for unauthorized user' do
snippets = described_class.new(nil, project: project).execute
@@ -145,11 +151,54 @@ describe SnippetsFinder do
end
it 'returns all snippets for an admin' do
- admin = create(:user, :admin)
snippets = described_class.new(admin, project: project).execute
expect(snippets).to contain_exactly(private_project_snippet, internal_project_snippet, public_project_snippet)
end
+
+ context 'filter by author' do
+ let!(:other_user) { create(:user) }
+ let!(:other_private_project_snippet) { create(:project_snippet, :private, project: project, author: other_user) }
+ let!(:other_internal_project_snippet) { create(:project_snippet, :internal, project: project, author: other_user) }
+ let!(:other_public_project_snippet) { create(:project_snippet, :public, project: project, author: other_user) }
+
+ it 'returns all snippets for project members' do
+ project.add_developer(user)
+
+ snippets = described_class.new(user, author: other_user).execute
+
+ expect(snippets)
+ .to contain_exactly(
+ other_private_project_snippet,
+ other_internal_project_snippet,
+ other_public_project_snippet
+ )
+ end
+ end
+ end
+
+ context 'explore snippets' do
+ it 'returns only public personal snippets for unauthenticated users' do
+ snippets = described_class.new(nil, explore: true).execute
+
+ expect(snippets).to contain_exactly(public_personal_snippet)
+ end
+
+ it 'also returns internal personal snippets for authenticated users' do
+ snippets = described_class.new(user, explore: true).execute
+
+ expect(snippets).to contain_exactly(
+ internal_personal_snippet, public_personal_snippet
+ )
+ end
+
+ it 'returns all personal snippets for admins' do
+ snippets = described_class.new(admin, explore: true).execute
+
+ expect(snippets).to contain_exactly(
+ private_personal_snippet, internal_personal_snippet, public_personal_snippet
+ )
+ end
end
context 'when the user cannot read cross project' do
diff --git a/spec/finders/tags_finder_spec.rb b/spec/finders/tags_finder_spec.rb
index 460e278e2d3..85f970b71c4 100644
--- a/spec/finders/tags_finder_spec.rb
+++ b/spec/finders/tags_finder_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe TagsFinder do
diff --git a/spec/finders/template_finder_spec.rb b/spec/finders/template_finder_spec.rb
index 114af9461e0..ed47752cf60 100644
--- a/spec/finders/template_finder_spec.rb
+++ b/spec/finders/template_finder_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe TemplateFinder do
diff --git a/spec/finders/todos_finder_spec.rb b/spec/finders/todos_finder_spec.rb
index f7b35e76925..044e135fa0b 100644
--- a/spec/finders/todos_finder_spec.rb
+++ b/spec/finders/todos_finder_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe TodosFinder do
@@ -14,6 +16,10 @@ describe TodosFinder do
end
describe '#execute' do
+ it 'returns no todos if user is nil' do
+ expect(described_class.new(nil, {}).execute).to be_empty
+ end
+
context 'filtering' do
let!(:todo1) { create(:todo, user: user, project: project, target: issue) }
let!(:todo2) { create(:todo, user: user, group: group, target: merge_request) }
@@ -36,14 +42,98 @@ describe TodosFinder do
expect(todos).to match_array([todo1])
end
- context 'with subgroups' do
- let(:subgroup) { create(:group, parent: group) }
- let!(:todo3) { create(:todo, user: user, group: subgroup, target: issue) }
+ context 'when filtering for actions' do
+ let!(:todo1) { create(:todo, user: user, project: project, target: issue, action: Todo::ASSIGNED) }
+ let!(:todo2) { create(:todo, user: user, group: group, target: merge_request, action: Todo::DIRECTLY_ADDRESSED) }
+
+ context 'by action ids' do
+ it 'returns the expected todos' do
+ todos = finder.new(user, { action_id: Todo::DIRECTLY_ADDRESSED }).execute
+
+ expect(todos).to match_array([todo2])
+ end
+
+ context 'multiple actions' do
+ it 'returns the expected todos' do
+ todos = finder.new(user, { action_id: [Todo::DIRECTLY_ADDRESSED, Todo::ASSIGNED] }).execute
+
+ expect(todos).to match_array([todo2, todo1])
+ end
+ end
+ end
+
+ context 'by action names' do
+ it 'returns the expected todos' do
+ todos = finder.new(user, { action: :directly_addressed }).execute
+
+ expect(todos).to match_array([todo2])
+ end
+
+ context 'multiple actions' do
+ it 'returns the expected todos' do
+ todos = finder.new(user, { action: [:directly_addressed, :assigned] }).execute
+
+ expect(todos).to match_array([todo2, todo1])
+ end
+ end
+ end
+ end
+
+ context 'when filtering by author' do
+ let(:author1) { create(:user) }
+ let(:author2) { create(:user) }
+
+ let!(:todo1) { create(:todo, user: user, author: author1) }
+ let!(:todo2) { create(:todo, user: user, author: author2) }
+
+ it 'returns correct todos when filtering by an author' do
+ todos = finder.new(user, { author_id: author1.id }).execute
+
+ expect(todos).to match_array([todo1])
+ end
+
+ context 'querying for multiple authors' do
+ it 'returns the correct todo items' do
+ todos = finder.new(user, { author_id: [author2.id, author1.id] }).execute
+
+ expect(todos).to match_array([todo2, todo1])
+ end
+ end
+ end
+
+ context 'by groups' do
+ context 'with subgroups' do
+ let(:subgroup) { create(:group, parent: group) }
+ let!(:todo3) { create(:todo, user: user, group: subgroup, target: issue) }
+
+ it 'returns todos from subgroups when filtered by a group' do
+ todos = finder.new(user, { group_id: group.id }).execute
+
+ expect(todos).to match_array([todo1, todo2, todo3])
+ end
+ end
+
+ context 'filtering for multiple groups' do
+ let_it_be(:group2) { create(:group) }
+ let_it_be(:group3) { create(:group) }
+
+ let!(:todo1) { create(:todo, user: user, project: project, target: issue) }
+ let!(:todo2) { create(:todo, user: user, group: group, target: merge_request) }
+ let!(:todo3) { create(:todo, user: user, group: group2, target: merge_request) }
+
+ let(:subgroup1) { create(:group, parent: group) }
+ let!(:todo4) { create(:todo, user: user, group: subgroup1, target: issue) }
+
+ let(:subgroup2) { create(:group, parent: group2) }
+ let!(:todo5) { create(:todo, user: user, group: subgroup2, target: issue) }
+
+ let!(:todo6) { create(:todo, user: user, group: group3, target: issue) }
- it 'returns todos from subgroups when filtered by a group' do
- todos = finder.new(user, { group_id: group.id }).execute
+ it 'returns the expected groups' do
+ todos = finder.new(user, { group_id: [group.id, group2.id] }).execute
- expect(todos).to match_array([todo1, todo2, todo3])
+ expect(todos).to match_array([todo1, todo2, todo3, todo4, todo5])
+ end
end
end
end
diff --git a/spec/finders/user_finder_spec.rb b/spec/finders/user_finder_spec.rb
index 4771b878b8e..c20d7850d68 100644
--- a/spec/finders/user_finder_spec.rb
+++ b/spec/finders/user_finder_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
describe UserFinder do
- set(:user) { create(:user) }
+ let_it_be(:user) { create(:user) }
describe '#find_by_id' do
context 'when the user exists' do
@@ -24,7 +24,7 @@ describe UserFinder do
context 'when the user does not exist' do
it 'returns nil' do
- found = described_class.new(1).find_by_id
+ found = described_class.new(-1).find_by_id
expect(found).to be_nil
end
@@ -84,7 +84,7 @@ describe UserFinder do
context 'when the user does not exist' do
it 'returns nil' do
- found = described_class.new(1).find_by_id_or_username
+ found = described_class.new(-1).find_by_id_or_username
expect(found).to be_nil
end
@@ -110,7 +110,7 @@ describe UserFinder do
context 'when the user does not exist' do
it 'raises ActiveRecord::RecordNotFound' do
- finder = described_class.new(1)
+ finder = described_class.new(-1)
expect { finder.find_by_id! }.to raise_error(ActiveRecord::RecordNotFound)
end
@@ -170,10 +170,32 @@ describe UserFinder do
context 'when the user does not exist' do
it 'raises ActiveRecord::RecordNotFound' do
- finder = described_class.new(1)
+ finder = described_class.new(-1)
expect { finder.find_by_id_or_username! }.to raise_error(ActiveRecord::RecordNotFound)
end
end
end
+
+ describe '#find_by_ssh_key_id' do
+ let_it_be(:ssh_key) { create(:key, user: user) }
+
+ it 'returns the user when passing the ssh key id' do
+ found = described_class.new(ssh_key.id).find_by_ssh_key_id
+
+ expect(found).to eq(user)
+ end
+
+ it 'returns the user when passing the ssh key id (string)' do
+ found = described_class.new(ssh_key.id.to_s).find_by_ssh_key_id
+
+ expect(found).to eq(user)
+ end
+
+ it 'returns nil when the id does not exist' do
+ found = described_class.new(-1).find_by_ssh_key_id
+
+ expect(found).to be_nil
+ end
+ end
end
diff --git a/spec/finders/user_recent_events_finder_spec.rb b/spec/finders/user_recent_events_finder_spec.rb
index 5ebceeb7586..eef6448a4a2 100644
--- a/spec/finders/user_recent_events_finder_spec.rb
+++ b/spec/finders/user_recent_events_finder_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe UserRecentEventsFinder do
diff --git a/spec/finders/users_finder_spec.rb b/spec/finders/users_finder_spec.rb
index d71d3c99272..7f1fc1cc1c5 100644
--- a/spec/finders/users_finder_spec.rb
+++ b/spec/finders/users_finder_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe UsersFinder do
diff --git a/spec/fixtures/api/schemas/deployment.json b/spec/fixtures/api/schemas/deployment.json
index 81c2d1ef5ab..0cfeadfe548 100644
--- a/spec/fixtures/api/schemas/deployment.json
+++ b/spec/fixtures/api/schemas/deployment.json
@@ -60,7 +60,8 @@
"scheduled_actions": {
"type": "array",
"items": { "$ref": "job/job.json" }
- }
+ },
+ "status": { "type": "string" }
},
"additionalProperties": false
}
diff --git a/spec/fixtures/api/schemas/evidences/evidence.json b/spec/fixtures/api/schemas/evidences/evidence.json
new file mode 100644
index 00000000000..ea3861258e1
--- /dev/null
+++ b/spec/fixtures/api/schemas/evidences/evidence.json
@@ -0,0 +1,11 @@
+{
+ "type": "object",
+ "required": [
+ "release"
+ ],
+ "properties": {
+ "release": { "$ref": "release.json" }
+ },
+ "additionalProperties": false
+}
+
diff --git a/spec/fixtures/api/schemas/evidences/issue.json b/spec/fixtures/api/schemas/evidences/issue.json
new file mode 100644
index 00000000000..fd9daf17ab8
--- /dev/null
+++ b/spec/fixtures/api/schemas/evidences/issue.json
@@ -0,0 +1,25 @@
+{
+ "type": "object",
+ "required": [
+ "id",
+ "title",
+ "description",
+ "author",
+ "state",
+ "iid",
+ "confidential",
+ "created_at",
+ "due_date"
+ ],
+ "properties": {
+ "id": { "type": "integer" },
+ "title": { "type": "string" },
+ "description": { "type": ["string", "null"] },
+ "state": { "type": "string" },
+ "iid": { "type": "integer" },
+ "confidential": { "type": "boolean" },
+ "created_at": { "type": "date" },
+ "due_date": { "type": ["date", "null"] }
+ },
+ "additionalProperties": false
+}
diff --git a/spec/fixtures/api/schemas/evidences/milestone.json b/spec/fixtures/api/schemas/evidences/milestone.json
new file mode 100644
index 00000000000..ab27fdecde2
--- /dev/null
+++ b/spec/fixtures/api/schemas/evidences/milestone.json
@@ -0,0 +1,27 @@
+{
+ "type": "object",
+ "required": [
+ "id",
+ "title",
+ "description",
+ "state",
+ "iid",
+ "created_at",
+ "due_date",
+ "issues"
+ ],
+ "properties": {
+ "id": { "type": "integer" },
+ "title": { "type": "string" },
+ "description": { "type": ["string", "null"] },
+ "state": { "type": "string" },
+ "iid": { "type": "integer" },
+ "created_at": { "type": "date" },
+ "due_date": { "type": ["date", "null"] },
+ "issues": {
+ "type": "array",
+ "items": { "$ref": "issue.json" }
+ }
+ },
+ "additionalProperties": false
+}
diff --git a/spec/fixtures/api/schemas/evidences/project.json b/spec/fixtures/api/schemas/evidences/project.json
new file mode 100644
index 00000000000..3a094bd276f
--- /dev/null
+++ b/spec/fixtures/api/schemas/evidences/project.json
@@ -0,0 +1,16 @@
+{
+ "type": "object",
+ "required": [
+ "id",
+ "name",
+ "description",
+ "created_at"
+ ],
+ "properties": {
+ "id": { "type": "integer" },
+ "name": { "type": "string" },
+ "description": { "type": ["string", "null"] },
+ "created_at": { "type": "date" }
+ },
+ "additionalProperties": false
+}
diff --git a/spec/fixtures/api/schemas/evidences/release.json b/spec/fixtures/api/schemas/evidences/release.json
new file mode 100644
index 00000000000..37eb9a9b5c0
--- /dev/null
+++ b/spec/fixtures/api/schemas/evidences/release.json
@@ -0,0 +1,25 @@
+{
+ "type": "object",
+ "required": [
+ "id",
+ "tag_name",
+ "name",
+ "description",
+ "created_at",
+ "project",
+ "milestones"
+ ],
+ "properties": {
+ "id": { "type": "integer" },
+ "tag_name": { "type": "string" },
+ "name": { "type": ["string", "null"] },
+ "description": { "type": "string" },
+ "created_at": { "type": "date" },
+ "project": { "$ref": "project.json" },
+ "milestones": {
+ "type": "array",
+ "items": { "$ref": "milestone.json" }
+ }
+ },
+ "additionalProperties": false
+}
diff --git a/spec/fixtures/api/schemas/job/build_trace.json b/spec/fixtures/api/schemas/job/build_trace.json
new file mode 100644
index 00000000000..becd881ea57
--- /dev/null
+++ b/spec/fixtures/api/schemas/job/build_trace.json
@@ -0,0 +1,31 @@
+{
+ "description": "Build trace",
+ "type": "object",
+ "required": [
+ "id",
+ "status",
+ "complete",
+ "state",
+ "append",
+ "truncated",
+ "offset",
+ "size",
+ "total"
+ ],
+ "properties": {
+ "id": { "type": "integer" },
+ "status": { "type": "string" },
+ "complete": { "type": "boolean" },
+ "state": { "type": ["string", "null"] },
+ "append": { "type": ["boolean", "null"] },
+ "truncated": { "type": ["boolean", "null"] },
+ "offset": { "type": ["integer", "null"] },
+ "size": { "type": ["integer", "null"] },
+ "total": { "type": ["integer", "null"] },
+ "html": { "type": ["string", "null"] },
+ "lines": {
+ "type": ["array", "null"],
+ "items": { "$ref": "./build_trace_line.json" }
+ }
+ }
+}
diff --git a/spec/fixtures/api/schemas/job/build_trace_line.json b/spec/fixtures/api/schemas/job/build_trace_line.json
new file mode 100644
index 00000000000..18726dff2bb
--- /dev/null
+++ b/spec/fixtures/api/schemas/job/build_trace_line.json
@@ -0,0 +1,18 @@
+{
+ "description": "Build trace line",
+ "type": "object",
+ "required": [
+ "offset",
+ "content"
+ ],
+ "properties": {
+ "offset": { "type": "integer" },
+ "content": {
+ "type": "array",
+ "items": { "$ref": "./build_trace_line_content.json" }
+ },
+ "section": "string",
+ "section_header": "boolean",
+ "section_duration": "string"
+ }
+}
diff --git a/spec/fixtures/api/schemas/job/build_trace_line_content.json b/spec/fixtures/api/schemas/job/build_trace_line_content.json
new file mode 100644
index 00000000000..41f8124c113
--- /dev/null
+++ b/spec/fixtures/api/schemas/job/build_trace_line_content.json
@@ -0,0 +1,11 @@
+{
+ "description": "Build trace line content",
+ "type": "object",
+ "required": [
+ "text"
+ ],
+ "properties": {
+ "text": { "type": "string" },
+ "style": { "type": "string" }
+ }
+}
diff --git a/spec/fixtures/api/schemas/list.json b/spec/fixtures/api/schemas/list.json
index b76ec115293..7603892e198 100644
--- a/spec/fixtures/api/schemas/list.json
+++ b/spec/fixtures/api/schemas/list.json
@@ -35,7 +35,8 @@
}
},
"title": { "type": "string" },
- "position": { "type": ["integer", "null"] }
+ "position": { "type": ["integer", "null"] },
+ "max_issue_count": { "type": "integer" }
},
"additionalProperties": true
}
diff --git a/spec/fixtures/api/schemas/public_api/v4/board.json b/spec/fixtures/api/schemas/public_api/v4/board.json
index d667f1d631c..8dc3999baa2 100644
--- a/spec/fixtures/api/schemas/public_api/v4/board.json
+++ b/spec/fixtures/api/schemas/public_api/v4/board.json
@@ -76,7 +76,8 @@
"name": { "type": "string" }
}
},
- "position": { "type": ["integer", "null"] }
+ "position": { "type": ["integer", "null"] },
+ "max_issue_count": { "type": "integer" }
},
"additionalProperties": false
}
diff --git a/spec/fixtures/api/schemas/public_api/v4/deployment.json b/spec/fixtures/api/schemas/public_api/v4/deployment.json
index 3af2dc27d55..2371509edd6 100644
--- a/spec/fixtures/api/schemas/public_api/v4/deployment.json
+++ b/spec/fixtures/api/schemas/public_api/v4/deployment.json
@@ -6,6 +6,7 @@
"ref",
"sha",
"created_at",
+ "updated_at",
"user",
"deployable"
],
@@ -15,6 +16,7 @@
"ref": { "type": "string" },
"sha": { "type": "string" },
"created_at": { "type": "string" },
+ "updated_at": { "type": "string" },
"user": {
"oneOf": [
{ "type": "null" },
@@ -26,7 +28,8 @@
{ "type": "null" },
{ "$ref": "job.json" }
]
- }
+ },
+ "status": { "type": "string" }
},
"additionalProperties": false
}
diff --git a/spec/fixtures/api/schemas/public_api/v4/environment.json b/spec/fixtures/api/schemas/public_api/v4/environment.json
index 242e90fb7ac..57352017f03 100644
--- a/spec/fixtures/api/schemas/public_api/v4/environment.json
+++ b/spec/fixtures/api/schemas/public_api/v4/environment.json
@@ -17,7 +17,8 @@
{ "type": "null" },
{ "$ref": "deployment.json" }
]
- }
+ },
+ "state": { "type": "string" }
},
"additionalProperties": false
}
diff --git a/spec/fixtures/api/schemas/public_api/v4/release.json b/spec/fixtures/api/schemas/public_api/v4/release.json
index 662e61a9c06..2bdc8bc711c 100644
--- a/spec/fixtures/api/schemas/public_api/v4/release.json
+++ b/spec/fixtures/api/schemas/public_api/v4/release.json
@@ -19,6 +19,9 @@
"type": "array",
"items": { "$ref": "milestone.json" }
},
+ "commit_path": { "type": "string" },
+ "tag_path": { "type": "string" },
+ "name": { "type": "string" },
"assets": {
"required": ["count", "links", "sources"],
"properties": {
@@ -33,6 +36,13 @@
}
},
"additionalProperties": false
+ },
+ "_links": {
+ "required": ["merge_requests_url", "issues_url"],
+ "properties": {
+ "merge_requests_url": { "type": "string" },
+ "issues_url": { "type": "string" }
+ }
}
},
"additionalProperties": false
diff --git a/spec/fixtures/api/schemas/public_api/v4/release/release_for_guest.json b/spec/fixtures/api/schemas/public_api/v4/release/release_for_guest.json
index 0c1e8fd5fb3..bce74892059 100644
--- a/spec/fixtures/api/schemas/public_api/v4/release/release_for_guest.json
+++ b/spec/fixtures/api/schemas/public_api/v4/release/release_for_guest.json
@@ -8,6 +8,12 @@
"created_at": { "type": "date" },
"released_at": { "type": "date" },
"upcoming_release": { "type": "boolean" },
+ "milestones": {
+ "type": "array",
+ "items": { "$ref": "../milestone.json" }
+ },
+ "commit_path": { "type": "string" },
+ "tag_path": { "type": "string" },
"author": {
"oneOf": [{ "type": "null" }, { "$ref": "../user/basic.json" }]
},
@@ -18,6 +24,13 @@
"links": { "$ref": "../../../release/links.json" }
},
"additionalProperties": false
+ },
+ "_links": {
+ "required": ["merge_requests_url", "issues_url"],
+ "properties": {
+ "merge_requests_url": { "type": "string" },
+ "issues_url": { "type": "string" }
+ }
}
},
"additionalProperties": false
diff --git a/spec/fixtures/audio_sample.wav b/spec/fixtures/audio_sample.wav
new file mode 100644
index 00000000000..e0a42d58920
--- /dev/null
+++ b/spec/fixtures/audio_sample.wav
Binary files differ
diff --git a/spec/fixtures/emails/auto_reply.eml b/spec/fixtures/emails/auto_reply.eml
index 7999c8d78b7..4d5a45df467 100644
--- a/spec/fixtures/emails/auto_reply.eml
+++ b/spec/fixtures/emails/auto_reply.eml
@@ -12,7 +12,7 @@ Mime-Version: 1.0
Content-Type: text/plain;
charset=ISO-8859-1
Content-Transfer-Encoding: 7bit
-Auto-Submitted: auto-generated
+X-Autoreply: yes
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)
diff --git a/spec/fixtures/emails/auto_submitted.eml b/spec/fixtures/emails/auto_submitted.eml
new file mode 100644
index 00000000000..7999c8d78b7
--- /dev/null
+++ b/spec/fixtures/emails/auto_submitted.eml
@@ -0,0 +1,21 @@
+Return-Path: <jake@adventuretime.ooo>
+Received: from iceking.adventuretime.ooo ([unix socket]) by iceking (Cyrus v2.2.13-Debian-2.2.13-19+squeeze3) with LMTPA; Thu, 13 Jun 2013 17:03:50 -0400
+Received: from mail-ie0-x234.google.com (mail-ie0-x234.google.com [IPv6:2607:f8b0:4001:c03::234]) by iceking.adventuretime.ooo (8.14.3/8.14.3/Debian-9.4) with ESMTP id r5DL3nFJ016967 (version=TLSv1/SSLv3 cipher=RC4-SHA bits=128 verify=NOT) for <reply+59d8df8370b7e95c5a49fbf86aeb2c93@discourse.example.com>; Thu, 13 Jun 2013 17:03:50 -0400
+Received: by mail-ie0-f180.google.com with SMTP id f4so21977375iea.25 for <reply+59d8df8370b7e95c5a49fbf86aeb2c93@discourse.example.com>; Thu, 13 Jun 2013 14:03:48 -0700
+Received: by 10.0.0.1 with HTTP; Thu, 13 Jun 2013 14:03:48 -0700
+Date: Thu, 13 Jun 2013 17:03:48 -0400
+From: Jake the Dog <jake@adventuretime.ooo>
+To: reply+636ca428858779856c226bb145ef4fad@appmail.adventuretime.ooo
+Message-ID: <CADkmRc+rNGAGGbV2iE5p918UVy4UyJqVcXRO2=otppgzduJSg@mail.gmail.com>
+Subject: re: [Discourse Meta] eviltrout posted in 'Adventure Time Sux'
+Mime-Version: 1.0
+Content-Type: text/plain;
+ charset=ISO-8859-1
+Content-Transfer-Encoding: 7bit
+Auto-Submitted: auto-generated
+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
+
+Test reply to Discourse email digest
diff --git a/spec/lib/gitlab/import_export/project.group.json b/spec/fixtures/lib/gitlab/import_export/project.group.json
index 66f5bb4c87b..47faf271cca 100644
--- a/spec/lib/gitlab/import_export/project.group.json
+++ b/spec/fixtures/lib/gitlab/import_export/project.group.json
@@ -129,7 +129,7 @@
"updated_at": "2017-08-15T18:37:40.807Z",
"branch_name": null,
"description": "Quam totam fuga numquam in eveniet.",
- "state": "opened",
+ "state": "closed",
"iid": 2,
"updated_by_id": 1,
"confidential": false,
diff --git a/spec/lib/gitlab/import_export/project.json b/spec/fixtures/lib/gitlab/import_export/project.json
index 5f4bf18c743..7d9c8cdef8f 100644
--- a/spec/lib/gitlab/import_export/project.json
+++ b/spec/fixtures/lib/gitlab/import_export/project.json
@@ -6161,7 +6161,7 @@
{
"id": 36,
"project_id": 5,
- "ref": "master",
+ "ref": null,
"sha": "sha-notes",
"before_sha": null,
"push_data": null,
@@ -6175,6 +6175,8 @@
"finished_at": null,
"user_id": 9999,
"duration": null,
+ "source": "push",
+ "merge_request_id": null,
"notes": [
{
"id": 999,
@@ -6289,19 +6291,21 @@
{
"id": 37,
"project_id": 5,
- "ref": null,
+ "ref": "master",
"sha": "048721d90c449b244b7b4c53a9186b04330174ec",
"before_sha": null,
"push_data": null,
"created_at": "2016-03-22T15:20:35.757Z",
"updated_at": "2016-03-22T15:20:35.757Z",
- "tag": null,
+ "tag": false,
"yaml_errors": null,
"committed_at": null,
"status": "failed",
"started_at": null,
"finished_at": null,
"duration": null,
+ "source": "merge_request_event",
+ "merge_request_id": 27,
"stages": [
{
"id": 21,
@@ -6372,7 +6376,32 @@
}
]
}
- ]
+ ],
+ "merge_request": {
+ "id": 27,
+ "target_branch": "feature",
+ "source_branch": "feature_conflict",
+ "source_project_id": 999,
+ "author_id": 1,
+ "assignee_id": null,
+ "title": "MR1",
+ "created_at": "2016-06-14T15:02:36.568Z",
+ "updated_at": "2016-06-14T15:02:56.815Z",
+ "state": "opened",
+ "merge_status": "unchecked",
+ "target_project_id": 5,
+ "iid": 9,
+ "description": null,
+ "position": 0,
+ "updated_by_id": null,
+ "merge_error": null,
+ "diff_head_sha": "HEAD",
+ "source_branch_sha": "ABCD",
+ "target_branch_sha": "DCBA",
+ "merge_params": {
+ "force_remove_source_branch": null
+ }
+ }
},
{
"id": 38,
diff --git a/spec/lib/gitlab/import_export/project.light.json b/spec/fixtures/lib/gitlab/import_export/project.light.json
index 2971ca0f0f8..2971ca0f0f8 100644
--- a/spec/lib/gitlab/import_export/project.light.json
+++ b/spec/fixtures/lib/gitlab/import_export/project.light.json
diff --git a/spec/lib/gitlab/import_export/project.milestone-iid.json b/spec/fixtures/lib/gitlab/import_export/project.milestone-iid.json
index b028147b5eb..b028147b5eb 100644
--- a/spec/lib/gitlab/import_export/project.milestone-iid.json
+++ b/spec/fixtures/lib/gitlab/import_export/project.milestone-iid.json
diff --git a/spec/fixtures/markdown.md.erb b/spec/fixtures/markdown.md.erb
index 8016cf7a86d..b19b45928d9 100644
--- a/spec/fixtures/markdown.md.erb
+++ b/spec/fixtures/markdown.md.erb
@@ -286,6 +286,10 @@ However the wrapping tags cannot be mixed as such:
![My Video](/assets/videos/gitlab-demo.mp4)
+### Audio
+
+![My Audio Clip](/assets/audio/gitlab-demo.wav)
+
### Colors
`#F00`
diff --git a/spec/fixtures/spdx.json b/spec/fixtures/spdx.json
new file mode 100644
index 00000000000..41989626bec
--- /dev/null
+++ b/spec/fixtures/spdx.json
@@ -0,0 +1 @@
+{ "licenseListVersion": "3.6", "licenses": [ { "reference": "./0BSD.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/0BSD.json", "referenceNumber": "319", "name": "BSD Zero Clause License", "licenseId": "0BSD", "seeAlso": [ "http://landley.net/toybox/license.html" ], "isOsiApproved": true }, { "reference": "./AAL.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/AAL.json", "referenceNumber": "21", "name": "Attribution Assurance License", "licenseId": "AAL", "seeAlso": [ "https://opensource.org/licenses/attribution" ], "isOsiApproved": true }, { "reference": "./ADSL.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/ADSL.json", "referenceNumber": "19", "name": "Amazon Digital Services License", "licenseId": "ADSL", "seeAlso": [ "https://fedoraproject.org/wiki/Licensing/AmazonDigitalServicesLicense" ], "isOsiApproved": false }, { "reference": "./AFL-1.1.html", "isDeprecatedLicenseId": false, "isFsfLibre": true, "detailsUrl": "http://spdx.org/licenses/AFL-1.1.json", "referenceNumber": "118", "name": "Academic Free License v1.1", "licenseId": "AFL-1.1", "seeAlso": [ "http://opensource.linux-mirror.org/licenses/afl-1.1.txt", "http://wayback.archive.org/web/20021004124254/http://www.opensource.org/licenses/academic.php" ], "isOsiApproved": true }, { "reference": "./AFL-1.2.html", "isDeprecatedLicenseId": false, "isFsfLibre": true, "detailsUrl": "http://spdx.org/licenses/AFL-1.2.json", "referenceNumber": "136", "name": "Academic Free License v1.2", "licenseId": "AFL-1.2", "seeAlso": [ "http://opensource.linux-mirror.org/licenses/afl-1.2.txt", "http://wayback.archive.org/web/20021204204652/http://www.opensource.org/licenses/academic.php" ], "isOsiApproved": true }, { "reference": "./AFL-2.0.html", "isDeprecatedLicenseId": false, "isFsfLibre": true, "detailsUrl": "http://spdx.org/licenses/AFL-2.0.json", "referenceNumber": "115", "name": "Academic Free License v2.0", "licenseId": "AFL-2.0", "seeAlso": [ "http://wayback.archive.org/web/20060924134533/http://www.opensource.org/licenses/afl-2.0.txt" ], "isOsiApproved": true }, { "reference": "./AFL-2.1.html", "isDeprecatedLicenseId": false, "isFsfLibre": true, "detailsUrl": "http://spdx.org/licenses/AFL-2.1.json", "referenceNumber": "251", "name": "Academic Free License v2.1", "licenseId": "AFL-2.1", "seeAlso": [ "http://opensource.linux-mirror.org/licenses/afl-2.1.txt" ], "isOsiApproved": true }, { "reference": "./AFL-3.0.html", "isDeprecatedLicenseId": false, "isFsfLibre": true, "detailsUrl": "http://spdx.org/licenses/AFL-3.0.json", "referenceNumber": "216", "name": "Academic Free License v3.0", "licenseId": "AFL-3.0", "seeAlso": [ "http://www.rosenlaw.com/AFL3.0.htm", "https://opensource.org/licenses/afl-3.0" ], "isOsiApproved": true }, { "reference": "./AGPL-1.0.html", "isDeprecatedLicenseId": true, "isFsfLibre": true, "detailsUrl": "http://spdx.org/licenses/AGPL-1.0.json", "referenceNumber": "335", "name": "Affero General Public License v1.0", "licenseId": "AGPL-1.0", "seeAlso": [ "http://www.affero.org/oagpl.html" ], "isOsiApproved": false }, { "reference": "./AGPL-1.0-only.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/AGPL-1.0-only.json", "referenceNumber": "384", "name": "Affero General Public License v1.0 only", "licenseId": "AGPL-1.0-only", "seeAlso": [ "http://www.affero.org/oagpl.html" ], "isOsiApproved": false }, { "reference": "./AGPL-1.0-or-later.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/AGPL-1.0-or-later.json", "referenceNumber": "332", "name": "Affero General Public License v1.0 or later", "licenseId": "AGPL-1.0-or-later", "seeAlso": [ "http://www.affero.org/oagpl.html" ], "isOsiApproved": false }, { "reference": "./AGPL-3.0.html", "isDeprecatedLicenseId": true, "isFsfLibre": true, "detailsUrl": "http://spdx.org/licenses/AGPL-3.0.json", "referenceNumber": "229", "name": "GNU Affero General Public License v3.0", "licenseId": "AGPL-3.0", "seeAlso": [ "https://www.gnu.org/licenses/agpl.txt", "https://opensource.org/licenses/AGPL-3.0" ], "isOsiApproved": true }, { "reference": "./AGPL-3.0-only.html", "isDeprecatedLicenseId": false, "isFsfLibre": true, "detailsUrl": "http://spdx.org/licenses/AGPL-3.0-only.json", "referenceNumber": "95", "name": "GNU Affero General Public License v3.0 only", "licenseId": "AGPL-3.0-only", "seeAlso": [ "https://www.gnu.org/licenses/agpl.txt", "https://opensource.org/licenses/AGPL-3.0" ], "isOsiApproved": true }, { "reference": "./AGPL-3.0-or-later.html", "isDeprecatedLicenseId": false, "isFsfLibre": true, "detailsUrl": "http://spdx.org/licenses/AGPL-3.0-or-later.json", "referenceNumber": "155", "name": "GNU Affero General Public License v3.0 or later", "licenseId": "AGPL-3.0-or-later", "seeAlso": [ "https://www.gnu.org/licenses/agpl.txt", "https://opensource.org/licenses/AGPL-3.0" ], "isOsiApproved": true }, { "reference": "./AMDPLPA.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/AMDPLPA.json", "referenceNumber": "33", "name": "AMD\u0027s plpa_map.c License", "licenseId": "AMDPLPA", "seeAlso": [ "https://fedoraproject.org/wiki/Licensing/AMD_plpa_map_License" ], "isOsiApproved": false }, { "reference": "./AML.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/AML.json", "referenceNumber": "148", "name": "Apple MIT License", "licenseId": "AML", "seeAlso": [ "https://fedoraproject.org/wiki/Licensing/Apple_MIT_License" ], "isOsiApproved": false }, { "reference": "./AMPAS.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/AMPAS.json", "referenceNumber": "191", "name": "Academy of Motion Picture Arts and Sciences BSD", "licenseId": "AMPAS", "seeAlso": [ "https://fedoraproject.org/wiki/Licensing/BSD#AMPASBSD" ], "isOsiApproved": false }, { "reference": "./ANTLR-PD.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/ANTLR-PD.json", "referenceNumber": "395", "name": "ANTLR Software Rights Notice", "licenseId": "ANTLR-PD", "seeAlso": [ "http://www.antlr2.org/license.html" ], "isOsiApproved": false }, { "reference": "./APAFML.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/APAFML.json", "referenceNumber": "195", "name": "Adobe Postscript AFM License", "licenseId": "APAFML", "seeAlso": [ "https://fedoraproject.org/wiki/Licensing/AdobePostscriptAFM" ], "isOsiApproved": false }, { "reference": "./APL-1.0.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/APL-1.0.json", "referenceNumber": "252", "name": "Adaptive Public License 1.0", "licenseId": "APL-1.0", "seeAlso": [ "https://opensource.org/licenses/APL-1.0" ], "isOsiApproved": true }, { "reference": "./APSL-1.0.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/APSL-1.0.json", "referenceNumber": "354", "name": "Apple Public Source License 1.0", "licenseId": "APSL-1.0", "seeAlso": [ "https://fedoraproject.org/wiki/Licensing/Apple_Public_Source_License_1.0" ], "isOsiApproved": true }, { "reference": "./APSL-1.1.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/APSL-1.1.json", "referenceNumber": "324", "name": "Apple Public Source License 1.1", "licenseId": "APSL-1.1", "seeAlso": [ "http://www.opensource.apple.com/source/IOSerialFamily/IOSerialFamily-7/APPLE_LICENSE" ], "isOsiApproved": true }, { "reference": "./APSL-1.2.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/APSL-1.2.json", "referenceNumber": "34", "name": "Apple Public Source License 1.2", "licenseId": "APSL-1.2", "seeAlso": [ "http://www.samurajdata.se/opensource/mirror/licenses/apsl.php" ], "isOsiApproved": true }, { "reference": "./APSL-2.0.html", "isDeprecatedLicenseId": false, "isFsfLibre": true, "detailsUrl": "http://spdx.org/licenses/APSL-2.0.json", "referenceNumber": "109", "name": "Apple Public Source License 2.0", "licenseId": "APSL-2.0", "seeAlso": [ "http://www.opensource.apple.com/license/apsl/" ], "isOsiApproved": true }, { "reference": "./Abstyles.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/Abstyles.json", "referenceNumber": "80", "name": "Abstyles License", "licenseId": "Abstyles", "seeAlso": [ "https://fedoraproject.org/wiki/Licensing/Abstyles" ], "isOsiApproved": false }, { "reference": "./Adobe-2006.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/Adobe-2006.json", "referenceNumber": "285", "name": "Adobe Systems Incorporated Source Code License Agreement", "licenseId": "Adobe-2006", "seeAlso": [ "https://fedoraproject.org/wiki/Licensing/AdobeLicense" ], "isOsiApproved": false }, { "reference": "./Adobe-Glyph.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/Adobe-Glyph.json", "referenceNumber": "107", "name": "Adobe Glyph List License", "licenseId": "Adobe-Glyph", "seeAlso": [ "https://fedoraproject.org/wiki/Licensing/MIT#AdobeGlyph" ], "isOsiApproved": false }, { "reference": "./Afmparse.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/Afmparse.json", "referenceNumber": "42", "name": "Afmparse License", "licenseId": "Afmparse", "seeAlso": [ "https://fedoraproject.org/wiki/Licensing/Afmparse" ], "isOsiApproved": false }, { "reference": "./Aladdin.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/Aladdin.json", "referenceNumber": "258", "name": "Aladdin Free Public License", "licenseId": "Aladdin", "seeAlso": [ "http://pages.cs.wisc.edu/~ghost/doc/AFPL/6.01/Public.htm" ], "isOsiApproved": false }, { "reference": "./Apache-1.0.html", "isDeprecatedLicenseId": false, "isFsfLibre": true, "detailsUrl": "http://spdx.org/licenses/Apache-1.0.json", "referenceNumber": "237", "name": "Apache License 1.0", "licenseId": "Apache-1.0", "seeAlso": [ "http://www.apache.org/licenses/LICENSE-1.0" ], "isOsiApproved": false }, { "reference": "./Apache-1.1.html", "isDeprecatedLicenseId": false, "isFsfLibre": true, "detailsUrl": "http://spdx.org/licenses/Apache-1.1.json", "referenceNumber": "84", "name": "Apache License 1.1", "licenseId": "Apache-1.1", "seeAlso": [ "http://apache.org/licenses/LICENSE-1.1", "https://opensource.org/licenses/Apache-1.1" ], "isOsiApproved": true }, { "reference": "./Apache-2.0.html", "isDeprecatedLicenseId": false, "isFsfLibre": true, "detailsUrl": "http://spdx.org/licenses/Apache-2.0.json", "referenceNumber": "26", "name": "Apache License 2.0", "licenseId": "Apache-2.0", "seeAlso": [ "http://www.apache.org/licenses/LICENSE-2.0", "https://opensource.org/licenses/Apache-2.0" ], "isOsiApproved": true }, { "reference": "./Artistic-1.0.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/Artistic-1.0.json", "referenceNumber": "165", "name": "Artistic License 1.0", "licenseId": "Artistic-1.0", "seeAlso": [ "https://opensource.org/licenses/Artistic-1.0" ], "isOsiApproved": true }, { "reference": "./Artistic-1.0-Perl.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/Artistic-1.0-Perl.json", "referenceNumber": "377", "name": "Artistic License 1.0 (Perl)", "licenseId": "Artistic-1.0-Perl", "seeAlso": [ "http://dev.perl.org/licenses/artistic.html" ], "isOsiApproved": true }, { "reference": "./Artistic-1.0-cl8.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/Artistic-1.0-cl8.json", "referenceNumber": "13", "name": "Artistic License 1.0 w/clause 8", "licenseId": "Artistic-1.0-cl8", "seeAlso": [ "https://opensource.org/licenses/Artistic-1.0" ], "isOsiApproved": true }, { "reference": "./Artistic-2.0.html", "isDeprecatedLicenseId": false, "isFsfLibre": true, "detailsUrl": "http://spdx.org/licenses/Artistic-2.0.json", "referenceNumber": "189", "name": "Artistic License 2.0", "licenseId": "Artistic-2.0", "seeAlso": [ "http://www.perlfoundation.org/artistic_license_2_0", "https://opensource.org/licenses/artistic-license-2.0" ], "isOsiApproved": true }, { "reference": "./BSD-1-Clause.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/BSD-1-Clause.json", "referenceNumber": "358", "name": "BSD 1-Clause License", "licenseId": "BSD-1-Clause", "seeAlso": [ "https://svnweb.freebsd.org/base/head/include/ifaddrs.h?revision\u003d326823" ], "isOsiApproved": false }, { "reference": "./BSD-2-Clause.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/BSD-2-Clause.json", "referenceNumber": "325", "name": "BSD 2-Clause \"Simplified\" License", "licenseId": "BSD-2-Clause", "seeAlso": [ "https://opensource.org/licenses/BSD-2-Clause" ], "isOsiApproved": true }, { "reference": "./BSD-2-Clause-FreeBSD.html", "isDeprecatedLicenseId": false, "isFsfLibre": true, "detailsUrl": "http://spdx.org/licenses/BSD-2-Clause-FreeBSD.json", "referenceNumber": "121", "name": "BSD 2-Clause FreeBSD License", "licenseId": "BSD-2-Clause-FreeBSD", "seeAlso": [ "http://www.freebsd.org/copyright/freebsd-license.html" ], "isOsiApproved": false }, { "reference": "./BSD-2-Clause-NetBSD.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/BSD-2-Clause-NetBSD.json", "referenceNumber": "381", "name": "BSD 2-Clause NetBSD License", "licenseId": "BSD-2-Clause-NetBSD", "seeAlso": [ "http://www.netbsd.org/about/redistribution.html#default" ], "isOsiApproved": false }, { "reference": "./BSD-2-Clause-Patent.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/BSD-2-Clause-Patent.json", "referenceNumber": "169", "name": "BSD-2-Clause Plus Patent License", "licenseId": "BSD-2-Clause-Patent", "seeAlso": [ "https://opensource.org/licenses/BSDplusPatent" ], "isOsiApproved": true }, { "reference": "./BSD-3-Clause.html", "isDeprecatedLicenseId": false, "isFsfLibre": true, "detailsUrl": "http://spdx.org/licenses/BSD-3-Clause.json", "referenceNumber": "270", "name": "BSD 3-Clause \"New\" or \"Revised\" License", "licenseId": "BSD-3-Clause", "seeAlso": [ "https://opensource.org/licenses/BSD-3-Clause" ], "isOsiApproved": true }, { "reference": "./BSD-3-Clause-Attribution.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/BSD-3-Clause-Attribution.json", "referenceNumber": "39", "name": "BSD with attribution", "licenseId": "BSD-3-Clause-Attribution", "seeAlso": [ "https://fedoraproject.org/wiki/Licensing/BSD_with_Attribution" ], "isOsiApproved": false }, { "reference": "./BSD-3-Clause-Clear.html", "isDeprecatedLicenseId": false, "isFsfLibre": true, "detailsUrl": "http://spdx.org/licenses/BSD-3-Clause-Clear.json", "referenceNumber": "212", "name": "BSD 3-Clause Clear License", "licenseId": "BSD-3-Clause-Clear", "seeAlso": [ "http://labs.metacarta.com/license-explanation.html#license" ], "isOsiApproved": false }, { "reference": "./BSD-3-Clause-LBNL.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/BSD-3-Clause-LBNL.json", "referenceNumber": "337", "name": "Lawrence Berkeley National Labs BSD variant license", "licenseId": "BSD-3-Clause-LBNL", "seeAlso": [ "https://fedoraproject.org/wiki/Licensing/LBNLBSD" ], "isOsiApproved": true }, { "reference": "./BSD-3-Clause-No-Nuclear-License.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/BSD-3-Clause-No-Nuclear-License.json", "referenceNumber": "12", "name": "BSD 3-Clause No Nuclear License", "licenseId": "BSD-3-Clause-No-Nuclear-License", "seeAlso": [ "http://download.oracle.com/otn-pub/java/licenses/bsd.txt?AuthParam\u003d1467140197_43d516ce1776bd08a58235a7785be1cc" ], "isOsiApproved": false }, { "reference": "./BSD-3-Clause-No-Nuclear-License-2014.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/BSD-3-Clause-No-Nuclear-License-2014.json", "referenceNumber": "137", "name": "BSD 3-Clause No Nuclear License 2014", "licenseId": "BSD-3-Clause-No-Nuclear-License-2014", "seeAlso": [ "https://java.net/projects/javaeetutorial/pages/BerkeleyLicense" ], "isOsiApproved": false }, { "reference": "./BSD-3-Clause-No-Nuclear-Warranty.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/BSD-3-Clause-No-Nuclear-Warranty.json", "referenceNumber": "44", "name": "BSD 3-Clause No Nuclear Warranty", "licenseId": "BSD-3-Clause-No-Nuclear-Warranty", "seeAlso": [ "https://jogamp.org/git/?p\u003dgluegen.git;a\u003dblob_plain;f\u003dLICENSE.txt" ], "isOsiApproved": false }, { "reference": "./BSD-3-Clause-Open-MPI.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/BSD-3-Clause-Open-MPI.json", "referenceNumber": "349", "name": "BSD 3-Clause Open MPI variant", "licenseId": "BSD-3-Clause-Open-MPI", "seeAlso": [ "https://www.open-mpi.org/community/license.php", "http://www.netlib.org/lapack/LICENSE.txt" ], "isOsiApproved": false }, { "reference": "./BSD-4-Clause.html", "isDeprecatedLicenseId": false, "isFsfLibre": true, "detailsUrl": "http://spdx.org/licenses/BSD-4-Clause.json", "referenceNumber": "162", "name": "BSD 4-Clause \"Original\" or \"Old\" License", "licenseId": "BSD-4-Clause", "seeAlso": [ "http://directory.fsf.org/wiki/License:BSD_4Clause" ], "isOsiApproved": false }, { "reference": "./BSD-4-Clause-UC.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/BSD-4-Clause-UC.json", "referenceNumber": "203", "name": "BSD-4-Clause (University of California-Specific)", "licenseId": "BSD-4-Clause-UC", "seeAlso": [ "http://www.freebsd.org/copyright/license.html" ], "isOsiApproved": false }, { "reference": "./BSD-Protection.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/BSD-Protection.json", "referenceNumber": "119", "name": "BSD Protection License", "licenseId": "BSD-Protection", "seeAlso": [ "https://fedoraproject.org/wiki/Licensing/BSD_Protection_License" ], "isOsiApproved": false }, { "reference": "./BSD-Source-Code.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/BSD-Source-Code.json", "referenceNumber": "308", "name": "BSD Source Code Attribution", "licenseId": "BSD-Source-Code", "seeAlso": [ "https://github.com/robbiehanson/CocoaHTTPServer/blob/master/LICENSE.txt" ], "isOsiApproved": false }, { "reference": "./BSL-1.0.html", "isDeprecatedLicenseId": false, "isFsfLibre": true, "detailsUrl": "http://spdx.org/licenses/BSL-1.0.json", "referenceNumber": "224", "name": "Boost Software License 1.0", "licenseId": "BSL-1.0", "seeAlso": [ "http://www.boost.org/LICENSE_1_0.txt", "https://opensource.org/licenses/BSL-1.0" ], "isOsiApproved": true }, { "reference": "./Bahyph.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/Bahyph.json", "referenceNumber": "366", "name": "Bahyph License", "licenseId": "Bahyph", "seeAlso": [ "https://fedoraproject.org/wiki/Licensing/Bahyph" ], "isOsiApproved": false }, { "reference": "./Barr.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/Barr.json", "referenceNumber": "333", "name": "Barr License", "licenseId": "Barr", "seeAlso": [ "https://fedoraproject.org/wiki/Licensing/Barr" ], "isOsiApproved": false }, { "reference": "./Beerware.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/Beerware.json", "referenceNumber": "17", "name": "Beerware License", "licenseId": "Beerware", "seeAlso": [ "https://fedoraproject.org/wiki/Licensing/Beerware", "https://people.freebsd.org/~phk/" ], "isOsiApproved": false }, { "reference": "./BitTorrent-1.0.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/BitTorrent-1.0.json", "referenceNumber": "218", "name": "BitTorrent Open Source License v1.0", "licenseId": "BitTorrent-1.0", "seeAlso": [ "http://sources.gentoo.org/cgi-bin/viewvc.cgi/gentoo-x86/licenses/BitTorrent?r1\u003d1.1\u0026r2\u003d1.1.1.1\u0026diff_format\u003ds" ], "isOsiApproved": false }, { "reference": "./BitTorrent-1.1.html", "isDeprecatedLicenseId": false, "isFsfLibre": true, "detailsUrl": "http://spdx.org/licenses/BitTorrent-1.1.json", "referenceNumber": "179", "name": "BitTorrent Open Source License v1.1", "licenseId": "BitTorrent-1.1", "seeAlso": [ "http://directory.fsf.org/wiki/License:BitTorrentOSL1.1" ], "isOsiApproved": false }, { "reference": "./BlueOak-1.0.0.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/BlueOak-1.0.0.json", "referenceNumber": "23", "name": "Blue Oak Model License 1.0.0", "licenseId": "BlueOak-1.0.0", "seeAlso": [ "https://blueoakcouncil.org/license/1.0.0" ], "isOsiApproved": false }, { "reference": "./Borceux.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/Borceux.json", "referenceNumber": "311", "name": "Borceux license", "licenseId": "Borceux", "seeAlso": [ "https://fedoraproject.org/wiki/Licensing/Borceux" ], "isOsiApproved": false }, { "reference": "./CATOSL-1.1.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/CATOSL-1.1.json", "referenceNumber": "262", "name": "Computer Associates Trusted Open Source License 1.1", "licenseId": "CATOSL-1.1", "seeAlso": [ "https://opensource.org/licenses/CATOSL-1.1" ], "isOsiApproved": true }, { "reference": "./CC-BY-1.0.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/CC-BY-1.0.json", "referenceNumber": "128", "name": "Creative Commons Attribution 1.0 Generic", "licenseId": "CC-BY-1.0", "seeAlso": [ "https://creativecommons.org/licenses/by/1.0/legalcode" ], "isOsiApproved": false }, { "reference": "./CC-BY-2.0.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/CC-BY-2.0.json", "referenceNumber": "232", "name": "Creative Commons Attribution 2.0 Generic", "licenseId": "CC-BY-2.0", "seeAlso": [ "https://creativecommons.org/licenses/by/2.0/legalcode" ], "isOsiApproved": false }, { "reference": "./CC-BY-2.5.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/CC-BY-2.5.json", "referenceNumber": "129", "name": "Creative Commons Attribution 2.5 Generic", "licenseId": "CC-BY-2.5", "seeAlso": [ "https://creativecommons.org/licenses/by/2.5/legalcode" ], "isOsiApproved": false }, { "reference": "./CC-BY-3.0.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/CC-BY-3.0.json", "referenceNumber": "256", "name": "Creative Commons Attribution 3.0 Unported", "licenseId": "CC-BY-3.0", "seeAlso": [ "https://creativecommons.org/licenses/by/3.0/legalcode" ], "isOsiApproved": false }, { "reference": "./CC-BY-4.0.html", "isDeprecatedLicenseId": false, "isFsfLibre": true, "detailsUrl": "http://spdx.org/licenses/CC-BY-4.0.json", "referenceNumber": "330", "name": "Creative Commons Attribution 4.0 International", "licenseId": "CC-BY-4.0", "seeAlso": [ "https://creativecommons.org/licenses/by/4.0/legalcode" ], "isOsiApproved": false }, { "reference": "./CC-BY-NC-1.0.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/CC-BY-NC-1.0.json", "referenceNumber": "130", "name": "Creative Commons Attribution Non Commercial 1.0 Generic", "licenseId": "CC-BY-NC-1.0", "seeAlso": [ "https://creativecommons.org/licenses/by-nc/1.0/legalcode" ], "isOsiApproved": false }, { "reference": "./CC-BY-NC-2.0.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/CC-BY-NC-2.0.json", "referenceNumber": "244", "name": "Creative Commons Attribution Non Commercial 2.0 Generic", "licenseId": "CC-BY-NC-2.0", "seeAlso": [ "https://creativecommons.org/licenses/by-nc/2.0/legalcode" ], "isOsiApproved": false }, { "reference": "./CC-BY-NC-2.5.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/CC-BY-NC-2.5.json", "referenceNumber": "1", "name": "Creative Commons Attribution Non Commercial 2.5 Generic", "licenseId": "CC-BY-NC-2.5", "seeAlso": [ "https://creativecommons.org/licenses/by-nc/2.5/legalcode" ], "isOsiApproved": false }, { "reference": "./CC-BY-NC-3.0.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/CC-BY-NC-3.0.json", "referenceNumber": "255", "name": "Creative Commons Attribution Non Commercial 3.0 Unported", "licenseId": "CC-BY-NC-3.0", "seeAlso": [ "https://creativecommons.org/licenses/by-nc/3.0/legalcode" ], "isOsiApproved": false }, { "reference": "./CC-BY-NC-4.0.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/CC-BY-NC-4.0.json", "referenceNumber": "186", "name": "Creative Commons Attribution Non Commercial 4.0 International", "licenseId": "CC-BY-NC-4.0", "seeAlso": [ "https://creativecommons.org/licenses/by-nc/4.0/legalcode" ], "isOsiApproved": false }, { "reference": "./CC-BY-NC-ND-1.0.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/CC-BY-NC-ND-1.0.json", "referenceNumber": "59", "name": "Creative Commons Attribution Non Commercial No Derivatives 1.0 Generic", "licenseId": "CC-BY-NC-ND-1.0", "seeAlso": [ "https://creativecommons.org/licenses/by-nd-nc/1.0/legalcode" ], "isOsiApproved": false }, { "reference": "./CC-BY-NC-ND-2.0.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/CC-BY-NC-ND-2.0.json", "referenceNumber": "36", "name": "Creative Commons Attribution Non Commercial No Derivatives 2.0 Generic", "licenseId": "CC-BY-NC-ND-2.0", "seeAlso": [ "https://creativecommons.org/licenses/by-nc-nd/2.0/legalcode" ], "isOsiApproved": false }, { "reference": "./CC-BY-NC-ND-2.5.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/CC-BY-NC-ND-2.5.json", "referenceNumber": "158", "name": "Creative Commons Attribution Non Commercial No Derivatives 2.5 Generic", "licenseId": "CC-BY-NC-ND-2.5", "seeAlso": [ "https://creativecommons.org/licenses/by-nc-nd/2.5/legalcode" ], "isOsiApproved": false }, { "reference": "./CC-BY-NC-ND-3.0.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/CC-BY-NC-ND-3.0.json", "referenceNumber": "48", "name": "Creative Commons Attribution Non Commercial No Derivatives 3.0 Unported", "licenseId": "CC-BY-NC-ND-3.0", "seeAlso": [ "https://creativecommons.org/licenses/by-nc-nd/3.0/legalcode" ], "isOsiApproved": false }, { "reference": "./CC-BY-NC-ND-4.0.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/CC-BY-NC-ND-4.0.json", "referenceNumber": "281", "name": "Creative Commons Attribution Non Commercial No Derivatives 4.0 International", "licenseId": "CC-BY-NC-ND-4.0", "seeAlso": [ "https://creativecommons.org/licenses/by-nc-nd/4.0/legalcode" ], "isOsiApproved": false }, { "reference": "./CC-BY-NC-SA-1.0.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/CC-BY-NC-SA-1.0.json", "referenceNumber": "178", "name": "Creative Commons Attribution Non Commercial Share Alike 1.0 Generic", "licenseId": "CC-BY-NC-SA-1.0", "seeAlso": [ "https://creativecommons.org/licenses/by-nc-sa/1.0/legalcode" ], "isOsiApproved": false }, { "reference": "./CC-BY-NC-SA-2.0.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/CC-BY-NC-SA-2.0.json", "referenceNumber": "81", "name": "Creative Commons Attribution Non Commercial Share Alike 2.0 Generic", "licenseId": "CC-BY-NC-SA-2.0", "seeAlso": [ "https://creativecommons.org/licenses/by-nc-sa/2.0/legalcode" ], "isOsiApproved": false }, { "reference": "./CC-BY-NC-SA-2.5.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/CC-BY-NC-SA-2.5.json", "referenceNumber": "62", "name": "Creative Commons Attribution Non Commercial Share Alike 2.5 Generic", "licenseId": "CC-BY-NC-SA-2.5", "seeAlso": [ "https://creativecommons.org/licenses/by-nc-sa/2.5/legalcode" ], "isOsiApproved": false }, { "reference": "./CC-BY-NC-SA-3.0.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/CC-BY-NC-SA-3.0.json", "referenceNumber": "22", "name": "Creative Commons Attribution Non Commercial Share Alike 3.0 Unported", "licenseId": "CC-BY-NC-SA-3.0", "seeAlso": [ "https://creativecommons.org/licenses/by-nc-sa/3.0/legalcode" ], "isOsiApproved": false }, { "reference": "./CC-BY-NC-SA-4.0.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/CC-BY-NC-SA-4.0.json", "referenceNumber": "47", "name": "Creative Commons Attribution Non Commercial Share Alike 4.0 International", "licenseId": "CC-BY-NC-SA-4.0", "seeAlso": [ "https://creativecommons.org/licenses/by-nc-sa/4.0/legalcode" ], "isOsiApproved": false }, { "reference": "./CC-BY-ND-1.0.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/CC-BY-ND-1.0.json", "referenceNumber": "50", "name": "Creative Commons Attribution No Derivatives 1.0 Generic", "licenseId": "CC-BY-ND-1.0", "seeAlso": [ "https://creativecommons.org/licenses/by-nd/1.0/legalcode" ], "isOsiApproved": false }, { "reference": "./CC-BY-ND-2.0.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/CC-BY-ND-2.0.json", "referenceNumber": "287", "name": "Creative Commons Attribution No Derivatives 2.0 Generic", "licenseId": "CC-BY-ND-2.0", "seeAlso": [ "https://creativecommons.org/licenses/by-nd/2.0/legalcode" ], "isOsiApproved": false }, { "reference": "./CC-BY-ND-2.5.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/CC-BY-ND-2.5.json", "referenceNumber": "68", "name": "Creative Commons Attribution No Derivatives 2.5 Generic", "licenseId": "CC-BY-ND-2.5", "seeAlso": [ "https://creativecommons.org/licenses/by-nd/2.5/legalcode" ], "isOsiApproved": false }, { "reference": "./CC-BY-ND-3.0.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/CC-BY-ND-3.0.json", "referenceNumber": "393", "name": "Creative Commons Attribution No Derivatives 3.0 Unported", "licenseId": "CC-BY-ND-3.0", "seeAlso": [ "https://creativecommons.org/licenses/by-nd/3.0/legalcode" ], "isOsiApproved": false }, { "reference": "./CC-BY-ND-4.0.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/CC-BY-ND-4.0.json", "referenceNumber": "132", "name": "Creative Commons Attribution No Derivatives 4.0 International", "licenseId": "CC-BY-ND-4.0", "seeAlso": [ "https://creativecommons.org/licenses/by-nd/4.0/legalcode" ], "isOsiApproved": false }, { "reference": "./CC-BY-SA-1.0.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/CC-BY-SA-1.0.json", "referenceNumber": "322", "name": "Creative Commons Attribution Share Alike 1.0 Generic", "licenseId": "CC-BY-SA-1.0", "seeAlso": [ "https://creativecommons.org/licenses/by-sa/1.0/legalcode" ], "isOsiApproved": false }, { "reference": "./CC-BY-SA-2.0.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/CC-BY-SA-2.0.json", "referenceNumber": "142", "name": "Creative Commons Attribution Share Alike 2.0 Generic", "licenseId": "CC-BY-SA-2.0", "seeAlso": [ "https://creativecommons.org/licenses/by-sa/2.0/legalcode" ], "isOsiApproved": false }, { "reference": "./CC-BY-SA-2.5.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/CC-BY-SA-2.5.json", "referenceNumber": "306", "name": "Creative Commons Attribution Share Alike 2.5 Generic", "licenseId": "CC-BY-SA-2.5", "seeAlso": [ "https://creativecommons.org/licenses/by-sa/2.5/legalcode" ], "isOsiApproved": false }, { "reference": "./CC-BY-SA-3.0.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/CC-BY-SA-3.0.json", "referenceNumber": "394", "name": "Creative Commons Attribution Share Alike 3.0 Unported", "licenseId": "CC-BY-SA-3.0", "seeAlso": [ "https://creativecommons.org/licenses/by-sa/3.0/legalcode" ], "isOsiApproved": false }, { "reference": "./CC-BY-SA-4.0.html", "isDeprecatedLicenseId": false, "isFsfLibre": true, "detailsUrl": "http://spdx.org/licenses/CC-BY-SA-4.0.json", "referenceNumber": "32", "name": "Creative Commons Attribution Share Alike 4.0 International", "licenseId": "CC-BY-SA-4.0", "seeAlso": [ "https://creativecommons.org/licenses/by-sa/4.0/legalcode" ], "isOsiApproved": false }, { "reference": "./CC-PDDC.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/CC-PDDC.json", "referenceNumber": "371", "name": "Creative Commons Public Domain Dedication and Certification", "licenseId": "CC-PDDC", "seeAlso": [ "https://creativecommons.org/licenses/publicdomain/" ], "isOsiApproved": false }, { "reference": "./CC0-1.0.html", "isDeprecatedLicenseId": false, "isFsfLibre": true, "detailsUrl": "http://spdx.org/licenses/CC0-1.0.json", "referenceNumber": "213", "name": "Creative Commons Zero v1.0 Universal", "licenseId": "CC0-1.0", "seeAlso": [ "https://creativecommons.org/publicdomain/zero/1.0/legalcode" ], "isOsiApproved": false }, { "reference": "./CDDL-1.0.html", "isDeprecatedLicenseId": false, "isFsfLibre": true, "detailsUrl": "http://spdx.org/licenses/CDDL-1.0.json", "referenceNumber": "138", "name": "Common Development and Distribution License 1.0", "licenseId": "CDDL-1.0", "seeAlso": [ "https://opensource.org/licenses/cddl1" ], "isOsiApproved": true }, { "reference": "./CDDL-1.1.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/CDDL-1.1.json", "referenceNumber": "376", "name": "Common Development and Distribution License 1.1", "licenseId": "CDDL-1.1", "seeAlso": [ "http://glassfish.java.net/public/CDDL+GPL_1_1.html", "https://javaee.github.io/glassfish/LICENSE" ], "isOsiApproved": false }, { "reference": "./CDLA-Permissive-1.0.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/CDLA-Permissive-1.0.json", "referenceNumber": "250", "name": "Community Data License Agreement Permissive 1.0", "licenseId": "CDLA-Permissive-1.0", "seeAlso": [ "https://cdla.io/permissive-1-0" ], "isOsiApproved": false }, { "reference": "./CDLA-Sharing-1.0.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/CDLA-Sharing-1.0.json", "referenceNumber": "310", "name": "Community Data License Agreement Sharing 1.0", "licenseId": "CDLA-Sharing-1.0", "seeAlso": [ "https://cdla.io/sharing-1-0" ], "isOsiApproved": false }, { "reference": "./CECILL-1.0.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/CECILL-1.0.json", "referenceNumber": "223", "name": "CeCILL Free Software License Agreement v1.0", "licenseId": "CECILL-1.0", "seeAlso": [ "http://www.cecill.info/licences/Licence_CeCILL_V1-fr.html" ], "isOsiApproved": false }, { "reference": "./CECILL-1.1.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/CECILL-1.1.json", "referenceNumber": "300", "name": "CeCILL Free Software License Agreement v1.1", "licenseId": "CECILL-1.1", "seeAlso": [ "http://www.cecill.info/licences/Licence_CeCILL_V1.1-US.html" ], "isOsiApproved": false }, { "reference": "./CECILL-2.0.html", "isDeprecatedLicenseId": false, "isFsfLibre": true, "detailsUrl": "http://spdx.org/licenses/CECILL-2.0.json", "referenceNumber": "352", "name": "CeCILL Free Software License Agreement v2.0", "licenseId": "CECILL-2.0", "seeAlso": [ "http://www.cecill.info/licences/Licence_CeCILL_V2-en.html" ], "isOsiApproved": false }, { "reference": "./CECILL-2.1.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/CECILL-2.1.json", "referenceNumber": "120", "name": "CeCILL Free Software License Agreement v2.1", "licenseId": "CECILL-2.1", "seeAlso": [ "http://www.cecill.info/licences/Licence_CeCILL_V2.1-en.html" ], "isOsiApproved": true }, { "reference": "./CECILL-B.html", "isDeprecatedLicenseId": false, "isFsfLibre": true, "detailsUrl": "http://spdx.org/licenses/CECILL-B.json", "referenceNumber": "340", "name": "CeCILL-B Free Software License Agreement", "licenseId": "CECILL-B", "seeAlso": [ "http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.html" ], "isOsiApproved": false }, { "reference": "./CECILL-C.html", "isDeprecatedLicenseId": false, "isFsfLibre": true, "detailsUrl": "http://spdx.org/licenses/CECILL-C.json", "referenceNumber": "77", "name": "CeCILL-C Free Software License Agreement", "licenseId": "CECILL-C", "seeAlso": [ "http://www.cecill.info/licences/Licence_CeCILL-C_V1-en.html" ], "isOsiApproved": false }, { "reference": "./CERN-OHL-1.1.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/CERN-OHL-1.1.json", "referenceNumber": "341", "name": "CERN Open Hardware License v1.1", "licenseId": "CERN-OHL-1.1", "seeAlso": [ "https://www.ohwr.org/project/licenses/wikis/cern-ohl-v1.1" ], "isOsiApproved": false }, { "reference": "./CERN-OHL-1.2.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/CERN-OHL-1.2.json", "referenceNumber": "3", "name": "CERN Open Hardware Licence v1.2", "licenseId": "CERN-OHL-1.2", "seeAlso": [ "https://www.ohwr.org/project/licenses/wikis/cern-ohl-v1.2" ], "isOsiApproved": false }, { "reference": "./CNRI-Jython.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/CNRI-Jython.json", "referenceNumber": "94", "name": "CNRI Jython License", "licenseId": "CNRI-Jython", "seeAlso": [ "http://www.jython.org/license.html" ], "isOsiApproved": false }, { "reference": "./CNRI-Python.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/CNRI-Python.json", "referenceNumber": "45", "name": "CNRI Python License", "licenseId": "CNRI-Python", "seeAlso": [ "https://opensource.org/licenses/CNRI-Python" ], "isOsiApproved": true }, { "reference": "./CNRI-Python-GPL-Compatible.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/CNRI-Python-GPL-Compatible.json", "referenceNumber": "202", "name": "CNRI Python Open Source GPL Compatible License Agreement", "licenseId": "CNRI-Python-GPL-Compatible", "seeAlso": [ "http://www.python.org/download/releases/1.6.1/download_win/" ], "isOsiApproved": false }, { "reference": "./CPAL-1.0.html", "isDeprecatedLicenseId": false, "isFsfLibre": true, "detailsUrl": "http://spdx.org/licenses/CPAL-1.0.json", "referenceNumber": "170", "name": "Common Public Attribution License 1.0", "licenseId": "CPAL-1.0", "seeAlso": [ "https://opensource.org/licenses/CPAL-1.0" ], "isOsiApproved": true }, { "reference": "./CPL-1.0.html", "isDeprecatedLicenseId": false, "isFsfLibre": true, "detailsUrl": "http://spdx.org/licenses/CPL-1.0.json", "referenceNumber": "172", "name": "Common Public License 1.0", "licenseId": "CPL-1.0", "seeAlso": [ "https://opensource.org/licenses/CPL-1.0" ], "isOsiApproved": true }, { "reference": "./CPOL-1.02.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/CPOL-1.02.json", "referenceNumber": "28", "name": "Code Project Open License 1.02", "licenseId": "CPOL-1.02", "seeAlso": [ "http://www.codeproject.com/info/cpol10.aspx" ], "isOsiApproved": false }, { "reference": "./CUA-OPL-1.0.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/CUA-OPL-1.0.json", "referenceNumber": "365", "name": "CUA Office Public License v1.0", "licenseId": "CUA-OPL-1.0", "seeAlso": [ "https://opensource.org/licenses/CUA-OPL-1.0" ], "isOsiApproved": true }, { "reference": "./Caldera.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/Caldera.json", "referenceNumber": "108", "name": "Caldera License", "licenseId": "Caldera", "seeAlso": [ "http://www.lemis.com/grog/UNIX/ancient-source-all.pdf" ], "isOsiApproved": false }, { "reference": "./ClArtistic.html", "isDeprecatedLicenseId": false, "isFsfLibre": true, "detailsUrl": "http://spdx.org/licenses/ClArtistic.json", "referenceNumber": "271", "name": "Clarified Artistic License", "licenseId": "ClArtistic", "seeAlso": [ "http://gianluca.dellavedova.org/2011/01/03/clarified-artistic-license/", "http://www.ncftp.com/ncftp/doc/LICENSE.txt" ], "isOsiApproved": false }, { "reference": "./Condor-1.1.html", "isDeprecatedLicenseId": false, "isFsfLibre": true, "detailsUrl": "http://spdx.org/licenses/Condor-1.1.json", "referenceNumber": "307", "name": "Condor Public License v1.1", "licenseId": "Condor-1.1", "seeAlso": [ "http://research.cs.wisc.edu/condor/license.html#condor", "http://web.archive.org/web/20111123062036/http://research.cs.wisc.edu/condor/license.html#condor" ], "isOsiApproved": false }, { "reference": "./Crossword.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/Crossword.json", "referenceNumber": "363", "name": "Crossword License", "licenseId": "Crossword", "seeAlso": [ "https://fedoraproject.org/wiki/Licensing/Crossword" ], "isOsiApproved": false }, { "reference": "./CrystalStacker.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/CrystalStacker.json", "referenceNumber": "168", "name": "CrystalStacker License", "licenseId": "CrystalStacker", "seeAlso": [ "https://fedoraproject.org/wiki/Licensing:CrystalStacker?rd\u003dLicensing/CrystalStacker" ], "isOsiApproved": false }, { "reference": "./Cube.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/Cube.json", "referenceNumber": "370", "name": "Cube License", "licenseId": "Cube", "seeAlso": [ "https://fedoraproject.org/wiki/Licensing/Cube" ], "isOsiApproved": false }, { "reference": "./D-FSL-1.0.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/D-FSL-1.0.json", "referenceNumber": "182", "name": "Deutsche Freie Software Lizenz", "licenseId": "D-FSL-1.0", "seeAlso": [ "http://www.dipp.nrw.de/d-fsl/lizenzen/", "http://www.dipp.nrw.de/d-fsl/index_html/lizenzen/de/D-FSL-1_0_de.txt", "http://www.dipp.nrw.de/d-fsl/index_html/lizenzen/en/D-FSL-1_0_en.txt", "https://www.hbz-nrw.de/produkte/open-access/lizenzen/dfsl", "https://www.hbz-nrw.de/produkte/open-access/lizenzen/dfsl/deutsche-freie-software-lizenz", "https://www.hbz-nrw.de/produkte/open-access/lizenzen/dfsl/german-free-software-license", "https://www.hbz-nrw.de/produkte/open-access/lizenzen/dfsl/D-FSL-1_0_de.txt/at_download/file", "https://www.hbz-nrw.de/produkte/open-access/lizenzen/dfsl/D-FSL-1_0_en.txt/at_download/file" ], "isOsiApproved": false }, { "reference": "./DOC.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/DOC.json", "referenceNumber": "160", "name": "DOC License", "licenseId": "DOC", "seeAlso": [ "http://www.cs.wustl.edu/~schmidt/ACE-copying.html" ], "isOsiApproved": false }, { "reference": "./DSDP.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/DSDP.json", "referenceNumber": "141", "name": "DSDP License", "licenseId": "DSDP", "seeAlso": [ "https://fedoraproject.org/wiki/Licensing/DSDP" ], "isOsiApproved": false }, { "reference": "./Dotseqn.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/Dotseqn.json", "referenceNumber": "390", "name": "Dotseqn License", "licenseId": "Dotseqn", "seeAlso": [ "https://fedoraproject.org/wiki/Licensing/Dotseqn" ], "isOsiApproved": false }, { "reference": "./ECL-1.0.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/ECL-1.0.json", "referenceNumber": "396", "name": "Educational Community License v1.0", "licenseId": "ECL-1.0", "seeAlso": [ "https://opensource.org/licenses/ECL-1.0" ], "isOsiApproved": true }, { "reference": "./ECL-2.0.html", "isDeprecatedLicenseId": false, "isFsfLibre": true, "detailsUrl": "http://spdx.org/licenses/ECL-2.0.json", "referenceNumber": "298", "name": "Educational Community License v2.0", "licenseId": "ECL-2.0", "seeAlso": [ "https://opensource.org/licenses/ECL-2.0" ], "isOsiApproved": true }, { "reference": "./EFL-1.0.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/EFL-1.0.json", "referenceNumber": "150", "name": "Eiffel Forum License v1.0", "licenseId": "EFL-1.0", "seeAlso": [ "http://www.eiffel-nice.org/license/forum.txt", "https://opensource.org/licenses/EFL-1.0" ], "isOsiApproved": true }, { "reference": "./EFL-2.0.html", "isDeprecatedLicenseId": false, "isFsfLibre": true, "detailsUrl": "http://spdx.org/licenses/EFL-2.0.json", "referenceNumber": "161", "name": "Eiffel Forum License v2.0", "licenseId": "EFL-2.0", "seeAlso": [ "http://www.eiffel-nice.org/license/eiffel-forum-license-2.html", "https://opensource.org/licenses/EFL-2.0" ], "isOsiApproved": true }, { "reference": "./EPL-1.0.html", "isDeprecatedLicenseId": false, "isFsfLibre": true, "detailsUrl": "http://spdx.org/licenses/EPL-1.0.json", "referenceNumber": "214", "name": "Eclipse Public License 1.0", "licenseId": "EPL-1.0", "seeAlso": [ "http://www.eclipse.org/legal/epl-v10.html", "https://opensource.org/licenses/EPL-1.0" ], "isOsiApproved": true }, { "reference": "./EPL-2.0.html", "isDeprecatedLicenseId": false, "isFsfLibre": true, "detailsUrl": "http://spdx.org/licenses/EPL-2.0.json", "referenceNumber": "134", "name": "Eclipse Public License 2.0", "licenseId": "EPL-2.0", "seeAlso": [ "https://www.eclipse.org/legal/epl-2.0", "https://www.opensource.org/licenses/EPL-2.0" ], "isOsiApproved": true }, { "reference": "./EUDatagrid.html", "isDeprecatedLicenseId": false, "isFsfLibre": true, "detailsUrl": "http://spdx.org/licenses/EUDatagrid.json", "referenceNumber": "192", "name": "EU DataGrid Software License", "licenseId": "EUDatagrid", "seeAlso": [ "http://eu-datagrid.web.cern.ch/eu-datagrid/license.html", "https://opensource.org/licenses/EUDatagrid" ], "isOsiApproved": true }, { "reference": "./EUPL-1.0.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/EUPL-1.0.json", "referenceNumber": "173", "name": "European Union Public License 1.0", "licenseId": "EUPL-1.0", "seeAlso": [ "http://ec.europa.eu/idabc/en/document/7330.html", "http://ec.europa.eu/idabc/servlets/Doc027f.pdf?id\u003d31096" ], "isOsiApproved": false }, { "reference": "./EUPL-1.1.html", "isDeprecatedLicenseId": false, "isFsfLibre": true, "detailsUrl": "http://spdx.org/licenses/EUPL-1.1.json", "referenceNumber": "92", "name": "European Union Public License 1.1", "licenseId": "EUPL-1.1", "seeAlso": [ "https://joinup.ec.europa.eu/software/page/eupl/licence-eupl", "https://joinup.ec.europa.eu/sites/default/files/custom-page/attachment/eupl1.1.-licence-en_0.pdf", "https://opensource.org/licenses/EUPL-1.1" ], "isOsiApproved": true }, { "reference": "./EUPL-1.2.html", "isDeprecatedLicenseId": false, "isFsfLibre": true, "detailsUrl": "http://spdx.org/licenses/EUPL-1.2.json", "referenceNumber": "387", "name": "European Union Public License 1.2", "licenseId": "EUPL-1.2", "seeAlso": [ "https://joinup.ec.europa.eu/page/eupl-text-11-12", "https://joinup.ec.europa.eu/sites/default/files/custom-page/attachment/eupl_v1.2_en.pdf", "https://joinup.ec.europa.eu/sites/default/files/inline-files/EUPL%20v1_2%20EN(1).txt", "http://eur-lex.europa.eu/legal-content/EN/TXT/HTML/?uri\u003dCELEX:32017D0863", "https://opensource.org/licenses/EUPL-1.1" ], "isOsiApproved": true }, { "reference": "./Entessa.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/Entessa.json", "referenceNumber": "99", "name": "Entessa Public License v1.0", "licenseId": "Entessa", "seeAlso": [ "https://opensource.org/licenses/Entessa" ], "isOsiApproved": true }, { "reference": "./ErlPL-1.1.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/ErlPL-1.1.json", "referenceNumber": "157", "name": "Erlang Public License v1.1", "licenseId": "ErlPL-1.1", "seeAlso": [ "http://www.erlang.org/EPLICENSE" ], "isOsiApproved": false }, { "reference": "./Eurosym.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/Eurosym.json", "referenceNumber": "113", "name": "Eurosym License", "licenseId": "Eurosym", "seeAlso": [ "https://fedoraproject.org/wiki/Licensing/Eurosym" ], "isOsiApproved": false }, { "reference": "./FSFAP.html", "isDeprecatedLicenseId": false, "isFsfLibre": true, "detailsUrl": "http://spdx.org/licenses/FSFAP.json", "referenceNumber": "114", "name": "FSF All Permissive License", "licenseId": "FSFAP", "seeAlso": [ "https://www.gnu.org/prep/maintain/html_node/License-Notices-for-Other-Files.html" ], "isOsiApproved": false }, { "reference": "./FSFUL.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/FSFUL.json", "referenceNumber": "193", "name": "FSF Unlimited License", "licenseId": "FSFUL", "seeAlso": [ "https://fedoraproject.org/wiki/Licensing/FSF_Unlimited_License" ], "isOsiApproved": false }, { "reference": "./FSFULLR.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/FSFULLR.json", "referenceNumber": "43", "name": "FSF Unlimited License (with License Retention)", "licenseId": "FSFULLR", "seeAlso": [ "https://fedoraproject.org/wiki/Licensing/FSF_Unlimited_License#License_Retention_Variant" ], "isOsiApproved": false }, { "reference": "./FTL.html", "isDeprecatedLicenseId": false, "isFsfLibre": true, "detailsUrl": "http://spdx.org/licenses/FTL.json", "referenceNumber": "240", "name": "Freetype Project License", "licenseId": "FTL", "seeAlso": [ "http://freetype.fis.uniroma2.it/FTL.TXT", "http://git.savannah.gnu.org/cgit/freetype/freetype2.git/tree/docs/FTL.TXT" ], "isOsiApproved": false }, { "reference": "./Fair.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/Fair.json", "referenceNumber": "297", "name": "Fair License", "licenseId": "Fair", "seeAlso": [ "http://fairlicense.org/", "https://opensource.org/licenses/Fair" ], "isOsiApproved": true }, { "reference": "./Frameworx-1.0.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/Frameworx-1.0.json", "referenceNumber": "389", "name": "Frameworx Open License 1.0", "licenseId": "Frameworx-1.0", "seeAlso": [ "https://opensource.org/licenses/Frameworx-1.0" ], "isOsiApproved": true }, { "reference": "./FreeImage.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/FreeImage.json", "referenceNumber": "277", "name": "FreeImage Public License v1.0", "licenseId": "FreeImage", "seeAlso": [ "http://freeimage.sourceforge.net/freeimage-license.txt" ], "isOsiApproved": false }, { "reference": "./GFDL-1.1.html", "isDeprecatedLicenseId": true, "isFsfLibre": true, "detailsUrl": "http://spdx.org/licenses/GFDL-1.1.json", "referenceNumber": "98", "name": "GNU Free Documentation License v1.1", "licenseId": "GFDL-1.1", "seeAlso": [ "https://www.gnu.org/licenses/old-licenses/fdl-1.1.txt" ], "isOsiApproved": false }, { "reference": "./GFDL-1.1-only.html", "isDeprecatedLicenseId": false, "isFsfLibre": true, "detailsUrl": "http://spdx.org/licenses/GFDL-1.1-only.json", "referenceNumber": "102", "name": "GNU Free Documentation License v1.1 only", "licenseId": "GFDL-1.1-only", "seeAlso": [ "https://www.gnu.org/licenses/old-licenses/fdl-1.1.txt" ], "isOsiApproved": false }, { "reference": "./GFDL-1.1-or-later.html", "isDeprecatedLicenseId": false, "isFsfLibre": true, "detailsUrl": "http://spdx.org/licenses/GFDL-1.1-or-later.json", "referenceNumber": "348", "name": "GNU Free Documentation License v1.1 or later", "licenseId": "GFDL-1.1-or-later", "seeAlso": [ "https://www.gnu.org/licenses/old-licenses/fdl-1.1.txt" ], "isOsiApproved": false }, { "reference": "./GFDL-1.2.html", "isDeprecatedLicenseId": true, "isFsfLibre": true, "detailsUrl": "http://spdx.org/licenses/GFDL-1.2.json", "referenceNumber": "197", "name": "GNU Free Documentation License v1.2", "licenseId": "GFDL-1.2", "seeAlso": [ "https://www.gnu.org/licenses/old-licenses/fdl-1.2.txt" ], "isOsiApproved": false }, { "reference": "./GFDL-1.2-only.html", "isDeprecatedLicenseId": false, "isFsfLibre": true, "detailsUrl": "http://spdx.org/licenses/GFDL-1.2-only.json", "referenceNumber": "236", "name": "GNU Free Documentation License v1.2 only", "licenseId": "GFDL-1.2-only", "seeAlso": [ "https://www.gnu.org/licenses/old-licenses/fdl-1.2.txt" ], "isOsiApproved": false }, { "reference": "./GFDL-1.2-or-later.html", "isDeprecatedLicenseId": false, "isFsfLibre": true, "detailsUrl": "http://spdx.org/licenses/GFDL-1.2-or-later.json", "referenceNumber": "215", "name": "GNU Free Documentation License v1.2 or later", "licenseId": "GFDL-1.2-or-later", "seeAlso": [ "https://www.gnu.org/licenses/old-licenses/fdl-1.2.txt" ], "isOsiApproved": false }, { "reference": "./GFDL-1.3.html", "isDeprecatedLicenseId": true, "isFsfLibre": true, "detailsUrl": "http://spdx.org/licenses/GFDL-1.3.json", "referenceNumber": "112", "name": "GNU Free Documentation License v1.3", "licenseId": "GFDL-1.3", "seeAlso": [ "https://www.gnu.org/licenses/fdl-1.3.txt" ], "isOsiApproved": false }, { "reference": "./GFDL-1.3-only.html", "isDeprecatedLicenseId": false, "isFsfLibre": true, "detailsUrl": "http://spdx.org/licenses/GFDL-1.3-only.json", "referenceNumber": "69", "name": "GNU Free Documentation License v1.3 only", "licenseId": "GFDL-1.3-only", "seeAlso": [ "https://www.gnu.org/licenses/fdl-1.3.txt" ], "isOsiApproved": false }, { "reference": "./GFDL-1.3-or-later.html", "isDeprecatedLicenseId": false, "isFsfLibre": true, "detailsUrl": "http://spdx.org/licenses/GFDL-1.3-or-later.json", "referenceNumber": "4", "name": "GNU Free Documentation License v1.3 or later", "licenseId": "GFDL-1.3-or-later", "seeAlso": [ "https://www.gnu.org/licenses/fdl-1.3.txt" ], "isOsiApproved": false }, { "reference": "./GL2PS.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/GL2PS.json", "referenceNumber": "124", "name": "GL2PS License", "licenseId": "GL2PS", "seeAlso": [ "http://www.geuz.org/gl2ps/COPYING.GL2PS" ], "isOsiApproved": false }, { "reference": "./GPL-1.0.html", "isDeprecatedLicenseId": true, "detailsUrl": "http://spdx.org/licenses/GPL-1.0.json", "referenceNumber": "79", "name": "GNU General Public License v1.0 only", "licenseId": "GPL-1.0", "seeAlso": [ "https://www.gnu.org/licenses/old-licenses/gpl-1.0-standalone.html" ], "isOsiApproved": false }, { "reference": "./GPL-1.0+.html", "isDeprecatedLicenseId": true, "detailsUrl": "http://spdx.org/licenses/GPL-1.0+.json", "referenceNumber": "175", "name": "GNU General Public License v1.0 or later", "licenseId": "GPL-1.0+", "seeAlso": [ "https://www.gnu.org/licenses/old-licenses/gpl-1.0-standalone.html" ], "isOsiApproved": false }, { "reference": "./GPL-1.0-only.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/GPL-1.0-only.json", "referenceNumber": "15", "name": "GNU General Public License v1.0 only", "licenseId": "GPL-1.0-only", "seeAlso": [ "https://www.gnu.org/licenses/old-licenses/gpl-1.0-standalone.html" ], "isOsiApproved": false }, { "reference": "./GPL-1.0-or-later.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/GPL-1.0-or-later.json", "referenceNumber": "357", "name": "GNU General Public License v1.0 or later", "licenseId": "GPL-1.0-or-later", "seeAlso": [ "https://www.gnu.org/licenses/old-licenses/gpl-1.0-standalone.html" ], "isOsiApproved": false }, { "reference": "./GPL-2.0.html", "isDeprecatedLicenseId": true, "isFsfLibre": true, "detailsUrl": "http://spdx.org/licenses/GPL-2.0.json", "referenceNumber": "147", "name": "GNU General Public License v2.0 only", "licenseId": "GPL-2.0", "seeAlso": [ "https://www.gnu.org/licenses/old-licenses/gpl-2.0-standalone.html", "https://opensource.org/licenses/GPL-2.0" ], "isOsiApproved": true }, { "reference": "./GPL-2.0+.html", "isDeprecatedLicenseId": true, "isFsfLibre": true, "detailsUrl": "http://spdx.org/licenses/GPL-2.0+.json", "referenceNumber": "75", "name": "GNU General Public License v2.0 or later", "licenseId": "GPL-2.0+", "seeAlso": [ "https://www.gnu.org/licenses/old-licenses/gpl-2.0-standalone.html", "https://opensource.org/licenses/GPL-2.0" ], "isOsiApproved": true }, { "reference": "./GPL-2.0-only.html", "isDeprecatedLicenseId": false, "isFsfLibre": true, "detailsUrl": "http://spdx.org/licenses/GPL-2.0-only.json", "referenceNumber": "233", "name": "GNU General Public License v2.0 only", "licenseId": "GPL-2.0-only", "seeAlso": [ "https://www.gnu.org/licenses/old-licenses/gpl-2.0-standalone.html", "https://opensource.org/licenses/GPL-2.0" ], "isOsiApproved": true }, { "reference": "./GPL-2.0-or-later.html", "isDeprecatedLicenseId": false, "isFsfLibre": true, "detailsUrl": "http://spdx.org/licenses/GPL-2.0-or-later.json", "referenceNumber": "56", "name": "GNU General Public License v2.0 or later", "licenseId": "GPL-2.0-or-later", "seeAlso": [ "https://www.gnu.org/licenses/old-licenses/gpl-2.0-standalone.html", "https://opensource.org/licenses/GPL-2.0" ], "isOsiApproved": true }, { "reference": "./GPL-2.0-with-GCC-exception.html", "isDeprecatedLicenseId": true, "detailsUrl": "http://spdx.org/licenses/GPL-2.0-with-GCC-exception.json", "referenceNumber": "117", "name": "GNU General Public License v2.0 w/GCC Runtime Library exception", "licenseId": "GPL-2.0-with-GCC-exception", "seeAlso": [ "https://gcc.gnu.org/git/?p\u003dgcc.git;a\u003dblob;f\u003dgcc/libgcc1.c;h\u003d762f5143fc6eed57b6797c82710f3538aa52b40b;hb\u003dcb143a3ce4fb417c68f5fa2691a1b1b1053dfba9#l10" ], "isOsiApproved": false }, { "reference": "./GPL-2.0-with-autoconf-exception.html", "isDeprecatedLicenseId": true, "detailsUrl": "http://spdx.org/licenses/GPL-2.0-with-autoconf-exception.json", "referenceNumber": "355", "name": "GNU General Public License v2.0 w/Autoconf exception", "licenseId": "GPL-2.0-with-autoconf-exception", "seeAlso": [ "http://ac-archive.sourceforge.net/doc/copyright.html" ], "isOsiApproved": false }, { "reference": "./GPL-2.0-with-bison-exception.html", "isDeprecatedLicenseId": true, "detailsUrl": "http://spdx.org/licenses/GPL-2.0-with-bison-exception.json", "referenceNumber": "378", "name": "GNU General Public License v2.0 w/Bison exception", "licenseId": "GPL-2.0-with-bison-exception", "seeAlso": [ "http://git.savannah.gnu.org/cgit/bison.git/tree/data/yacc.c?id\u003d193d7c7054ba7197b0789e14965b739162319b5e#n141" ], "isOsiApproved": false }, { "reference": "./GPL-2.0-with-classpath-exception.html", "isDeprecatedLicenseId": true, "detailsUrl": "http://spdx.org/licenses/GPL-2.0-with-classpath-exception.json", "referenceNumber": "60", "name": "GNU General Public License v2.0 w/Classpath exception", "licenseId": "GPL-2.0-with-classpath-exception", "seeAlso": [ "https://www.gnu.org/software/classpath/license.html" ], "isOsiApproved": false }, { "reference": "./GPL-2.0-with-font-exception.html", "isDeprecatedLicenseId": true, "detailsUrl": "http://spdx.org/licenses/GPL-2.0-with-font-exception.json", "referenceNumber": "375", "name": "GNU General Public License v2.0 w/Font exception", "licenseId": "GPL-2.0-with-font-exception", "seeAlso": [ "https://www.gnu.org/licenses/gpl-faq.html#FontException" ], "isOsiApproved": false }, { "reference": "./GPL-3.0.html", "isDeprecatedLicenseId": true, "isFsfLibre": true, "detailsUrl": "http://spdx.org/licenses/GPL-3.0.json", "referenceNumber": "242", "name": "GNU General Public License v3.0 only", "licenseId": "GPL-3.0", "seeAlso": [ "https://www.gnu.org/licenses/gpl-3.0-standalone.html", "https://opensource.org/licenses/GPL-3.0" ], "isOsiApproved": true }, { "reference": "./GPL-3.0+.html", "isDeprecatedLicenseId": true, "isFsfLibre": true, "detailsUrl": "http://spdx.org/licenses/GPL-3.0+.json", "referenceNumber": "73", "name": "GNU General Public License v3.0 or later", "licenseId": "GPL-3.0+", "seeAlso": [ "https://www.gnu.org/licenses/gpl-3.0-standalone.html", "https://opensource.org/licenses/GPL-3.0" ], "isOsiApproved": true }, { "reference": "./GPL-3.0-only.html", "isDeprecatedLicenseId": false, "isFsfLibre": true, "detailsUrl": "http://spdx.org/licenses/GPL-3.0-only.json", "referenceNumber": "206", "name": "GNU General Public License v3.0 only", "licenseId": "GPL-3.0-only", "seeAlso": [ "https://www.gnu.org/licenses/gpl-3.0-standalone.html", "https://opensource.org/licenses/GPL-3.0" ], "isOsiApproved": true }, { "reference": "./GPL-3.0-or-later.html", "isDeprecatedLicenseId": false, "isFsfLibre": true, "detailsUrl": "http://spdx.org/licenses/GPL-3.0-or-later.json", "referenceNumber": "196", "name": "GNU General Public License v3.0 or later", "licenseId": "GPL-3.0-or-later", "seeAlso": [ "https://www.gnu.org/licenses/gpl-3.0-standalone.html", "https://opensource.org/licenses/GPL-3.0" ], "isOsiApproved": true }, { "reference": "./GPL-3.0-with-GCC-exception.html", "isDeprecatedLicenseId": true, "detailsUrl": "http://spdx.org/licenses/GPL-3.0-with-GCC-exception.json", "referenceNumber": "221", "name": "GNU General Public License v3.0 w/GCC Runtime Library exception", "licenseId": "GPL-3.0-with-GCC-exception", "seeAlso": [ "https://www.gnu.org/licenses/gcc-exception-3.1.html" ], "isOsiApproved": true }, { "reference": "./GPL-3.0-with-autoconf-exception.html", "isDeprecatedLicenseId": true, "detailsUrl": "http://spdx.org/licenses/GPL-3.0-with-autoconf-exception.json", "referenceNumber": "235", "name": "GNU General Public License v3.0 w/Autoconf exception", "licenseId": "GPL-3.0-with-autoconf-exception", "seeAlso": [ "https://www.gnu.org/licenses/autoconf-exception-3.0.html" ], "isOsiApproved": false }, { "reference": "./Giftware.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/Giftware.json", "referenceNumber": "369", "name": "Giftware License", "licenseId": "Giftware", "seeAlso": [ "http://liballeg.org/license.html#allegro-4-the-giftware-license" ], "isOsiApproved": false }, { "reference": "./Glide.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/Glide.json", "referenceNumber": "374", "name": "3dfx Glide License", "licenseId": "Glide", "seeAlso": [ "http://www.users.on.net/~triforce/glidexp/COPYING.txt" ], "isOsiApproved": false }, { "reference": "./Glulxe.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/Glulxe.json", "referenceNumber": "93", "name": "Glulxe License", "licenseId": "Glulxe", "seeAlso": [ "https://fedoraproject.org/wiki/Licensing/Glulxe" ], "isOsiApproved": false }, { "reference": "./HPND.html", "isDeprecatedLicenseId": false, "isFsfLibre": true, "detailsUrl": "http://spdx.org/licenses/HPND.json", "referenceNumber": "264", "name": "Historical Permission Notice and Disclaimer", "licenseId": "HPND", "seeAlso": [ "https://opensource.org/licenses/HPND" ], "isOsiApproved": true }, { "reference": "./HPND-sell-variant.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/HPND-sell-variant.json", "referenceNumber": "145", "name": "Historical Permission Notice and Disclaimer - sell variant", "licenseId": "HPND-sell-variant", "seeAlso": [ "https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/net/sunrpc/auth_gss/gss_generic_token.c?h\u003dv4.19" ], "isOsiApproved": false }, { "reference": "./HaskellReport.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/HaskellReport.json", "referenceNumber": "122", "name": "Haskell Language Report License", "licenseId": "HaskellReport", "seeAlso": [ "https://fedoraproject.org/wiki/Licensing/Haskell_Language_Report_License" ], "isOsiApproved": false }, { "reference": "./IBM-pibs.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/IBM-pibs.json", "referenceNumber": "207", "name": "IBM PowerPC Initialization and Boot Software", "licenseId": "IBM-pibs", "seeAlso": [ "http://git.denx.de/?p\u003du-boot.git;a\u003dblob;f\u003darch/powerpc/cpu/ppc4xx/miiphy.c;h\u003d297155fdafa064b955e53e9832de93bfb0cfb85b;hb\u003d9fab4bf4cc077c21e43941866f3f2c196f28670d" ], "isOsiApproved": false }, { "reference": "./ICU.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/ICU.json", "referenceNumber": "194", "name": "ICU License", "licenseId": "ICU", "seeAlso": [ "http://source.icu-project.org/repos/icu/icu/trunk/license.html" ], "isOsiApproved": false }, { "reference": "./IJG.html", "isDeprecatedLicenseId": false, "isFsfLibre": true, "detailsUrl": "http://spdx.org/licenses/IJG.json", "referenceNumber": "55", "name": "Independent JPEG Group License", "licenseId": "IJG", "seeAlso": [ "http://dev.w3.org/cvsweb/Amaya/libjpeg/Attic/README?rev\u003d1.2" ], "isOsiApproved": false }, { "reference": "./IPA.html", "isDeprecatedLicenseId": false, "isFsfLibre": true, "detailsUrl": "http://spdx.org/licenses/IPA.json", "referenceNumber": "312", "name": "IPA Font License", "licenseId": "IPA", "seeAlso": [ "https://opensource.org/licenses/IPA" ], "isOsiApproved": true }, { "reference": "./IPL-1.0.html", "isDeprecatedLicenseId": false, "isFsfLibre": true, "detailsUrl": "http://spdx.org/licenses/IPL-1.0.json", "referenceNumber": "31", "name": "IBM Public License v1.0", "licenseId": "IPL-1.0", "seeAlso": [ "https://opensource.org/licenses/IPL-1.0" ], "isOsiApproved": true }, { "reference": "./ISC.html", "isDeprecatedLicenseId": false, "isFsfLibre": true, "detailsUrl": "http://spdx.org/licenses/ISC.json", "referenceNumber": "110", "name": "ISC License", "licenseId": "ISC", "seeAlso": [ "https://www.isc.org/downloads/software-support-policy/isc-license/", "https://opensource.org/licenses/ISC" ], "isOsiApproved": true }, { "reference": "./ImageMagick.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/ImageMagick.json", "referenceNumber": "231", "name": "ImageMagick License", "licenseId": "ImageMagick", "seeAlso": [ "http://www.imagemagick.org/script/license.php" ], "isOsiApproved": false }, { "reference": "./Imlib2.html", "isDeprecatedLicenseId": false, "isFsfLibre": true, "detailsUrl": "http://spdx.org/licenses/Imlib2.json", "referenceNumber": "257", "name": "Imlib2 License", "licenseId": "Imlib2", "seeAlso": [ "http://trac.enlightenment.org/e/browser/trunk/imlib2/COPYING", "https://git.enlightenment.org/legacy/imlib2.git/tree/COPYING" ], "isOsiApproved": false }, { "reference": "./Info-ZIP.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/Info-ZIP.json", "referenceNumber": "104", "name": "Info-ZIP License", "licenseId": "Info-ZIP", "seeAlso": [ "http://www.info-zip.org/license.html" ], "isOsiApproved": false }, { "reference": "./Intel.html", "isDeprecatedLicenseId": false, "isFsfLibre": true, "detailsUrl": "http://spdx.org/licenses/Intel.json", "referenceNumber": "167", "name": "Intel Open Source License", "licenseId": "Intel", "seeAlso": [ "https://opensource.org/licenses/Intel" ], "isOsiApproved": true }, { "reference": "./Intel-ACPI.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/Intel-ACPI.json", "referenceNumber": "88", "name": "Intel ACPI Software License Agreement", "licenseId": "Intel-ACPI", "seeAlso": [ "https://fedoraproject.org/wiki/Licensing/Intel_ACPI_Software_License_Agreement" ], "isOsiApproved": false }, { "reference": "./Interbase-1.0.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/Interbase-1.0.json", "referenceNumber": "83", "name": "Interbase Public License v1.0", "licenseId": "Interbase-1.0", "seeAlso": [ "https://web.archive.org/web/20060319014854/http://info.borland.com/devsupport/interbase/opensource/IPL.html" ], "isOsiApproved": false }, { "reference": "./JPNIC.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/JPNIC.json", "referenceNumber": "105", "name": "Japan Network Information Center License", "licenseId": "JPNIC", "seeAlso": [ "https://gitlab.isc.org/isc-projects/bind9/blob/master/COPYRIGHT#L366" ], "isOsiApproved": false }, { "reference": "./JSON.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/JSON.json", "referenceNumber": "372", "name": "JSON License", "licenseId": "JSON", "seeAlso": [ "http://www.json.org/license.html" ], "isOsiApproved": false }, { "reference": "./JasPer-2.0.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/JasPer-2.0.json", "referenceNumber": "239", "name": "JasPer License", "licenseId": "JasPer-2.0", "seeAlso": [ "http://www.ece.uvic.ca/~mdadams/jasper/LICENSE" ], "isOsiApproved": false }, { "reference": "./LAL-1.2.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/LAL-1.2.json", "referenceNumber": "380", "name": "Licence Art Libre 1.2", "licenseId": "LAL-1.2", "seeAlso": [ "http://artlibre.org/licence/lal/licence-art-libre-12/" ], "isOsiApproved": false }, { "reference": "./LAL-1.3.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/LAL-1.3.json", "referenceNumber": "156", "name": "Licence Art Libre 1.3", "licenseId": "LAL-1.3", "seeAlso": [ "http://artlibre.org/" ], "isOsiApproved": false }, { "reference": "./LGPL-2.0.html", "isDeprecatedLicenseId": true, "detailsUrl": "http://spdx.org/licenses/LGPL-2.0.json", "referenceNumber": "268", "name": "GNU Library General Public License v2 only", "licenseId": "LGPL-2.0", "seeAlso": [ "https://www.gnu.org/licenses/old-licenses/lgpl-2.0-standalone.html" ], "isOsiApproved": true }, { "reference": "./LGPL-2.0+.html", "isDeprecatedLicenseId": true, "detailsUrl": "http://spdx.org/licenses/LGPL-2.0+.json", "referenceNumber": "52", "name": "GNU Library General Public License v2 or later", "licenseId": "LGPL-2.0+", "seeAlso": [ "https://www.gnu.org/licenses/old-licenses/lgpl-2.0-standalone.html" ], "isOsiApproved": true }, { "reference": "./LGPL-2.0-only.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/LGPL-2.0-only.json", "referenceNumber": "276", "name": "GNU Library General Public License v2 only", "licenseId": "LGPL-2.0-only", "seeAlso": [ "https://www.gnu.org/licenses/old-licenses/lgpl-2.0-standalone.html" ], "isOsiApproved": true }, { "reference": "./LGPL-2.0-or-later.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/LGPL-2.0-or-later.json", "referenceNumber": "217", "name": "GNU Library General Public License v2 or later", "licenseId": "LGPL-2.0-or-later", "seeAlso": [ "https://www.gnu.org/licenses/old-licenses/lgpl-2.0-standalone.html" ], "isOsiApproved": true }, { "reference": "./LGPL-2.1.html", "isDeprecatedLicenseId": true, "isFsfLibre": true, "detailsUrl": "http://spdx.org/licenses/LGPL-2.1.json", "referenceNumber": "166", "name": "GNU Lesser General Public License v2.1 only", "licenseId": "LGPL-2.1", "seeAlso": [ "https://www.gnu.org/licenses/old-licenses/lgpl-2.1-standalone.html", "https://opensource.org/licenses/LGPL-2.1" ], "isOsiApproved": true }, { "reference": "./LGPL-2.1+.html", "isDeprecatedLicenseId": true, "isFsfLibre": true, "detailsUrl": "http://spdx.org/licenses/LGPL-2.1+.json", "referenceNumber": "64", "name": "GNU Library General Public License v2.1 or later", "licenseId": "LGPL-2.1+", "seeAlso": [ "https://www.gnu.org/licenses/old-licenses/lgpl-2.1-standalone.html", "https://opensource.org/licenses/LGPL-2.1" ], "isOsiApproved": true }, { "reference": "./LGPL-2.1-only.html", "isDeprecatedLicenseId": false, "isFsfLibre": true, "detailsUrl": "http://spdx.org/licenses/LGPL-2.1-only.json", "referenceNumber": "2", "name": "GNU Lesser General Public License v2.1 only", "licenseId": "LGPL-2.1-only", "seeAlso": [ "https://www.gnu.org/licenses/old-licenses/lgpl-2.1-standalone.html", "https://opensource.org/licenses/LGPL-2.1" ], "isOsiApproved": true }, { "reference": "./LGPL-2.1-or-later.html", "isDeprecatedLicenseId": false, "isFsfLibre": true, "detailsUrl": "http://spdx.org/licenses/LGPL-2.1-or-later.json", "referenceNumber": "338", "name": "GNU Lesser General Public License v2.1 or later", "licenseId": "LGPL-2.1-or-later", "seeAlso": [ "https://www.gnu.org/licenses/old-licenses/lgpl-2.1-standalone.html", "https://opensource.org/licenses/LGPL-2.1" ], "isOsiApproved": true }, { "reference": "./LGPL-3.0.html", "isDeprecatedLicenseId": true, "isFsfLibre": true, "detailsUrl": "http://spdx.org/licenses/LGPL-3.0.json", "referenceNumber": "210", "name": "GNU Lesser General Public License v3.0 only", "licenseId": "LGPL-3.0", "seeAlso": [ "https://www.gnu.org/licenses/lgpl-3.0-standalone.html", "https://opensource.org/licenses/LGPL-3.0" ], "isOsiApproved": true }, { "reference": "./LGPL-3.0+.html", "isDeprecatedLicenseId": true, "isFsfLibre": true, "detailsUrl": "http://spdx.org/licenses/LGPL-3.0+.json", "referenceNumber": "152", "name": "GNU Lesser General Public License v3.0 or later", "licenseId": "LGPL-3.0+", "seeAlso": [ "https://www.gnu.org/licenses/lgpl-3.0-standalone.html", "https://opensource.org/licenses/LGPL-3.0" ], "isOsiApproved": true }, { "reference": "./LGPL-3.0-only.html", "isDeprecatedLicenseId": false, "isFsfLibre": true, "detailsUrl": "http://spdx.org/licenses/LGPL-3.0-only.json", "referenceNumber": "254", "name": "GNU Lesser General Public License v3.0 only", "licenseId": "LGPL-3.0-only", "seeAlso": [ "https://www.gnu.org/licenses/lgpl-3.0-standalone.html", "https://opensource.org/licenses/LGPL-3.0" ], "isOsiApproved": true }, { "reference": "./LGPL-3.0-or-later.html", "isDeprecatedLicenseId": false, "isFsfLibre": true, "detailsUrl": "http://spdx.org/licenses/LGPL-3.0-or-later.json", "referenceNumber": "301", "name": "GNU Lesser General Public License v3.0 or later", "licenseId": "LGPL-3.0-or-later", "seeAlso": [ "https://www.gnu.org/licenses/lgpl-3.0-standalone.html", "https://opensource.org/licenses/LGPL-3.0" ], "isOsiApproved": true }, { "reference": "./LGPLLR.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/LGPLLR.json", "referenceNumber": "103", "name": "Lesser General Public License For Linguistic Resources", "licenseId": "LGPLLR", "seeAlso": [ "http://www-igm.univ-mlv.fr/~unitex/lgpllr.html" ], "isOsiApproved": false }, { "reference": "./LPL-1.0.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/LPL-1.0.json", "referenceNumber": "89", "name": "Lucent Public License Version 1.0", "licenseId": "LPL-1.0", "seeAlso": [ "https://opensource.org/licenses/LPL-1.0" ], "isOsiApproved": true }, { "reference": "./LPL-1.02.html", "isDeprecatedLicenseId": false, "isFsfLibre": true, "detailsUrl": "http://spdx.org/licenses/LPL-1.02.json", "referenceNumber": "131", "name": "Lucent Public License v1.02", "licenseId": "LPL-1.02", "seeAlso": [ "http://plan9.bell-labs.com/plan9/license.html", "https://opensource.org/licenses/LPL-1.02" ], "isOsiApproved": true }, { "reference": "./LPPL-1.0.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/LPPL-1.0.json", "referenceNumber": "259", "name": "LaTeX Project Public License v1.0", "licenseId": "LPPL-1.0", "seeAlso": [ "http://www.latex-project.org/lppl/lppl-1-0.txt" ], "isOsiApproved": false }, { "reference": "./LPPL-1.1.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/LPPL-1.1.json", "referenceNumber": "309", "name": "LaTeX Project Public License v1.1", "licenseId": "LPPL-1.1", "seeAlso": [ "http://www.latex-project.org/lppl/lppl-1-1.txt" ], "isOsiApproved": false }, { "reference": "./LPPL-1.2.html", "isDeprecatedLicenseId": false, "isFsfLibre": true, "detailsUrl": "http://spdx.org/licenses/LPPL-1.2.json", "referenceNumber": "392", "name": "LaTeX Project Public License v1.2", "licenseId": "LPPL-1.2", "seeAlso": [ "http://www.latex-project.org/lppl/lppl-1-2.txt" ], "isOsiApproved": false }, { "reference": "./LPPL-1.3a.html", "isDeprecatedLicenseId": false, "isFsfLibre": true, "detailsUrl": "http://spdx.org/licenses/LPPL-1.3a.json", "referenceNumber": "305", "name": "LaTeX Project Public License v1.3a", "licenseId": "LPPL-1.3a", "seeAlso": [ "http://www.latex-project.org/lppl/lppl-1-3a.txt" ], "isOsiApproved": false }, { "reference": "./LPPL-1.3c.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/LPPL-1.3c.json", "referenceNumber": "326", "name": "LaTeX Project Public License v1.3c", "licenseId": "LPPL-1.3c", "seeAlso": [ "http://www.latex-project.org/lppl/lppl-1-3c.txt", "https://opensource.org/licenses/LPPL-1.3c" ], "isOsiApproved": true }, { "reference": "./Latex2e.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/Latex2e.json", "referenceNumber": "283", "name": "Latex2e License", "licenseId": "Latex2e", "seeAlso": [ "https://fedoraproject.org/wiki/Licensing/Latex2e" ], "isOsiApproved": false }, { "reference": "./Leptonica.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/Leptonica.json", "referenceNumber": "159", "name": "Leptonica License", "licenseId": "Leptonica", "seeAlso": [ "https://fedoraproject.org/wiki/Licensing/Leptonica" ], "isOsiApproved": false }, { "reference": "./LiLiQ-P-1.1.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/LiLiQ-P-1.1.json", "referenceNumber": "379", "name": "Licence Libre du Québec – Permissive version 1.1", "licenseId": "LiLiQ-P-1.1", "seeAlso": [ "https://forge.gouv.qc.ca/licence/fr/liliq-v1-1/", "http://opensource.org/licenses/LiLiQ-P-1.1" ], "isOsiApproved": true }, { "reference": "./LiLiQ-R-1.1.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/LiLiQ-R-1.1.json", "referenceNumber": "286", "name": "Licence Libre du Québec – Réciprocité version 1.1", "licenseId": "LiLiQ-R-1.1", "seeAlso": [ "https://www.forge.gouv.qc.ca/participez/licence-logicielle/licence-libre-du-quebec-liliq-en-francais/licence-libre-du-quebec-reciprocite-liliq-r-v1-1/", "http://opensource.org/licenses/LiLiQ-R-1.1" ], "isOsiApproved": true }, { "reference": "./LiLiQ-Rplus-1.1.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/LiLiQ-Rplus-1.1.json", "referenceNumber": "139", "name": "Licence Libre du Québec – Réciprocité forte version 1.1", "licenseId": "LiLiQ-Rplus-1.1", "seeAlso": [ "https://www.forge.gouv.qc.ca/participez/licence-logicielle/licence-libre-du-quebec-liliq-en-francais/licence-libre-du-quebec-reciprocite-forte-liliq-r-v1-1/", "http://opensource.org/licenses/LiLiQ-Rplus-1.1" ], "isOsiApproved": true }, { "reference": "./Libpng.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/Libpng.json", "referenceNumber": "101", "name": "libpng License", "licenseId": "Libpng", "seeAlso": [ "http://www.libpng.org/pub/png/src/libpng-LICENSE.txt" ], "isOsiApproved": false }, { "reference": "./Linux-OpenIB.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/Linux-OpenIB.json", "referenceNumber": "5", "name": "Linux Kernel Variant of OpenIB.org license", "licenseId": "Linux-OpenIB", "seeAlso": [ "https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/drivers/infiniband/core/sa.h" ], "isOsiApproved": false }, { "reference": "./MIT.html", "isDeprecatedLicenseId": false, "isFsfLibre": true, "detailsUrl": "http://spdx.org/licenses/MIT.json", "referenceNumber": "201", "name": "MIT License", "licenseId": "MIT", "seeAlso": [ "https://opensource.org/licenses/MIT" ], "isOsiApproved": true }, { "reference": "./MIT-0.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/MIT-0.json", "referenceNumber": "6", "name": "MIT No Attribution", "licenseId": "MIT-0", "seeAlso": [ "https://github.com/aws/mit-0", "https://romanrm.net/mit-zero", "https://github.com/awsdocs/aws-cloud9-user-guide/blob/master/LICENSE-SAMPLECODE" ], "isOsiApproved": true }, { "reference": "./MIT-CMU.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/MIT-CMU.json", "referenceNumber": "9", "name": "CMU License", "licenseId": "MIT-CMU", "seeAlso": [ "https://fedoraproject.org/wiki/Licensing:MIT?rd\u003dLicensing/MIT#CMU_Style", "https://github.com/python-pillow/Pillow/blob/fffb426092c8db24a5f4b6df243a8a3c01fb63cd/LICENSE" ], "isOsiApproved": false }, { "reference": "./MIT-advertising.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/MIT-advertising.json", "referenceNumber": "8", "name": "Enlightenment License (e16)", "licenseId": "MIT-advertising", "seeAlso": [ "https://fedoraproject.org/wiki/Licensing/MIT_With_Advertising" ], "isOsiApproved": false }, { "reference": "./MIT-enna.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/MIT-enna.json", "referenceNumber": "25", "name": "enna License", "licenseId": "MIT-enna", "seeAlso": [ "https://fedoraproject.org/wiki/Licensing/MIT#enna" ], "isOsiApproved": false }, { "reference": "./MIT-feh.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/MIT-feh.json", "referenceNumber": "38", "name": "feh License", "licenseId": "MIT-feh", "seeAlso": [ "https://fedoraproject.org/wiki/Licensing/MIT#feh" ], "isOsiApproved": false }, { "reference": "./MITNFA.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/MITNFA.json", "referenceNumber": "294", "name": "MIT +no-false-attribs license", "licenseId": "MITNFA", "seeAlso": [ "https://fedoraproject.org/wiki/Licensing/MITNFA" ], "isOsiApproved": false }, { "reference": "./MPL-1.0.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/MPL-1.0.json", "referenceNumber": "49", "name": "Mozilla Public License 1.0", "licenseId": "MPL-1.0", "seeAlso": [ "http://www.mozilla.org/MPL/MPL-1.0.html", "https://opensource.org/licenses/MPL-1.0" ], "isOsiApproved": true }, { "reference": "./MPL-1.1.html", "isDeprecatedLicenseId": false, "isFsfLibre": true, "detailsUrl": "http://spdx.org/licenses/MPL-1.1.json", "referenceNumber": "304", "name": "Mozilla Public License 1.1", "licenseId": "MPL-1.1", "seeAlso": [ "http://www.mozilla.org/MPL/MPL-1.1.html", "https://opensource.org/licenses/MPL-1.1" ], "isOsiApproved": true }, { "reference": "./MPL-2.0.html", "isDeprecatedLicenseId": false, "isFsfLibre": true, "detailsUrl": "http://spdx.org/licenses/MPL-2.0.json", "referenceNumber": "234", "name": "Mozilla Public License 2.0", "licenseId": "MPL-2.0", "seeAlso": [ "http://www.mozilla.org/MPL/2.0/", "https://opensource.org/licenses/MPL-2.0" ], "isOsiApproved": true }, { "reference": "./MPL-2.0-no-copyleft-exception.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/MPL-2.0-no-copyleft-exception.json", "referenceNumber": "303", "name": "Mozilla Public License 2.0 (no copyleft exception)", "licenseId": "MPL-2.0-no-copyleft-exception", "seeAlso": [ "http://www.mozilla.org/MPL/2.0/", "https://opensource.org/licenses/MPL-2.0" ], "isOsiApproved": true }, { "reference": "./MS-PL.html", "isDeprecatedLicenseId": false, "isFsfLibre": true, "detailsUrl": "http://spdx.org/licenses/MS-PL.json", "referenceNumber": "336", "name": "Microsoft Public License", "licenseId": "MS-PL", "seeAlso": [ "http://www.microsoft.com/opensource/licenses.mspx", "https://opensource.org/licenses/MS-PL" ], "isOsiApproved": true }, { "reference": "./MS-RL.html", "isDeprecatedLicenseId": false, "isFsfLibre": true, "detailsUrl": "http://spdx.org/licenses/MS-RL.json", "referenceNumber": "280", "name": "Microsoft Reciprocal License", "licenseId": "MS-RL", "seeAlso": [ "http://www.microsoft.com/opensource/licenses.mspx", "https://opensource.org/licenses/MS-RL" ], "isOsiApproved": true }, { "reference": "./MTLL.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/MTLL.json", "referenceNumber": "181", "name": "Matrix Template Library License", "licenseId": "MTLL", "seeAlso": [ "https://fedoraproject.org/wiki/Licensing/Matrix_Template_Library_License" ], "isOsiApproved": false }, { "reference": "./MakeIndex.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/MakeIndex.json", "referenceNumber": "187", "name": "MakeIndex License", "licenseId": "MakeIndex", "seeAlso": [ "https://fedoraproject.org/wiki/Licensing/MakeIndex" ], "isOsiApproved": false }, { "reference": "./MirOS.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/MirOS.json", "referenceNumber": "299", "name": "MirOS License", "licenseId": "MirOS", "seeAlso": [ "https://opensource.org/licenses/MirOS" ], "isOsiApproved": true }, { "reference": "./Motosoto.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/Motosoto.json", "referenceNumber": "317", "name": "Motosoto License", "licenseId": "Motosoto", "seeAlso": [ "https://opensource.org/licenses/Motosoto" ], "isOsiApproved": true }, { "reference": "./Multics.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/Multics.json", "referenceNumber": "63", "name": "Multics License", "licenseId": "Multics", "seeAlso": [ "https://opensource.org/licenses/Multics" ], "isOsiApproved": true }, { "reference": "./Mup.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/Mup.json", "referenceNumber": "353", "name": "Mup License", "licenseId": "Mup", "seeAlso": [ "https://fedoraproject.org/wiki/Licensing/Mup" ], "isOsiApproved": false }, { "reference": "./NASA-1.3.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/NASA-1.3.json", "referenceNumber": "87", "name": "NASA Open Source Agreement 1.3", "licenseId": "NASA-1.3", "seeAlso": [ "http://ti.arc.nasa.gov/opensource/nosa/", "https://opensource.org/licenses/NASA-1.3" ], "isOsiApproved": true }, { "reference": "./NBPL-1.0.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/NBPL-1.0.json", "referenceNumber": "361", "name": "Net Boolean Public License v1", "licenseId": "NBPL-1.0", "seeAlso": [ "http://www.openldap.org/devel/gitweb.cgi?p\u003dopenldap.git;a\u003dblob;f\u003dLICENSE;hb\u003d37b4b3f6cc4bf34e1d3dec61e69914b9819d8894" ], "isOsiApproved": false }, { "reference": "./NCSA.html", "isDeprecatedLicenseId": false, "isFsfLibre": true, "detailsUrl": "http://spdx.org/licenses/NCSA.json", "referenceNumber": "58", "name": "University of Illinois/NCSA Open Source License", "licenseId": "NCSA", "seeAlso": [ "http://otm.illinois.edu/uiuc_openSource", "https://opensource.org/licenses/NCSA" ], "isOsiApproved": true }, { "reference": "./NGPL.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/NGPL.json", "referenceNumber": "71", "name": "Nethack General Public License", "licenseId": "NGPL", "seeAlso": [ "https://opensource.org/licenses/NGPL" ], "isOsiApproved": true }, { "reference": "./NLOD-1.0.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/NLOD-1.0.json", "referenceNumber": "209", "name": "Norwegian Licence for Open Government Data", "licenseId": "NLOD-1.0", "seeAlso": [ "http://data.norge.no/nlod/en/1.0" ], "isOsiApproved": false }, { "reference": "./NLPL.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/NLPL.json", "referenceNumber": "344", "name": "No Limit Public License", "licenseId": "NLPL", "seeAlso": [ "https://fedoraproject.org/wiki/Licensing/NLPL" ], "isOsiApproved": false }, { "reference": "./NOSL.html", "isDeprecatedLicenseId": false, "isFsfLibre": true, "detailsUrl": "http://spdx.org/licenses/NOSL.json", "referenceNumber": "383", "name": "Netizen Open Source License", "licenseId": "NOSL", "seeAlso": [ "http://bits.netizen.com.au/licenses/NOSL/nosl.txt" ], "isOsiApproved": false }, { "reference": "./NPL-1.0.html", "isDeprecatedLicenseId": false, "isFsfLibre": true, "detailsUrl": "http://spdx.org/licenses/NPL-1.0.json", "referenceNumber": "328", "name": "Netscape Public License v1.0", "licenseId": "NPL-1.0", "seeAlso": [ "http://www.mozilla.org/MPL/NPL/1.0/" ], "isOsiApproved": false }, { "reference": "./NPL-1.1.html", "isDeprecatedLicenseId": false, "isFsfLibre": true, "detailsUrl": "http://spdx.org/licenses/NPL-1.1.json", "referenceNumber": "185", "name": "Netscape Public License v1.1", "licenseId": "NPL-1.1", "seeAlso": [ "http://www.mozilla.org/MPL/NPL/1.1/" ], "isOsiApproved": false }, { "reference": "./NPOSL-3.0.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/NPOSL-3.0.json", "referenceNumber": "222", "name": "Non-Profit Open Software License 3.0", "licenseId": "NPOSL-3.0", "seeAlso": [ "https://opensource.org/licenses/NOSL3.0" ], "isOsiApproved": true }, { "reference": "./NRL.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/NRL.json", "referenceNumber": "53", "name": "NRL License", "licenseId": "NRL", "seeAlso": [ "http://web.mit.edu/network/isakmp/nrllicense.html" ], "isOsiApproved": false }, { "reference": "./NTP.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/NTP.json", "referenceNumber": "261", "name": "NTP License", "licenseId": "NTP", "seeAlso": [ "https://opensource.org/licenses/NTP" ], "isOsiApproved": true }, { "reference": "./Naumen.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/Naumen.json", "referenceNumber": "278", "name": "Naumen Public License", "licenseId": "Naumen", "seeAlso": [ "https://opensource.org/licenses/Naumen" ], "isOsiApproved": true }, { "reference": "./Net-SNMP.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/Net-SNMP.json", "referenceNumber": "284", "name": "Net-SNMP License", "licenseId": "Net-SNMP", "seeAlso": [ "http://net-snmp.sourceforge.net/about/license.html" ], "isOsiApproved": false }, { "reference": "./NetCDF.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/NetCDF.json", "referenceNumber": "46", "name": "NetCDF license", "licenseId": "NetCDF", "seeAlso": [ "http://www.unidata.ucar.edu/software/netcdf/copyright.html" ], "isOsiApproved": false }, { "reference": "./Newsletr.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/Newsletr.json", "referenceNumber": "279", "name": "Newsletr License", "licenseId": "Newsletr", "seeAlso": [ "https://fedoraproject.org/wiki/Licensing/Newsletr" ], "isOsiApproved": false }, { "reference": "./Nokia.html", "isDeprecatedLicenseId": false, "isFsfLibre": true, "detailsUrl": "http://spdx.org/licenses/Nokia.json", "referenceNumber": "327", "name": "Nokia Open Source License", "licenseId": "Nokia", "seeAlso": [ "https://opensource.org/licenses/nokia" ], "isOsiApproved": true }, { "reference": "./Noweb.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/Noweb.json", "referenceNumber": "364", "name": "Noweb License", "licenseId": "Noweb", "seeAlso": [ "https://fedoraproject.org/wiki/Licensing/Noweb" ], "isOsiApproved": false }, { "reference": "./Nunit.html", "isDeprecatedLicenseId": true, "isFsfLibre": true, "detailsUrl": "http://spdx.org/licenses/Nunit.json", "referenceNumber": "288", "name": "Nunit License", "licenseId": "Nunit", "seeAlso": [ "https://fedoraproject.org/wiki/Licensing/Nunit" ], "isOsiApproved": false }, { "reference": "./OCCT-PL.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/OCCT-PL.json", "referenceNumber": "282", "name": "Open CASCADE Technology Public License", "licenseId": "OCCT-PL", "seeAlso": [ "http://www.opencascade.com/content/occt-public-license" ], "isOsiApproved": false }, { "reference": "./OCLC-2.0.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/OCLC-2.0.json", "referenceNumber": "111", "name": "OCLC Research Public License 2.0", "licenseId": "OCLC-2.0", "seeAlso": [ "http://www.oclc.org/research/activities/software/license/v2final.htm", "https://opensource.org/licenses/OCLC-2.0" ], "isOsiApproved": true }, { "reference": "./ODC-By-1.0.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/ODC-By-1.0.json", "referenceNumber": "144", "name": "Open Data Commons Attribution License v1.0", "licenseId": "ODC-By-1.0", "seeAlso": [ "https://opendatacommons.org/licenses/by/1.0/" ], "isOsiApproved": false }, { "reference": "./ODbL-1.0.html", "isDeprecatedLicenseId": false, "isFsfLibre": true, "detailsUrl": "http://spdx.org/licenses/ODbL-1.0.json", "referenceNumber": "246", "name": "ODC Open Database License v1.0", "licenseId": "ODbL-1.0", "seeAlso": [ "http://www.opendatacommons.org/licenses/odbl/1.0/" ], "isOsiApproved": false }, { "reference": "./OFL-1.0.html", "isDeprecatedLicenseId": false, "isFsfLibre": true, "detailsUrl": "http://spdx.org/licenses/OFL-1.0.json", "referenceNumber": "153", "name": "SIL Open Font License 1.0", "licenseId": "OFL-1.0", "seeAlso": [ "http://scripts.sil.org/cms/scripts/page.php?item_id\u003dOFL10_web" ], "isOsiApproved": false }, { "reference": "./OFL-1.1.html", "isDeprecatedLicenseId": false, "isFsfLibre": true, "detailsUrl": "http://spdx.org/licenses/OFL-1.1.json", "referenceNumber": "315", "name": "SIL Open Font License 1.1", "licenseId": "OFL-1.1", "seeAlso": [ "http://scripts.sil.org/cms/scripts/page.php?item_id\u003dOFL_web", "https://opensource.org/licenses/OFL-1.1" ], "isOsiApproved": true }, { "reference": "./OGL-UK-1.0.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/OGL-UK-1.0.json", "referenceNumber": "116", "name": "Open Government Licence v1.0", "licenseId": "OGL-UK-1.0", "seeAlso": [ "http://www.nationalarchives.gov.uk/doc/open-government-licence/version/1/" ], "isOsiApproved": false }, { "reference": "./OGL-UK-2.0.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/OGL-UK-2.0.json", "referenceNumber": "289", "name": "Open Government Licence v2.0", "licenseId": "OGL-UK-2.0", "seeAlso": [ "http://www.nationalarchives.gov.uk/doc/open-government-licence/version/2/" ], "isOsiApproved": false }, { "reference": "./OGL-UK-3.0.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/OGL-UK-3.0.json", "referenceNumber": "226", "name": "Open Government Licence v3.0", "licenseId": "OGL-UK-3.0", "seeAlso": [ "http://www.nationalarchives.gov.uk/doc/open-government-licence/version/3/" ], "isOsiApproved": false }, { "reference": "./OGTSL.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/OGTSL.json", "referenceNumber": "125", "name": "Open Group Test Suite License", "licenseId": "OGTSL", "seeAlso": [ "http://www.opengroup.org/testing/downloads/The_Open_Group_TSL.txt", "https://opensource.org/licenses/OGTSL" ], "isOsiApproved": true }, { "reference": "./OLDAP-1.1.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/OLDAP-1.1.json", "referenceNumber": "97", "name": "Open LDAP Public License v1.1", "licenseId": "OLDAP-1.1", "seeAlso": [ "http://www.openldap.org/devel/gitweb.cgi?p\u003dopenldap.git;a\u003dblob;f\u003dLICENSE;hb\u003d806557a5ad59804ef3a44d5abfbe91d706b0791f" ], "isOsiApproved": false }, { "reference": "./OLDAP-1.2.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/OLDAP-1.2.json", "referenceNumber": "190", "name": "Open LDAP Public License v1.2", "licenseId": "OLDAP-1.2", "seeAlso": [ "http://www.openldap.org/devel/gitweb.cgi?p\u003dopenldap.git;a\u003dblob;f\u003dLICENSE;hb\u003d42b0383c50c299977b5893ee695cf4e486fb0dc7" ], "isOsiApproved": false }, { "reference": "./OLDAP-1.3.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/OLDAP-1.3.json", "referenceNumber": "106", "name": "Open LDAP Public License v1.3", "licenseId": "OLDAP-1.3", "seeAlso": [ "http://www.openldap.org/devel/gitweb.cgi?p\u003dopenldap.git;a\u003dblob;f\u003dLICENSE;hb\u003de5f8117f0ce088d0bd7a8e18ddf37eaa40eb09b1" ], "isOsiApproved": false }, { "reference": "./OLDAP-1.4.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/OLDAP-1.4.json", "referenceNumber": "30", "name": "Open LDAP Public License v1.4", "licenseId": "OLDAP-1.4", "seeAlso": [ "http://www.openldap.org/devel/gitweb.cgi?p\u003dopenldap.git;a\u003dblob;f\u003dLICENSE;hb\u003dc9f95c2f3f2ffb5e0ae55fe7388af75547660941" ], "isOsiApproved": false }, { "reference": "./OLDAP-2.0.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/OLDAP-2.0.json", "referenceNumber": "266", "name": "Open LDAP Public License v2.0 (or possibly 2.0A and 2.0B)", "licenseId": "OLDAP-2.0", "seeAlso": [ "http://www.openldap.org/devel/gitweb.cgi?p\u003dopenldap.git;a\u003dblob;f\u003dLICENSE;hb\u003dcbf50f4e1185a21abd4c0a54d3f4341fe28f36ea" ], "isOsiApproved": false }, { "reference": "./OLDAP-2.0.1.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/OLDAP-2.0.1.json", "referenceNumber": "350", "name": "Open LDAP Public License v2.0.1", "licenseId": "OLDAP-2.0.1", "seeAlso": [ "http://www.openldap.org/devel/gitweb.cgi?p\u003dopenldap.git;a\u003dblob;f\u003dLICENSE;hb\u003db6d68acd14e51ca3aab4428bf26522aa74873f0e" ], "isOsiApproved": false }, { "reference": "./OLDAP-2.1.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/OLDAP-2.1.json", "referenceNumber": "154", "name": "Open LDAP Public License v2.1", "licenseId": "OLDAP-2.1", "seeAlso": [ "http://www.openldap.org/devel/gitweb.cgi?p\u003dopenldap.git;a\u003dblob;f\u003dLICENSE;hb\u003db0d176738e96a0d3b9f85cb51e140a86f21be715" ], "isOsiApproved": false }, { "reference": "./OLDAP-2.2.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/OLDAP-2.2.json", "referenceNumber": "362", "name": "Open LDAP Public License v2.2", "licenseId": "OLDAP-2.2", "seeAlso": [ "http://www.openldap.org/devel/gitweb.cgi?p\u003dopenldap.git;a\u003dblob;f\u003dLICENSE;hb\u003d470b0c18ec67621c85881b2733057fecf4a1acc3" ], "isOsiApproved": false }, { "reference": "./OLDAP-2.2.1.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/OLDAP-2.2.1.json", "referenceNumber": "339", "name": "Open LDAP Public License v2.2.1", "licenseId": "OLDAP-2.2.1", "seeAlso": [ "http://www.openldap.org/devel/gitweb.cgi?p\u003dopenldap.git;a\u003dblob;f\u003dLICENSE;hb\u003d4bc786f34b50aa301be6f5600f58a980070f481e" ], "isOsiApproved": false }, { "reference": "./OLDAP-2.2.2.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/OLDAP-2.2.2.json", "referenceNumber": "199", "name": "Open LDAP Public License 2.2.2", "licenseId": "OLDAP-2.2.2", "seeAlso": [ "http://www.openldap.org/devel/gitweb.cgi?p\u003dopenldap.git;a\u003dblob;f\u003dLICENSE;hb\u003ddf2cc1e21eb7c160695f5b7cffd6296c151ba188" ], "isOsiApproved": false }, { "reference": "./OLDAP-2.3.html", "isDeprecatedLicenseId": false, "isFsfLibre": true, "detailsUrl": "http://spdx.org/licenses/OLDAP-2.3.json", "referenceNumber": "164", "name": "Open LDAP Public License v2.3", "licenseId": "OLDAP-2.3", "seeAlso": [ "http://www.openldap.org/devel/gitweb.cgi?p\u003dopenldap.git;a\u003dblob;f\u003dLICENSE;hb\u003dd32cf54a32d581ab475d23c810b0a7fbaf8d63c3" ], "isOsiApproved": false }, { "reference": "./OLDAP-2.4.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/OLDAP-2.4.json", "referenceNumber": "66", "name": "Open LDAP Public License v2.4", "licenseId": "OLDAP-2.4", "seeAlso": [ "http://www.openldap.org/devel/gitweb.cgi?p\u003dopenldap.git;a\u003dblob;f\u003dLICENSE;hb\u003dcd1284c4a91a8a380d904eee68d1583f989ed386" ], "isOsiApproved": false }, { "reference": "./OLDAP-2.5.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/OLDAP-2.5.json", "referenceNumber": "183", "name": "Open LDAP Public License v2.5", "licenseId": "OLDAP-2.5", "seeAlso": [ "http://www.openldap.org/devel/gitweb.cgi?p\u003dopenldap.git;a\u003dblob;f\u003dLICENSE;hb\u003d6852b9d90022e8593c98205413380536b1b5a7cf" ], "isOsiApproved": false }, { "reference": "./OLDAP-2.6.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/OLDAP-2.6.json", "referenceNumber": "61", "name": "Open LDAP Public License v2.6", "licenseId": "OLDAP-2.6", "seeAlso": [ "http://www.openldap.org/devel/gitweb.cgi?p\u003dopenldap.git;a\u003dblob;f\u003dLICENSE;hb\u003d1cae062821881f41b73012ba816434897abf4205" ], "isOsiApproved": false }, { "reference": "./OLDAP-2.7.html", "isDeprecatedLicenseId": false, "isFsfLibre": true, "detailsUrl": "http://spdx.org/licenses/OLDAP-2.7.json", "referenceNumber": "123", "name": "Open LDAP Public License v2.7", "licenseId": "OLDAP-2.7", "seeAlso": [ "http://www.openldap.org/devel/gitweb.cgi?p\u003dopenldap.git;a\u003dblob;f\u003dLICENSE;hb\u003d47c2415c1df81556eeb39be6cad458ef87c534a2" ], "isOsiApproved": false }, { "reference": "./OLDAP-2.8.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/OLDAP-2.8.json", "referenceNumber": "37", "name": "Open LDAP Public License v2.8", "licenseId": "OLDAP-2.8", "seeAlso": [ "http://www.openldap.org/software/release/license.html" ], "isOsiApproved": false }, { "reference": "./OML.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/OML.json", "referenceNumber": "65", "name": "Open Market License", "licenseId": "OML", "seeAlso": [ "https://fedoraproject.org/wiki/Licensing/Open_Market_License" ], "isOsiApproved": false }, { "reference": "./OPL-1.0.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/OPL-1.0.json", "referenceNumber": "343", "name": "Open Public License v1.0", "licenseId": "OPL-1.0", "seeAlso": [ "http://old.koalateam.com/jackaroo/OPL_1_0.TXT", "https://fedoraproject.org/wiki/Licensing/Open_Public_License" ], "isOsiApproved": false }, { "reference": "./OSET-PL-2.1.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/OSET-PL-2.1.json", "referenceNumber": "291", "name": "OSET Public License version 2.1", "licenseId": "OSET-PL-2.1", "seeAlso": [ "http://www.osetfoundation.org/public-license", "https://opensource.org/licenses/OPL-2.1" ], "isOsiApproved": true }, { "reference": "./OSL-1.0.html", "isDeprecatedLicenseId": false, "isFsfLibre": true, "detailsUrl": "http://spdx.org/licenses/OSL-1.0.json", "referenceNumber": "85", "name": "Open Software License 1.0", "licenseId": "OSL-1.0", "seeAlso": [ "https://opensource.org/licenses/OSL-1.0" ], "isOsiApproved": true }, { "reference": "./OSL-1.1.html", "isDeprecatedLicenseId": false, "isFsfLibre": true, "detailsUrl": "http://spdx.org/licenses/OSL-1.1.json", "referenceNumber": "334", "name": "Open Software License 1.1", "licenseId": "OSL-1.1", "seeAlso": [ "https://fedoraproject.org/wiki/Licensing/OSL1.1" ], "isOsiApproved": false }, { "reference": "./OSL-2.0.html", "isDeprecatedLicenseId": false, "isFsfLibre": true, "detailsUrl": "http://spdx.org/licenses/OSL-2.0.json", "referenceNumber": "20", "name": "Open Software License 2.0", "licenseId": "OSL-2.0", "seeAlso": [ "http://web.archive.org/web/20041020171434/http://www.rosenlaw.com/osl2.0.html" ], "isOsiApproved": true }, { "reference": "./OSL-2.1.html", "isDeprecatedLicenseId": false, "isFsfLibre": true, "detailsUrl": "http://spdx.org/licenses/OSL-2.1.json", "referenceNumber": "24", "name": "Open Software License 2.1", "licenseId": "OSL-2.1", "seeAlso": [ "http://web.archive.org/web/20050212003940/http://www.rosenlaw.com/osl21.htm", "https://opensource.org/licenses/OSL-2.1" ], "isOsiApproved": true }, { "reference": "./OSL-3.0.html", "isDeprecatedLicenseId": false, "isFsfLibre": true, "detailsUrl": "http://spdx.org/licenses/OSL-3.0.json", "referenceNumber": "100", "name": "Open Software License 3.0", "licenseId": "OSL-3.0", "seeAlso": [ "https://web.archive.org/web/20120101081418/http://rosenlaw.com:80/OSL3.0.htm", "https://opensource.org/licenses/OSL-3.0" ], "isOsiApproved": true }, { "reference": "./OpenSSL.html", "isDeprecatedLicenseId": false, "isFsfLibre": true, "detailsUrl": "http://spdx.org/licenses/OpenSSL.json", "referenceNumber": "249", "name": "OpenSSL License", "licenseId": "OpenSSL", "seeAlso": [ "http://www.openssl.org/source/license.html" ], "isOsiApproved": false }, { "reference": "./PDDL-1.0.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/PDDL-1.0.json", "referenceNumber": "14", "name": "ODC Public Domain Dedication \u0026 License 1.0", "licenseId": "PDDL-1.0", "seeAlso": [ "http://opendatacommons.org/licenses/pddl/1.0/" ], "isOsiApproved": false }, { "reference": "./PHP-3.0.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/PHP-3.0.json", "referenceNumber": "385", "name": "PHP License v3.0", "licenseId": "PHP-3.0", "seeAlso": [ "http://www.php.net/license/3_0.txt", "https://opensource.org/licenses/PHP-3.0" ], "isOsiApproved": true }, { "reference": "./PHP-3.01.html", "isDeprecatedLicenseId": false, "isFsfLibre": true, "detailsUrl": "http://spdx.org/licenses/PHP-3.01.json", "referenceNumber": "316", "name": "PHP License v3.01", "licenseId": "PHP-3.01", "seeAlso": [ "http://www.php.net/license/3_01.txt" ], "isOsiApproved": false }, { "reference": "./Parity-6.0.0.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/Parity-6.0.0.json", "referenceNumber": "91", "name": "The Parity Public License 6.0.0", "licenseId": "Parity-6.0.0", "seeAlso": [ "https://paritylicense.com/versions/6.0.0.html" ], "isOsiApproved": false }, { "reference": "./Plexus.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/Plexus.json", "referenceNumber": "225", "name": "Plexus Classworlds License", "licenseId": "Plexus", "seeAlso": [ "https://fedoraproject.org/wiki/Licensing/Plexus_Classworlds_License" ], "isOsiApproved": false }, { "reference": "./PostgreSQL.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/PostgreSQL.json", "referenceNumber": "247", "name": "PostgreSQL License", "licenseId": "PostgreSQL", "seeAlso": [ "http://www.postgresql.org/about/licence", "https://opensource.org/licenses/PostgreSQL" ], "isOsiApproved": true }, { "reference": "./Python-2.0.html", "isDeprecatedLicenseId": false, "isFsfLibre": true, "detailsUrl": "http://spdx.org/licenses/Python-2.0.json", "referenceNumber": "35", "name": "Python License 2.0", "licenseId": "Python-2.0", "seeAlso": [ "https://opensource.org/licenses/Python-2.0" ], "isOsiApproved": true }, { "reference": "./QPL-1.0.html", "isDeprecatedLicenseId": false, "isFsfLibre": true, "detailsUrl": "http://spdx.org/licenses/QPL-1.0.json", "referenceNumber": "27", "name": "Q Public License 1.0", "licenseId": "QPL-1.0", "seeAlso": [ "http://doc.qt.nokia.com/3.3/license.html", "https://opensource.org/licenses/QPL-1.0" ], "isOsiApproved": true }, { "reference": "./Qhull.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/Qhull.json", "referenceNumber": "67", "name": "Qhull License", "licenseId": "Qhull", "seeAlso": [ "https://fedoraproject.org/wiki/Licensing/Qhull" ], "isOsiApproved": false }, { "reference": "./RHeCos-1.1.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/RHeCos-1.1.json", "referenceNumber": "149", "name": "Red Hat eCos Public License v1.1", "licenseId": "RHeCos-1.1", "seeAlso": [ "http://ecos.sourceware.org/old-license.html" ], "isOsiApproved": false }, { "reference": "./RPL-1.1.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/RPL-1.1.json", "referenceNumber": "269", "name": "Reciprocal Public License 1.1", "licenseId": "RPL-1.1", "seeAlso": [ "https://opensource.org/licenses/RPL-1.1" ], "isOsiApproved": true }, { "reference": "./RPL-1.5.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/RPL-1.5.json", "referenceNumber": "227", "name": "Reciprocal Public License 1.5", "licenseId": "RPL-1.5", "seeAlso": [ "https://opensource.org/licenses/RPL-1.5" ], "isOsiApproved": true }, { "reference": "./RPSL-1.0.html", "isDeprecatedLicenseId": false, "isFsfLibre": true, "detailsUrl": "http://spdx.org/licenses/RPSL-1.0.json", "referenceNumber": "273", "name": "RealNetworks Public Source License v1.0", "licenseId": "RPSL-1.0", "seeAlso": [ "https://helixcommunity.org/content/rpsl", "https://opensource.org/licenses/RPSL-1.0" ], "isOsiApproved": true }, { "reference": "./RSA-MD.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/RSA-MD.json", "referenceNumber": "82", "name": "RSA Message-Digest License ", "licenseId": "RSA-MD", "seeAlso": [ "http://www.faqs.org/rfcs/rfc1321.html" ], "isOsiApproved": false }, { "reference": "./RSCPL.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/RSCPL.json", "referenceNumber": "211", "name": "Ricoh Source Code Public License", "licenseId": "RSCPL", "seeAlso": [ "http://wayback.archive.org/web/20060715140826/http://www.risource.org/RPL/RPL-1.0A.shtml", "https://opensource.org/licenses/RSCPL" ], "isOsiApproved": true }, { "reference": "./Rdisc.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/Rdisc.json", "referenceNumber": "295", "name": "Rdisc License", "licenseId": "Rdisc", "seeAlso": [ "https://fedoraproject.org/wiki/Licensing/Rdisc_License" ], "isOsiApproved": false }, { "reference": "./Ruby.html", "isDeprecatedLicenseId": false, "isFsfLibre": true, "detailsUrl": "http://spdx.org/licenses/Ruby.json", "referenceNumber": "263", "name": "Ruby License", "licenseId": "Ruby", "seeAlso": [ "http://www.ruby-lang.org/en/LICENSE.txt" ], "isOsiApproved": false }, { "reference": "./SAX-PD.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/SAX-PD.json", "referenceNumber": "140", "name": "Sax Public Domain Notice", "licenseId": "SAX-PD", "seeAlso": [ "http://www.saxproject.org/copying.html" ], "isOsiApproved": false }, { "reference": "./SCEA.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/SCEA.json", "referenceNumber": "16", "name": "SCEA Shared Source License", "licenseId": "SCEA", "seeAlso": [ "http://research.scea.com/scea_shared_source_license.html" ], "isOsiApproved": false }, { "reference": "./SGI-B-1.0.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/SGI-B-1.0.json", "referenceNumber": "90", "name": "SGI Free Software License B v1.0", "licenseId": "SGI-B-1.0", "seeAlso": [ "http://oss.sgi.com/projects/FreeB/SGIFreeSWLicB.1.0.html" ], "isOsiApproved": false }, { "reference": "./SGI-B-1.1.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/SGI-B-1.1.json", "referenceNumber": "241", "name": "SGI Free Software License B v1.1", "licenseId": "SGI-B-1.1", "seeAlso": [ "http://oss.sgi.com/projects/FreeB/" ], "isOsiApproved": false }, { "reference": "./SGI-B-2.0.html", "isDeprecatedLicenseId": false, "isFsfLibre": true, "detailsUrl": "http://spdx.org/licenses/SGI-B-2.0.json", "referenceNumber": "272", "name": "SGI Free Software License B v2.0", "licenseId": "SGI-B-2.0", "seeAlso": [ "http://oss.sgi.com/projects/FreeB/SGIFreeSWLicB.2.0.pdf" ], "isOsiApproved": false }, { "reference": "./SHL-0.5.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/SHL-0.5.json", "referenceNumber": "72", "name": "Solderpad Hardware License v0.5", "licenseId": "SHL-0.5", "seeAlso": [ "https://solderpad.org/licenses/SHL-0.5/" ], "isOsiApproved": false }, { "reference": "./SHL-0.51.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/SHL-0.51.json", "referenceNumber": "314", "name": "Solderpad Hardware License, Version 0.51", "licenseId": "SHL-0.51", "seeAlso": [ "https://solderpad.org/licenses/SHL-0.51/" ], "isOsiApproved": false }, { "reference": "./SISSL.html", "isDeprecatedLicenseId": false, "isFsfLibre": true, "detailsUrl": "http://spdx.org/licenses/SISSL.json", "referenceNumber": "74", "name": "Sun Industry Standards Source License v1.1", "licenseId": "SISSL", "seeAlso": [ "http://www.openoffice.org/licenses/sissl_license.html", "https://opensource.org/licenses/SISSL" ], "isOsiApproved": true }, { "reference": "./SISSL-1.2.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/SISSL-1.2.json", "referenceNumber": "7", "name": "Sun Industry Standards Source License v1.2", "licenseId": "SISSL-1.2", "seeAlso": [ "http://gridscheduler.sourceforge.net/Gridengine_SISSL_license.html" ], "isOsiApproved": false }, { "reference": "./SMLNJ.html", "isDeprecatedLicenseId": false, "isFsfLibre": true, "detailsUrl": "http://spdx.org/licenses/SMLNJ.json", "referenceNumber": "296", "name": "Standard ML of New Jersey License", "licenseId": "SMLNJ", "seeAlso": [ "https://www.smlnj.org/license.html" ], "isOsiApproved": false }, { "reference": "./SMPPL.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/SMPPL.json", "referenceNumber": "127", "name": "Secure Messaging Protocol Public License", "licenseId": "SMPPL", "seeAlso": [ "https://github.com/dcblake/SMP/blob/master/Documentation/License.txt" ], "isOsiApproved": false }, { "reference": "./SNIA.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/SNIA.json", "referenceNumber": "230", "name": "SNIA Public License 1.1", "licenseId": "SNIA", "seeAlso": [ "https://fedoraproject.org/wiki/Licensing/SNIA_Public_License" ], "isOsiApproved": false }, { "reference": "./SPL-1.0.html", "isDeprecatedLicenseId": false, "isFsfLibre": true, "detailsUrl": "http://spdx.org/licenses/SPL-1.0.json", "referenceNumber": "54", "name": "Sun Public License v1.0", "licenseId": "SPL-1.0", "seeAlso": [ "https://opensource.org/licenses/SPL-1.0" ], "isOsiApproved": true }, { "reference": "./SSPL-1.0.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/SSPL-1.0.json", "referenceNumber": "356", "name": "Server Side Public License, v 1", "licenseId": "SSPL-1.0", "seeAlso": [ "https://www.mongodb.com/licensing/server-side-public-license" ], "isOsiApproved": false }, { "reference": "./SWL.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/SWL.json", "referenceNumber": "208", "name": "Scheme Widget Library (SWL) Software License Agreement", "licenseId": "SWL", "seeAlso": [ "https://fedoraproject.org/wiki/Licensing/SWL" ], "isOsiApproved": false }, { "reference": "./Saxpath.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/Saxpath.json", "referenceNumber": "18", "name": "Saxpath License", "licenseId": "Saxpath", "seeAlso": [ "https://fedoraproject.org/wiki/Licensing/Saxpath_License" ], "isOsiApproved": false }, { "reference": "./Sendmail.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/Sendmail.json", "referenceNumber": "151", "name": "Sendmail License", "licenseId": "Sendmail", "seeAlso": [ "http://www.sendmail.com/pdfs/open_source/sendmail_license.pdf", "https://web.archive.org/web/20160322142305/https://www.sendmail.com/pdfs/open_source/sendmail_license.pdf" ], "isOsiApproved": false }, { "reference": "./Sendmail-8.23.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/Sendmail-8.23.json", "referenceNumber": "41", "name": "Sendmail License 8.23", "licenseId": "Sendmail-8.23", "seeAlso": [ "https://www.proofpoint.com/sites/default/files/sendmail-license.pdf", "https://web.archive.org/web/20181003101040/https://www.proofpoint.com/sites/default/files/sendmail-license.pdf" ], "isOsiApproved": false }, { "reference": "./SimPL-2.0.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/SimPL-2.0.json", "referenceNumber": "184", "name": "Simple Public License 2.0", "licenseId": "SimPL-2.0", "seeAlso": [ "https://opensource.org/licenses/SimPL-2.0" ], "isOsiApproved": true }, { "reference": "./Sleepycat.html", "isDeprecatedLicenseId": false, "isFsfLibre": true, "detailsUrl": "http://spdx.org/licenses/Sleepycat.json", "referenceNumber": "290", "name": "Sleepycat License", "licenseId": "Sleepycat", "seeAlso": [ "https://opensource.org/licenses/Sleepycat" ], "isOsiApproved": true }, { "reference": "./Spencer-86.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/Spencer-86.json", "referenceNumber": "313", "name": "Spencer License 86", "licenseId": "Spencer-86", "seeAlso": [ "https://fedoraproject.org/wiki/Licensing/Henry_Spencer_Reg-Ex_Library_License" ], "isOsiApproved": false }, { "reference": "./Spencer-94.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/Spencer-94.json", "referenceNumber": "29", "name": "Spencer License 94", "licenseId": "Spencer-94", "seeAlso": [ "https://fedoraproject.org/wiki/Licensing/Henry_Spencer_Reg-Ex_Library_License" ], "isOsiApproved": false }, { "reference": "./Spencer-99.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/Spencer-99.json", "referenceNumber": "386", "name": "Spencer License 99", "licenseId": "Spencer-99", "seeAlso": [ "http://www.opensource.apple.com/source/tcl/tcl-5/tcl/generic/regfronts.c" ], "isOsiApproved": false }, { "reference": "./StandardML-NJ.html", "isDeprecatedLicenseId": true, "isFsfLibre": true, "detailsUrl": "http://spdx.org/licenses/StandardML-NJ.json", "referenceNumber": "219", "name": "Standard ML of New Jersey License", "licenseId": "StandardML-NJ", "seeAlso": [ "http://www.smlnj.org//license.html" ], "isOsiApproved": false }, { "reference": "./SugarCRM-1.1.3.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/SugarCRM-1.1.3.json", "referenceNumber": "292", "name": "SugarCRM Public License v1.1.3", "licenseId": "SugarCRM-1.1.3", "seeAlso": [ "http://www.sugarcrm.com/crm/SPL" ], "isOsiApproved": false }, { "reference": "./TAPR-OHL-1.0.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/TAPR-OHL-1.0.json", "referenceNumber": "267", "name": "TAPR Open Hardware License v1.0", "licenseId": "TAPR-OHL-1.0", "seeAlso": [ "https://www.tapr.org/OHL" ], "isOsiApproved": false }, { "reference": "./TCL.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/TCL.json", "referenceNumber": "265", "name": "TCL/TK License", "licenseId": "TCL", "seeAlso": [ "http://www.tcl.tk/software/tcltk/license.html", "https://fedoraproject.org/wiki/Licensing/TCL" ], "isOsiApproved": false }, { "reference": "./TCP-wrappers.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/TCP-wrappers.json", "referenceNumber": "274", "name": "TCP Wrappers License", "licenseId": "TCP-wrappers", "seeAlso": [ "http://rc.quest.com/topics/openssh/license.php#tcpwrappers" ], "isOsiApproved": false }, { "reference": "./TMate.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/TMate.json", "referenceNumber": "253", "name": "TMate Open Source License", "licenseId": "TMate", "seeAlso": [ "http://svnkit.com/license.html" ], "isOsiApproved": false }, { "reference": "./TORQUE-1.1.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/TORQUE-1.1.json", "referenceNumber": "171", "name": "TORQUE v2.5+ Software License v1.1", "licenseId": "TORQUE-1.1", "seeAlso": [ "https://fedoraproject.org/wiki/Licensing/TORQUEv1.1" ], "isOsiApproved": false }, { "reference": "./TOSL.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/TOSL.json", "referenceNumber": "360", "name": "Trusster Open Source License", "licenseId": "TOSL", "seeAlso": [ "https://fedoraproject.org/wiki/Licensing/TOSL" ], "isOsiApproved": false }, { "reference": "./TU-Berlin-1.0.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/TU-Berlin-1.0.json", "referenceNumber": "373", "name": "Technische Universitaet Berlin License 1.0", "licenseId": "TU-Berlin-1.0", "seeAlso": [ "https://github.com/swh/ladspa/blob/7bf6f3799fdba70fda297c2d8fd9f526803d9680/gsm/COPYRIGHT" ], "isOsiApproved": false }, { "reference": "./TU-Berlin-2.0.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/TU-Berlin-2.0.json", "referenceNumber": "391", "name": "Technische Universitaet Berlin License 2.0", "licenseId": "TU-Berlin-2.0", "seeAlso": [ "https://github.com/CorsixTH/deps/blob/fd339a9f526d1d9c9f01ccf39e438a015da50035/licences/libgsm.txt" ], "isOsiApproved": false }, { "reference": "./UPL-1.0.html", "isDeprecatedLicenseId": false, "isFsfLibre": true, "detailsUrl": "http://spdx.org/licenses/UPL-1.0.json", "referenceNumber": "205", "name": "Universal Permissive License v1.0", "licenseId": "UPL-1.0", "seeAlso": [ "https://opensource.org/licenses/UPL" ], "isOsiApproved": true }, { "reference": "./Unicode-DFS-2015.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/Unicode-DFS-2015.json", "referenceNumber": "11", "name": "Unicode License Agreement - Data Files and Software (2015)", "licenseId": "Unicode-DFS-2015", "seeAlso": [ "https://web.archive.org/web/20151224134844/http://unicode.org/copyright.html" ], "isOsiApproved": false }, { "reference": "./Unicode-DFS-2016.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/Unicode-DFS-2016.json", "referenceNumber": "382", "name": "Unicode License Agreement - Data Files and Software (2016)", "licenseId": "Unicode-DFS-2016", "seeAlso": [ "http://www.unicode.org/copyright.html" ], "isOsiApproved": false }, { "reference": "./Unicode-TOU.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/Unicode-TOU.json", "referenceNumber": "70", "name": "Unicode Terms of Use", "licenseId": "Unicode-TOU", "seeAlso": [ "http://www.unicode.org/copyright.html" ], "isOsiApproved": false }, { "reference": "./Unlicense.html", "isDeprecatedLicenseId": false, "isFsfLibre": true, "detailsUrl": "http://spdx.org/licenses/Unlicense.json", "referenceNumber": "293", "name": "The Unlicense", "licenseId": "Unlicense", "seeAlso": [ "http://unlicense.org/" ], "isOsiApproved": false }, { "reference": "./VOSTROM.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/VOSTROM.json", "referenceNumber": "228", "name": "VOSTROM Public License for Open Source", "licenseId": "VOSTROM", "seeAlso": [ "https://fedoraproject.org/wiki/Licensing/VOSTROM" ], "isOsiApproved": false }, { "reference": "./VSL-1.0.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/VSL-1.0.json", "referenceNumber": "180", "name": "Vovida Software License v1.0", "licenseId": "VSL-1.0", "seeAlso": [ "https://opensource.org/licenses/VSL-1.0" ], "isOsiApproved": true }, { "reference": "./Vim.html", "isDeprecatedLicenseId": false, "isFsfLibre": true, "detailsUrl": "http://spdx.org/licenses/Vim.json", "referenceNumber": "133", "name": "Vim License", "licenseId": "Vim", "seeAlso": [ "http://vimdoc.sourceforge.net/htmldoc/uganda.html" ], "isOsiApproved": false }, { "reference": "./W3C.html", "isDeprecatedLicenseId": false, "isFsfLibre": true, "detailsUrl": "http://spdx.org/licenses/W3C.json", "referenceNumber": "351", "name": "W3C Software Notice and License (2002-12-31)", "licenseId": "W3C", "seeAlso": [ "http://www.w3.org/Consortium/Legal/2002/copyright-software-20021231.html", "https://opensource.org/licenses/W3C" ], "isOsiApproved": true }, { "reference": "./W3C-19980720.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/W3C-19980720.json", "referenceNumber": "323", "name": "W3C Software Notice and License (1998-07-20)", "licenseId": "W3C-19980720", "seeAlso": [ "http://www.w3.org/Consortium/Legal/copyright-software-19980720.html" ], "isOsiApproved": false }, { "reference": "./W3C-20150513.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/W3C-20150513.json", "referenceNumber": "51", "name": "W3C Software Notice and Document License (2015-05-13)", "licenseId": "W3C-20150513", "seeAlso": [ "https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document" ], "isOsiApproved": false }, { "reference": "./WTFPL.html", "isDeprecatedLicenseId": false, "isFsfLibre": true, "detailsUrl": "http://spdx.org/licenses/WTFPL.json", "referenceNumber": "368", "name": "Do What The F*ck You Want To Public License", "licenseId": "WTFPL", "seeAlso": [ "http://sam.zoy.org/wtfpl/COPYING" ], "isOsiApproved": false }, { "reference": "./Watcom-1.0.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/Watcom-1.0.json", "referenceNumber": "177", "name": "Sybase Open Watcom Public License 1.0", "licenseId": "Watcom-1.0", "seeAlso": [ "https://opensource.org/licenses/Watcom-1.0" ], "isOsiApproved": true }, { "reference": "./Wsuipa.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/Wsuipa.json", "referenceNumber": "135", "name": "Wsuipa License", "licenseId": "Wsuipa", "seeAlso": [ "https://fedoraproject.org/wiki/Licensing/Wsuipa" ], "isOsiApproved": false }, { "reference": "./X11.html", "isDeprecatedLicenseId": false, "isFsfLibre": true, "detailsUrl": "http://spdx.org/licenses/X11.json", "referenceNumber": "188", "name": "X11 License", "licenseId": "X11", "seeAlso": [ "http://www.xfree86.org/3.3.6/COPYRIGHT2.html#3" ], "isOsiApproved": false }, { "reference": "./XFree86-1.1.html", "isDeprecatedLicenseId": false, "isFsfLibre": true, "detailsUrl": "http://spdx.org/licenses/XFree86-1.1.json", "referenceNumber": "243", "name": "XFree86 License 1.1", "licenseId": "XFree86-1.1", "seeAlso": [ "http://www.xfree86.org/current/LICENSE4.html" ], "isOsiApproved": false }, { "reference": "./XSkat.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/XSkat.json", "referenceNumber": "96", "name": "XSkat License", "licenseId": "XSkat", "seeAlso": [ "https://fedoraproject.org/wiki/Licensing/XSkat_License" ], "isOsiApproved": false }, { "reference": "./Xerox.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/Xerox.json", "referenceNumber": "163", "name": "Xerox License", "licenseId": "Xerox", "seeAlso": [ "https://fedoraproject.org/wiki/Licensing/Xerox" ], "isOsiApproved": false }, { "reference": "./Xnet.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/Xnet.json", "referenceNumber": "388", "name": "X.Net License", "licenseId": "Xnet", "seeAlso": [ "https://opensource.org/licenses/Xnet" ], "isOsiApproved": true }, { "reference": "./YPL-1.0.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/YPL-1.0.json", "referenceNumber": "174", "name": "Yahoo! Public License v1.0", "licenseId": "YPL-1.0", "seeAlso": [ "http://www.zimbra.com/license/yahoo_public_license_1.0.html" ], "isOsiApproved": false }, { "reference": "./YPL-1.1.html", "isDeprecatedLicenseId": false, "isFsfLibre": true, "detailsUrl": "http://spdx.org/licenses/YPL-1.1.json", "referenceNumber": "57", "name": "Yahoo! Public License v1.1", "licenseId": "YPL-1.1", "seeAlso": [ "http://www.zimbra.com/license/yahoo_public_license_1.1.html" ], "isOsiApproved": false }, { "reference": "./ZPL-1.1.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/ZPL-1.1.json", "referenceNumber": "359", "name": "Zope Public License 1.1", "licenseId": "ZPL-1.1", "seeAlso": [ "http://old.zope.org/Resources/License/ZPL-1.1" ], "isOsiApproved": false }, { "reference": "./ZPL-2.0.html", "isDeprecatedLicenseId": false, "isFsfLibre": true, "detailsUrl": "http://spdx.org/licenses/ZPL-2.0.json", "referenceNumber": "78", "name": "Zope Public License 2.0", "licenseId": "ZPL-2.0", "seeAlso": [ "http://old.zope.org/Resources/License/ZPL-2.0", "https://opensource.org/licenses/ZPL-2.0" ], "isOsiApproved": true }, { "reference": "./ZPL-2.1.html", "isDeprecatedLicenseId": false, "isFsfLibre": true, "detailsUrl": "http://spdx.org/licenses/ZPL-2.1.json", "referenceNumber": "345", "name": "Zope Public License 2.1", "licenseId": "ZPL-2.1", "seeAlso": [ "http://old.zope.org/Resources/ZPL/" ], "isOsiApproved": false }, { "reference": "./Zed.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/Zed.json", "referenceNumber": "248", "name": "Zed License", "licenseId": "Zed", "seeAlso": [ "https://fedoraproject.org/wiki/Licensing/Zed" ], "isOsiApproved": false }, { "reference": "./Zend-2.0.html", "isDeprecatedLicenseId": false, "isFsfLibre": true, "detailsUrl": "http://spdx.org/licenses/Zend-2.0.json", "referenceNumber": "198", "name": "Zend License v2.0", "licenseId": "Zend-2.0", "seeAlso": [ "https://web.archive.org/web/20130517195954/http://www.zend.com/license/2_00.txt" ], "isOsiApproved": false }, { "reference": "./Zimbra-1.3.html", "isDeprecatedLicenseId": false, "isFsfLibre": true, "detailsUrl": "http://spdx.org/licenses/Zimbra-1.3.json", "referenceNumber": "40", "name": "Zimbra Public License v1.3", "licenseId": "Zimbra-1.3", "seeAlso": [ "http://web.archive.org/web/20100302225219/http://www.zimbra.com/license/zimbra-public-license-1-3.html" ], "isOsiApproved": false }, { "reference": "./Zimbra-1.4.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/Zimbra-1.4.json", "referenceNumber": "238", "name": "Zimbra Public License v1.4", "licenseId": "Zimbra-1.4", "seeAlso": [ "http://www.zimbra.com/legal/zimbra-public-license-1-4" ], "isOsiApproved": false }, { "reference": "./Zlib.html", "isDeprecatedLicenseId": false, "isFsfLibre": true, "detailsUrl": "http://spdx.org/licenses/Zlib.json", "referenceNumber": "320", "name": "zlib License", "licenseId": "Zlib", "seeAlso": [ "http://www.zlib.net/zlib_license.html", "https://opensource.org/licenses/Zlib" ], "isOsiApproved": true }, { "reference": "./blessing.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/blessing.json", "referenceNumber": "331", "name": "SQLite Blessing", "licenseId": "blessing", "seeAlso": [ "https://www.sqlite.org/src/artifact/e33a4df7e32d742a?ln\u003d4-9", "https://sqlite.org/src/artifact/df5091916dbb40e6" ], "isOsiApproved": false }, { "reference": "./bzip2-1.0.5.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/bzip2-1.0.5.json", "referenceNumber": "200", "name": "bzip2 and libbzip2 License v1.0.5", "licenseId": "bzip2-1.0.5", "seeAlso": [ "http://bzip.org/1.0.5/bzip2-manual-1.0.5.html" ], "isOsiApproved": false }, { "reference": "./bzip2-1.0.6.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/bzip2-1.0.6.json", "referenceNumber": "302", "name": "bzip2 and libbzip2 License v1.0.6", "licenseId": "bzip2-1.0.6", "seeAlso": [ "https://github.com/asimonov-im/bzip2/blob/master/LICENSE" ], "isOsiApproved": false }, { "reference": "./copyleft-next-0.3.0.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/copyleft-next-0.3.0.json", "referenceNumber": "176", "name": "copyleft-next 0.3.0", "licenseId": "copyleft-next-0.3.0", "seeAlso": [ "https://github.com/copyleft-next/copyleft-next/blob/master/Releases/copyleft-next-0.3.0" ], "isOsiApproved": false }, { "reference": "./copyleft-next-0.3.1.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/copyleft-next-0.3.1.json", "referenceNumber": "347", "name": "copyleft-next 0.3.1", "licenseId": "copyleft-next-0.3.1", "seeAlso": [ "https://github.com/copyleft-next/copyleft-next/blob/master/Releases/copyleft-next-0.3.1" ], "isOsiApproved": false }, { "reference": "./curl.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/curl.json", "referenceNumber": "260", "name": "curl License", "licenseId": "curl", "seeAlso": [ "https://github.com/bagder/curl/blob/master/COPYING" ], "isOsiApproved": false }, { "reference": "./diffmark.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/diffmark.json", "referenceNumber": "367", "name": "diffmark license", "licenseId": "diffmark", "seeAlso": [ "https://fedoraproject.org/wiki/Licensing/diffmark" ], "isOsiApproved": false }, { "reference": "./dvipdfm.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/dvipdfm.json", "referenceNumber": "143", "name": "dvipdfm License", "licenseId": "dvipdfm", "seeAlso": [ "https://fedoraproject.org/wiki/Licensing/dvipdfm" ], "isOsiApproved": false }, { "reference": "./eCos-2.0.html", "isDeprecatedLicenseId": true, "isFsfLibre": true, "detailsUrl": "http://spdx.org/licenses/eCos-2.0.json", "referenceNumber": "329", "name": "eCos license version 2.0", "licenseId": "eCos-2.0", "seeAlso": [ "https://www.gnu.org/licenses/ecos-license.html" ], "isOsiApproved": false }, { "reference": "./eGenix.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/eGenix.json", "referenceNumber": "204", "name": "eGenix.com Public License 1.1.0", "licenseId": "eGenix", "seeAlso": [ "http://www.egenix.com/products/eGenix.com-Public-License-1.1.0.pdf", "https://fedoraproject.org/wiki/Licensing/eGenix.com_Public_License_1.1.0" ], "isOsiApproved": false }, { "reference": "./gSOAP-1.3b.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/gSOAP-1.3b.json", "referenceNumber": "346", "name": "gSOAP Public License v1.3b", "licenseId": "gSOAP-1.3b", "seeAlso": [ "http://www.cs.fsu.edu/~engelen/license.html" ], "isOsiApproved": false }, { "reference": "./gnuplot.html", "isDeprecatedLicenseId": false, "isFsfLibre": true, "detailsUrl": "http://spdx.org/licenses/gnuplot.json", "referenceNumber": "10", "name": "gnuplot License", "licenseId": "gnuplot", "seeAlso": [ "https://fedoraproject.org/wiki/Licensing/Gnuplot" ], "isOsiApproved": false }, { "reference": "./iMatix.html", "isDeprecatedLicenseId": false, "isFsfLibre": true, "detailsUrl": "http://spdx.org/licenses/iMatix.json", "referenceNumber": "342", "name": "iMatix Standard Function Library Agreement", "licenseId": "iMatix", "seeAlso": [ "http://legacy.imatix.com/html/sfl/sfl4.htm#license" ], "isOsiApproved": false }, { "reference": "./libpng-2.0.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/libpng-2.0.json", "referenceNumber": "76", "name": "PNG Reference Library version 2", "licenseId": "libpng-2.0", "seeAlso": [ "http://www.libpng.org/pub/png/src/libpng-LICENSE.txt" ], "isOsiApproved": false }, { "reference": "./libtiff.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/libtiff.json", "referenceNumber": "220", "name": "libtiff License", "licenseId": "libtiff", "seeAlso": [ "https://fedoraproject.org/wiki/Licensing/libtiff" ], "isOsiApproved": false }, { "reference": "./mpich2.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/mpich2.json", "referenceNumber": "318", "name": "mpich2 License", "licenseId": "mpich2", "seeAlso": [ "https://fedoraproject.org/wiki/Licensing/MIT" ], "isOsiApproved": false }, { "reference": "./psfrag.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/psfrag.json", "referenceNumber": "245", "name": "psfrag License", "licenseId": "psfrag", "seeAlso": [ "https://fedoraproject.org/wiki/Licensing/psfrag" ], "isOsiApproved": false }, { "reference": "./psutils.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/psutils.json", "referenceNumber": "126", "name": "psutils License", "licenseId": "psutils", "seeAlso": [ "https://fedoraproject.org/wiki/Licensing/psutils" ], "isOsiApproved": false }, { "reference": "./wxWindows.html", "isDeprecatedLicenseId": true, "detailsUrl": "http://spdx.org/licenses/wxWindows.json", "referenceNumber": "86", "name": "wxWindows Library License", "licenseId": "wxWindows", "seeAlso": [ "https://opensource.org/licenses/WXwindows" ], "isOsiApproved": false }, { "reference": "./xinetd.html", "isDeprecatedLicenseId": false, "isFsfLibre": true, "detailsUrl": "http://spdx.org/licenses/xinetd.json", "referenceNumber": "146", "name": "xinetd License", "licenseId": "xinetd", "seeAlso": [ "https://fedoraproject.org/wiki/Licensing/Xinetd_License" ], "isOsiApproved": false }, { "reference": "./xpp.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/xpp.json", "referenceNumber": "275", "name": "XPP License", "licenseId": "xpp", "seeAlso": [ "https://fedoraproject.org/wiki/Licensing/xpp" ], "isOsiApproved": false }, { "reference": "./zlib-acknowledgement.html", "isDeprecatedLicenseId": false, "detailsUrl": "http://spdx.org/licenses/zlib-acknowledgement.json", "referenceNumber": "321", "name": "zlib/libpng License with Acknowledgement", "licenseId": "zlib-acknowledgement", "seeAlso": [ "https://fedoraproject.org/wiki/Licensing/ZlibWithAcknowledgement" ], "isOsiApproved": false } ], "releaseDate": "2019-07-10" }
diff --git a/spec/frontend/clusters/stores/clusters_store_spec.js b/spec/frontend/clusters/stores/clusters_store_spec.js
index ee3b7d8aa90..5ee06eb44c9 100644
--- a/spec/frontend/clusters/stores/clusters_store_spec.js
+++ b/spec/frontend/clusters/stores/clusters_store_spec.js
@@ -54,8 +54,11 @@ describe('Clusters Store', () => {
environmentsHelpPath: null,
clustersHelpPath: null,
deployBoardsHelpPath: null,
+ cloudRunHelpPath: null,
status: mockResponseData.status,
statusReason: mockResponseData.status_reason,
+ providerType: null,
+ preInstalledKnative: false,
rbac: false,
applications: {
helm: {
diff --git a/spec/frontend/commit/commit_pipeline_status_component_spec.js b/spec/frontend/commit/commit_pipeline_status_component_spec.js
new file mode 100644
index 00000000000..1736d1d0df8
--- /dev/null
+++ b/spec/frontend/commit/commit_pipeline_status_component_spec.js
@@ -0,0 +1,179 @@
+import Visibility from 'visibilityjs';
+import { GlLoadingIcon } from '@gitlab/ui';
+import Poll from '~/lib/utils/poll';
+import flash from '~/flash';
+import CiIcon from '~/vue_shared/components/ci_icon.vue';
+import CommitPipelineStatus from '~/projects/tree/components/commit_pipeline_status_component.vue';
+import { shallowMount } from '@vue/test-utils';
+import { getJSONFixture } from '../helpers/fixtures';
+
+jest.mock('~/lib/utils/poll');
+jest.mock('visibilityjs');
+jest.mock('~/flash');
+
+const mockFetchData = jest.fn();
+jest.mock('~/projects/tree/services/commit_pipeline_service', () =>
+ jest.fn().mockImplementation(() => ({
+ fetchData: mockFetchData.mockReturnValue(Promise.resolve()),
+ })),
+);
+
+describe('Commit pipeline status component', () => {
+ let wrapper;
+ const { pipelines } = getJSONFixture('pipelines/pipelines.json');
+ const { status: mockCiStatus } = pipelines[0].details;
+
+ const defaultProps = {
+ endpoint: 'endpoint',
+ };
+
+ const createComponent = (props = {}) => {
+ wrapper = shallowMount(CommitPipelineStatus, {
+ propsData: {
+ ...defaultProps,
+ ...props,
+ },
+ sync: false,
+ });
+ };
+
+ const findLoader = () => wrapper.find(GlLoadingIcon);
+ const findLink = () => wrapper.find('a');
+ const findCiIcon = () => findLink().find(CiIcon);
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ jest.clearAllMocks();
+ });
+
+ describe('Visibility management', () => {
+ describe('when component is hidden', () => {
+ beforeEach(() => {
+ Visibility.hidden.mockReturnValue(true);
+ createComponent();
+ });
+
+ it('does not start polling', () => {
+ const [pollInstance] = Poll.mock.instances;
+ expect(pollInstance.makeRequest).not.toHaveBeenCalled();
+ });
+
+ it('requests pipeline data', () => {
+ expect(mockFetchData).toHaveBeenCalled();
+ });
+ });
+
+ describe('when component is visible', () => {
+ beforeEach(() => {
+ Visibility.hidden.mockReturnValue(false);
+ createComponent();
+ });
+
+ it('starts polling', () => {
+ const [pollInstance] = [...Poll.mock.instances].reverse();
+ expect(pollInstance.makeRequest).toHaveBeenCalled();
+ });
+ });
+
+ describe('when component changes its visibility', () => {
+ it.each`
+ visibility | action
+ ${false} | ${'restart'}
+ ${true} | ${'stop'}
+ `(
+ '$action polling when component visibility becomes $visibility',
+ ({ visibility, action }) => {
+ Visibility.hidden.mockReturnValue(!visibility);
+ createComponent();
+ const [pollInstance] = Poll.mock.instances;
+ expect(pollInstance[action]).not.toHaveBeenCalled();
+ Visibility.hidden.mockReturnValue(visibility);
+ const [visibilityHandler] = Visibility.change.mock.calls[0];
+ visibilityHandler();
+ expect(pollInstance[action]).toHaveBeenCalled();
+ },
+ );
+ });
+ });
+
+ it('stops polling when component is destroyed', () => {
+ createComponent();
+ wrapper.destroy();
+ const [pollInstance] = Poll.mock.instances;
+ expect(pollInstance.stop).toHaveBeenCalled();
+ });
+
+ describe('when polling', () => {
+ let pollConfig;
+ beforeEach(() => {
+ Poll.mockImplementation(config => {
+ pollConfig = config;
+ return { makeRequest: jest.fn(), restart: jest.fn(), stop: jest.fn() };
+ });
+ createComponent();
+ });
+
+ it('shows the loading icon at start', () => {
+ createComponent();
+ expect(findLoader().exists()).toBe(true);
+
+ pollConfig.successCallback({
+ data: { pipelines: [] },
+ });
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(findLoader().exists()).toBe(false);
+ });
+ });
+
+ describe('is successful', () => {
+ beforeEach(() => {
+ pollConfig.successCallback({
+ data: { pipelines: [{ details: { status: mockCiStatus } }] },
+ });
+ return wrapper.vm.$nextTick();
+ });
+
+ it('does not render loader', () => {
+ expect(findLoader().exists()).toBe(false);
+ });
+
+ it('renders link with href', () => {
+ expect(findLink().attributes('href')).toEqual(mockCiStatus.details_path);
+ });
+
+ it('renders CI icon', () => {
+ expect(findCiIcon().attributes('data-original-title')).toEqual('Pipeline: pending');
+ expect(findCiIcon().props('status')).toEqual(mockCiStatus);
+ });
+ });
+
+ describe('is not successful', () => {
+ beforeEach(() => {
+ pollConfig.errorCallback();
+ });
+
+ it('does not render loader', () => {
+ expect(findLoader().exists()).toBe(false);
+ });
+
+ it('renders link with href', () => {
+ expect(findLink().attributes('href')).toBeUndefined();
+ });
+
+ it('renders not found CI icon', () => {
+ expect(findCiIcon().attributes('data-original-title')).toEqual('Pipeline: not found');
+ expect(findCiIcon().props('status')).toEqual({
+ text: 'not found',
+ icon: 'status_notfound',
+ group: 'notfound',
+ });
+ });
+
+ it('displays flash error message', () => {
+ expect(flash).toHaveBeenCalled();
+ });
+ });
+ });
+});
diff --git a/spec/frontend/create_cluster/eks_cluster/components/cluster_form_dropdown_spec.js b/spec/frontend/create_cluster/eks_cluster/components/cluster_form_dropdown_spec.js
index e873ef0b2fa..366c2fc7b26 100644
--- a/spec/frontend/create_cluster/eks_cluster/components/cluster_form_dropdown_spec.js
+++ b/spec/frontend/create_cluster/eks_cluster/components/cluster_form_dropdown_spec.js
@@ -7,12 +7,22 @@ import DropdownHiddenInput from '~/vue_shared/components/dropdown/dropdown_hidde
describe('ClusterFormDropdown', () => {
let vm;
+ const firstItem = { name: 'item 1', value: 1 };
+ const secondItem = { name: 'item 2', value: 2 };
+ const items = [firstItem, secondItem, { name: 'item 3', value: 3 }];
beforeEach(() => {
vm = shallowMount(ClusterFormDropdown);
});
afterEach(() => vm.destroy());
+ describe('when initial value is provided', () => {
+ it('sets selectedItem to initial value', () => {
+ vm.setProps({ items, value: secondItem.value });
+ expect(vm.find(DropdownButton).props('toggleText')).toEqual(secondItem.name);
+ });
+ });
+
describe('when no item is selected', () => {
it('displays placeholder text', () => {
const placeholder = 'placeholder';
@@ -24,18 +34,19 @@ describe('ClusterFormDropdown', () => {
});
describe('when an item is selected', () => {
- const selectedItem = { name: 'Name', value: 'value' };
-
beforeEach(() => {
- vm.setData({ selectedItem });
+ vm.setProps({ items });
+ vm.findAll('.js-dropdown-item')
+ .at(1)
+ .trigger('click');
});
it('displays selected item label', () => {
- expect(vm.find(DropdownButton).props('toggleText')).toEqual(selectedItem.name);
+ expect(vm.find(DropdownButton).props('toggleText')).toEqual(secondItem.name);
});
it('sets selected value to dropdown hidden input', () => {
- expect(vm.find(DropdownHiddenInput).props('value')).toEqual(selectedItem.value);
+ expect(vm.find(DropdownHiddenInput).props('value')).toEqual(secondItem.value);
});
});
@@ -124,9 +135,7 @@ describe('ClusterFormDropdown', () => {
});
it('it filters results by search query', () => {
- const secondItem = { name: 'second item' };
- const items = [{ name: 'first item' }, secondItem];
- const searchQuery = 'second';
+ const searchQuery = secondItem.name;
vm.setProps({ items });
vm.setData({ searchQuery });
diff --git a/spec/frontend/create_cluster/eks_cluster/components/eks_cluster_configuration_form_spec.js b/spec/frontend/create_cluster/eks_cluster/components/eks_cluster_configuration_form_spec.js
new file mode 100644
index 00000000000..69290f6dfa9
--- /dev/null
+++ b/spec/frontend/create_cluster/eks_cluster/components/eks_cluster_configuration_form_spec.js
@@ -0,0 +1,457 @@
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+import Vuex from 'vuex';
+import Vue from 'vue';
+import { GlFormCheckbox } from '@gitlab/ui';
+
+import EksClusterConfigurationForm from '~/create_cluster/eks_cluster/components/eks_cluster_configuration_form.vue';
+import RegionDropdown from '~/create_cluster/eks_cluster/components/region_dropdown.vue';
+import eksClusterFormState from '~/create_cluster/eks_cluster/store/state';
+import clusterDropdownStoreState from '~/create_cluster/eks_cluster/store/cluster_dropdown/state';
+
+const localVue = createLocalVue();
+localVue.use(Vuex);
+
+describe('EksClusterConfigurationForm', () => {
+ let store;
+ let actions;
+ let state;
+ let rolesState;
+ let regionsState;
+ let vpcsState;
+ let subnetsState;
+ let keyPairsState;
+ let securityGroupsState;
+ let vpcsActions;
+ let rolesActions;
+ let regionsActions;
+ let subnetsActions;
+ let keyPairsActions;
+ let securityGroupsActions;
+ let vm;
+
+ beforeEach(() => {
+ state = eksClusterFormState();
+ actions = {
+ setClusterName: jest.fn(),
+ setEnvironmentScope: jest.fn(),
+ setKubernetesVersion: jest.fn(),
+ setRegion: jest.fn(),
+ setVpc: jest.fn(),
+ setSubnet: jest.fn(),
+ setRole: jest.fn(),
+ setKeyPair: jest.fn(),
+ setSecurityGroup: jest.fn(),
+ setGitlabManagedCluster: jest.fn(),
+ };
+ regionsActions = {
+ fetchItems: jest.fn(),
+ };
+ keyPairsActions = {
+ fetchItems: jest.fn(),
+ };
+ vpcsActions = {
+ fetchItems: jest.fn(),
+ };
+ subnetsActions = {
+ fetchItems: jest.fn(),
+ };
+ rolesActions = {
+ fetchItems: jest.fn(),
+ };
+ securityGroupsActions = {
+ fetchItems: jest.fn(),
+ };
+ rolesState = {
+ ...clusterDropdownStoreState(),
+ };
+ regionsState = {
+ ...clusterDropdownStoreState(),
+ };
+ vpcsState = {
+ ...clusterDropdownStoreState(),
+ };
+ subnetsState = {
+ ...clusterDropdownStoreState(),
+ };
+ keyPairsState = {
+ ...clusterDropdownStoreState(),
+ };
+ securityGroupsState = {
+ ...clusterDropdownStoreState(),
+ };
+ store = new Vuex.Store({
+ state,
+ actions,
+ modules: {
+ vpcs: {
+ namespaced: true,
+ state: vpcsState,
+ actions: vpcsActions,
+ },
+ regions: {
+ namespaced: true,
+ state: regionsState,
+ actions: regionsActions,
+ },
+ subnets: {
+ namespaced: true,
+ state: subnetsState,
+ actions: subnetsActions,
+ },
+ roles: {
+ namespaced: true,
+ state: rolesState,
+ actions: rolesActions,
+ },
+ keyPairs: {
+ namespaced: true,
+ state: keyPairsState,
+ actions: keyPairsActions,
+ },
+ securityGroups: {
+ namespaced: true,
+ state: securityGroupsState,
+ actions: securityGroupsActions,
+ },
+ },
+ });
+ });
+
+ beforeEach(() => {
+ vm = shallowMount(EksClusterConfigurationForm, {
+ localVue,
+ store,
+ propsData: {
+ gitlabManagedClusterHelpPath: '',
+ kubernetesIntegrationHelpPath: '',
+ },
+ });
+ });
+
+ afterEach(() => {
+ vm.destroy();
+ });
+
+ const findClusterNameInput = () => vm.find('[id=eks-cluster-name]');
+ const findEnvironmentScopeInput = () => vm.find('[id=eks-environment-scope]');
+ const findKubernetesVersionDropdown = () => vm.find('[field-id="eks-kubernetes-version"]');
+ const findRegionDropdown = () => vm.find(RegionDropdown);
+ const findKeyPairDropdown = () => vm.find('[field-id="eks-key-pair"]');
+ const findVpcDropdown = () => vm.find('[field-id="eks-vpc"]');
+ const findSubnetDropdown = () => vm.find('[field-id="eks-subnet"]');
+ const findRoleDropdown = () => vm.find('[field-id="eks-role"]');
+ const findSecurityGroupDropdown = () => vm.find('[field-id="eks-security-group"]');
+ const findGitlabManagedClusterCheckbox = () => vm.find(GlFormCheckbox);
+
+ describe('when mounted', () => {
+ it('fetches available regions', () => {
+ expect(regionsActions.fetchItems).toHaveBeenCalled();
+ });
+
+ it('fetches available roles', () => {
+ expect(rolesActions.fetchItems).toHaveBeenCalled();
+ });
+ });
+
+ it('sets isLoadingRoles to RoleDropdown loading property', () => {
+ rolesState.isLoadingItems = true;
+
+ return Vue.nextTick().then(() => {
+ expect(findRoleDropdown().props('loading')).toBe(rolesState.isLoadingItems);
+ });
+ });
+
+ it('sets roles to RoleDropdown items property', () => {
+ expect(findRoleDropdown().props('items')).toBe(rolesState.items);
+ });
+
+ it('sets RoleDropdown hasErrors to true when loading roles failed', () => {
+ rolesState.loadingItemsError = new Error();
+
+ expect(findRoleDropdown().props('hasErrors')).toEqual(true);
+ });
+
+ it('sets isLoadingRegions to RegionDropdown loading property', () => {
+ regionsState.isLoadingItems = true;
+
+ return Vue.nextTick().then(() => {
+ expect(findRegionDropdown().props('loading')).toBe(regionsState.isLoadingItems);
+ });
+ });
+
+ it('sets regions to RegionDropdown regions property', () => {
+ expect(findRegionDropdown().props('regions')).toBe(regionsState.items);
+ });
+
+ it('sets loadingRegionsError to RegionDropdown error property', () => {
+ expect(findRegionDropdown().props('error')).toBe(regionsState.loadingItemsError);
+ });
+
+ it('disables KeyPairDropdown when no region is selected', () => {
+ expect(findKeyPairDropdown().props('disabled')).toBe(true);
+ });
+
+ it('enables KeyPairDropdown when no region is selected', () => {
+ state.selectedRegion = { name: 'west-1 ' };
+
+ return Vue.nextTick().then(() => {
+ expect(findKeyPairDropdown().props('disabled')).toBe(false);
+ });
+ });
+
+ it('sets isLoadingKeyPairs to KeyPairDropdown loading property', () => {
+ keyPairsState.isLoadingItems = true;
+
+ return Vue.nextTick().then(() => {
+ expect(findKeyPairDropdown().props('loading')).toBe(keyPairsState.isLoadingItems);
+ });
+ });
+
+ it('sets keyPairs to KeyPairDropdown items property', () => {
+ expect(findKeyPairDropdown().props('items')).toBe(keyPairsState.items);
+ });
+
+ it('sets KeyPairDropdown hasErrors to true when loading key pairs fails', () => {
+ keyPairsState.loadingItemsError = new Error();
+
+ expect(findKeyPairDropdown().props('hasErrors')).toEqual(true);
+ });
+
+ it('disables VpcDropdown when no region is selected', () => {
+ expect(findVpcDropdown().props('disabled')).toBe(true);
+ });
+
+ it('enables VpcDropdown when no region is selected', () => {
+ state.selectedRegion = { name: 'west-1 ' };
+
+ return Vue.nextTick().then(() => {
+ expect(findVpcDropdown().props('disabled')).toBe(false);
+ });
+ });
+
+ it('sets isLoadingVpcs to VpcDropdown loading property', () => {
+ vpcsState.isLoadingItems = true;
+
+ return Vue.nextTick().then(() => {
+ expect(findVpcDropdown().props('loading')).toBe(vpcsState.isLoadingItems);
+ });
+ });
+
+ it('sets vpcs to VpcDropdown items property', () => {
+ expect(findVpcDropdown().props('items')).toBe(vpcsState.items);
+ });
+
+ it('sets VpcDropdown hasErrors to true when loading vpcs fails', () => {
+ vpcsState.loadingItemsError = new Error();
+
+ expect(findVpcDropdown().props('hasErrors')).toEqual(true);
+ });
+
+ it('disables SubnetDropdown when no vpc is selected', () => {
+ expect(findSubnetDropdown().props('disabled')).toBe(true);
+ });
+
+ it('enables SubnetDropdown when a vpc is selected', () => {
+ state.selectedVpc = { name: 'vpc-1 ' };
+
+ return Vue.nextTick().then(() => {
+ expect(findSubnetDropdown().props('disabled')).toBe(false);
+ });
+ });
+
+ it('sets isLoadingSubnets to SubnetDropdown loading property', () => {
+ subnetsState.isLoadingItems = true;
+
+ return Vue.nextTick().then(() => {
+ expect(findSubnetDropdown().props('loading')).toBe(subnetsState.isLoadingItems);
+ });
+ });
+
+ it('sets subnets to SubnetDropdown items property', () => {
+ expect(findSubnetDropdown().props('items')).toBe(subnetsState.items);
+ });
+
+ it('sets SubnetDropdown hasErrors to true when loading subnets fails', () => {
+ subnetsState.loadingItemsError = new Error();
+
+ expect(findSubnetDropdown().props('hasErrors')).toEqual(true);
+ });
+
+ it('disables SecurityGroupDropdown when no vpc is selected', () => {
+ expect(findSecurityGroupDropdown().props('disabled')).toBe(true);
+ });
+
+ it('enables SecurityGroupDropdown when a vpc is selected', () => {
+ state.selectedVpc = { name: 'vpc-1 ' };
+
+ return Vue.nextTick().then(() => {
+ expect(findSecurityGroupDropdown().props('disabled')).toBe(false);
+ });
+ });
+
+ it('sets isLoadingSecurityGroups to SecurityGroupDropdown loading property', () => {
+ securityGroupsState.isLoadingItems = true;
+
+ return Vue.nextTick().then(() => {
+ expect(findSecurityGroupDropdown().props('loading')).toBe(securityGroupsState.isLoadingItems);
+ });
+ });
+
+ it('sets securityGroups to SecurityGroupDropdown items property', () => {
+ expect(findSecurityGroupDropdown().props('items')).toBe(securityGroupsState.items);
+ });
+
+ it('sets SecurityGroupDropdown hasErrors to true when loading security groups fails', () => {
+ securityGroupsState.loadingItemsError = new Error();
+
+ expect(findSecurityGroupDropdown().props('hasErrors')).toEqual(true);
+ });
+
+ describe('when region is selected', () => {
+ const region = { name: 'us-west-2' };
+
+ beforeEach(() => {
+ findRegionDropdown().vm.$emit('input', region);
+ });
+
+ it('dispatches setRegion action', () => {
+ expect(actions.setRegion).toHaveBeenCalledWith(expect.anything(), { region }, undefined);
+ });
+
+ it('fetches available vpcs', () => {
+ expect(vpcsActions.fetchItems).toHaveBeenCalledWith(expect.anything(), { region }, undefined);
+ });
+
+ it('fetches available key pairs', () => {
+ expect(keyPairsActions.fetchItems).toHaveBeenCalledWith(
+ expect.anything(),
+ { region },
+ undefined,
+ );
+ });
+ });
+
+ it('dispatches setClusterName when cluster name input changes', () => {
+ const clusterName = 'name';
+
+ findClusterNameInput().vm.$emit('input', clusterName);
+
+ expect(actions.setClusterName).toHaveBeenCalledWith(
+ expect.anything(),
+ { clusterName },
+ undefined,
+ );
+ });
+
+ it('dispatches setEnvironmentScope when environment scope input changes', () => {
+ const environmentScope = 'production';
+
+ findEnvironmentScopeInput().vm.$emit('input', environmentScope);
+
+ expect(actions.setEnvironmentScope).toHaveBeenCalledWith(
+ expect.anything(),
+ { environmentScope },
+ undefined,
+ );
+ });
+
+ it('dispatches setKubernetesVersion when kubernetes version dropdown changes', () => {
+ const kubernetesVersion = { name: '1.11' };
+
+ findKubernetesVersionDropdown().vm.$emit('input', kubernetesVersion);
+
+ expect(actions.setKubernetesVersion).toHaveBeenCalledWith(
+ expect.anything(),
+ { kubernetesVersion },
+ undefined,
+ );
+ });
+
+ it('dispatches setGitlabManagedCluster when gitlab managed cluster input changes', () => {
+ const gitlabManagedCluster = false;
+
+ findGitlabManagedClusterCheckbox().vm.$emit('input', gitlabManagedCluster);
+
+ expect(actions.setGitlabManagedCluster).toHaveBeenCalledWith(
+ expect.anything(),
+ { gitlabManagedCluster },
+ undefined,
+ );
+ });
+
+ describe('when vpc is selected', () => {
+ const vpc = { name: 'vpc-1' };
+
+ beforeEach(() => {
+ findVpcDropdown().vm.$emit('input', vpc);
+ });
+
+ it('dispatches setVpc action', () => {
+ expect(actions.setVpc).toHaveBeenCalledWith(expect.anything(), { vpc }, undefined);
+ });
+
+ it('dispatches fetchSubnets action', () => {
+ expect(subnetsActions.fetchItems).toHaveBeenCalledWith(expect.anything(), { vpc }, undefined);
+ });
+
+ it('dispatches fetchSecurityGroups action', () => {
+ expect(securityGroupsActions.fetchItems).toHaveBeenCalledWith(
+ expect.anything(),
+ { vpc },
+ undefined,
+ );
+ });
+ });
+
+ describe('when a subnet is selected', () => {
+ const subnet = { name: 'subnet-1' };
+
+ beforeEach(() => {
+ findSubnetDropdown().vm.$emit('input', subnet);
+ });
+
+ it('dispatches setSubnet action', () => {
+ expect(actions.setSubnet).toHaveBeenCalledWith(expect.anything(), { subnet }, undefined);
+ });
+ });
+
+ describe('when role is selected', () => {
+ const role = { name: 'admin' };
+
+ beforeEach(() => {
+ findRoleDropdown().vm.$emit('input', role);
+ });
+
+ it('dispatches setRole action', () => {
+ expect(actions.setRole).toHaveBeenCalledWith(expect.anything(), { role }, undefined);
+ });
+ });
+
+ describe('when key pair is selected', () => {
+ const keyPair = { name: 'key pair' };
+
+ beforeEach(() => {
+ findKeyPairDropdown().vm.$emit('input', keyPair);
+ });
+
+ it('dispatches setKeyPair action', () => {
+ expect(actions.setKeyPair).toHaveBeenCalledWith(expect.anything(), { keyPair }, undefined);
+ });
+ });
+
+ describe('when security group is selected', () => {
+ const securityGroup = { name: 'default group' };
+
+ beforeEach(() => {
+ findSecurityGroupDropdown().vm.$emit('input', securityGroup);
+ });
+
+ it('dispatches setSecurityGroup action', () => {
+ expect(actions.setSecurityGroup).toHaveBeenCalledWith(
+ expect.anything(),
+ { securityGroup },
+ undefined,
+ );
+ });
+ });
+});
diff --git a/spec/frontend/create_cluster/eks_cluster/components/region_dropdown_spec.js b/spec/frontend/create_cluster/eks_cluster/components/region_dropdown_spec.js
new file mode 100644
index 00000000000..0ebb5026a4b
--- /dev/null
+++ b/spec/frontend/create_cluster/eks_cluster/components/region_dropdown_spec.js
@@ -0,0 +1,55 @@
+import { shallowMount } from '@vue/test-utils';
+
+import ClusterFormDropdown from '~/create_cluster/eks_cluster/components/cluster_form_dropdown.vue';
+import RegionDropdown from '~/create_cluster/eks_cluster/components/region_dropdown.vue';
+
+describe('RegionDropdown', () => {
+ let vm;
+
+ const getClusterFormDropdown = () => vm.find(ClusterFormDropdown);
+
+ beforeEach(() => {
+ vm = shallowMount(RegionDropdown);
+ });
+ afterEach(() => vm.destroy());
+
+ it('renders a cluster-form-dropdown', () => {
+ expect(getClusterFormDropdown().exists()).toBe(true);
+ });
+
+ it('sets regions to cluster-form-dropdown items property', () => {
+ const regions = [{ name: 'basic' }];
+
+ vm.setProps({ regions });
+
+ expect(getClusterFormDropdown().props('items')).toEqual(regions);
+ });
+
+ it('sets a loading text', () => {
+ expect(getClusterFormDropdown().props('loadingText')).toEqual('Loading Regions');
+ });
+
+ it('sets a placeholder', () => {
+ expect(getClusterFormDropdown().props('placeholder')).toEqual('Select a region');
+ });
+
+ it('sets an empty results text', () => {
+ expect(getClusterFormDropdown().props('emptyText')).toEqual('No region found');
+ });
+
+ it('sets a search field placeholder', () => {
+ expect(getClusterFormDropdown().props('searchFieldPlaceholder')).toEqual('Search regions');
+ });
+
+ it('sets hasErrors property', () => {
+ vm.setProps({ error: {} });
+
+ expect(getClusterFormDropdown().props('hasErrors')).toEqual(true);
+ });
+
+ it('sets an error message', () => {
+ expect(getClusterFormDropdown().props('errorMessage')).toEqual(
+ 'Could not load regions from your AWS account',
+ );
+ });
+});
diff --git a/spec/frontend/create_cluster/eks_cluster/components/role_name_dropdown_spec.js b/spec/frontend/create_cluster/eks_cluster/components/role_name_dropdown_spec.js
deleted file mode 100644
index 657637c1b56..00000000000
--- a/spec/frontend/create_cluster/eks_cluster/components/role_name_dropdown_spec.js
+++ /dev/null
@@ -1,43 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-
-import ClusterFormDropdown from '~/create_cluster/eks_cluster/components/cluster_form_dropdown.vue';
-import RoleNameDropdown from '~/create_cluster/eks_cluster/components/role_name_dropdown.vue';
-
-describe('RoleNameDropdown', () => {
- let vm;
-
- beforeEach(() => {
- vm = shallowMount(RoleNameDropdown);
- });
- afterEach(() => vm.destroy());
-
- it('renders a cluster-form-dropdown', () => {
- expect(vm.find(ClusterFormDropdown).exists()).toBe(true);
- });
-
- it('sets roles to cluster-form-dropdown items property', () => {
- const roles = [{ name: 'basic' }];
-
- vm.setProps({ roles });
-
- expect(vm.find(ClusterFormDropdown).props('items')).toEqual(roles);
- });
-
- it('sets a loading text', () => {
- expect(vm.find(ClusterFormDropdown).props('loadingText')).toEqual('Loading IAM Roles');
- });
-
- it('sets a placeholder', () => {
- expect(vm.find(ClusterFormDropdown).props('placeholder')).toEqual('Select role name');
- });
-
- it('sets an empty results text', () => {
- expect(vm.find(ClusterFormDropdown).props('emptyText')).toEqual('No IAM Roles found');
- });
-
- it('sets a search field placeholder', () => {
- expect(vm.find(ClusterFormDropdown).props('searchFieldPlaceholder')).toEqual(
- 'Search IAM Roles',
- );
- });
-});
diff --git a/spec/frontend/create_cluster/eks_cluster/store/actions_spec.js b/spec/frontend/create_cluster/eks_cluster/store/actions_spec.js
new file mode 100644
index 00000000000..1ed7f806804
--- /dev/null
+++ b/spec/frontend/create_cluster/eks_cluster/store/actions_spec.js
@@ -0,0 +1,60 @@
+import testAction from 'helpers/vuex_action_helper';
+
+import createState from '~/create_cluster/eks_cluster/store/state';
+import * as actions from '~/create_cluster/eks_cluster/store/actions';
+import {
+ SET_CLUSTER_NAME,
+ SET_ENVIRONMENT_SCOPE,
+ SET_KUBERNETES_VERSION,
+ SET_REGION,
+ SET_VPC,
+ SET_KEY_PAIR,
+ SET_SUBNET,
+ SET_ROLE,
+ SET_SECURITY_GROUP,
+ SET_GITLAB_MANAGED_CLUSTER,
+} from '~/create_cluster/eks_cluster/store/mutation_types';
+
+describe('EKS Cluster Store Actions', () => {
+ let clusterName;
+ let environmentScope;
+ let kubernetesVersion;
+ let region;
+ let vpc;
+ let subnet;
+ let role;
+ let keyPair;
+ let securityGroup;
+ let gitlabManagedCluster;
+
+ beforeEach(() => {
+ clusterName = 'my cluster';
+ environmentScope = 'production';
+ kubernetesVersion = '11.1';
+ region = { name: 'regions-1' };
+ vpc = { name: 'vpc-1' };
+ subnet = { name: 'subnet-1' };
+ role = { name: 'role-1' };
+ keyPair = { name: 'key-pair-1' };
+ securityGroup = { name: 'default group' };
+ gitlabManagedCluster = true;
+ });
+
+ it.each`
+ action | mutation | payload | payloadDescription
+ ${'setClusterName'} | ${SET_CLUSTER_NAME} | ${{ clusterName }} | ${'cluster name'}
+ ${'setEnvironmentScope'} | ${SET_ENVIRONMENT_SCOPE} | ${{ environmentScope }} | ${'environment scope'}
+ ${'setKubernetesVersion'} | ${SET_KUBERNETES_VERSION} | ${{ kubernetesVersion }} | ${'kubernetes version'}
+ ${'setRole'} | ${SET_ROLE} | ${{ role }} | ${'role'}
+ ${'setRegion'} | ${SET_REGION} | ${{ region }} | ${'region'}
+ ${'setKeyPair'} | ${SET_KEY_PAIR} | ${{ keyPair }} | ${'key pair'}
+ ${'setVpc'} | ${SET_VPC} | ${{ vpc }} | ${'vpc'}
+ ${'setSubnet'} | ${SET_SUBNET} | ${{ subnet }} | ${'subnet'}
+ ${'setSecurityGroup'} | ${SET_SECURITY_GROUP} | ${{ securityGroup }} | ${'securityGroup'}
+ ${'setGitlabManagedCluster'} | ${SET_GITLAB_MANAGED_CLUSTER} | ${gitlabManagedCluster} | ${'gitlab managed cluster'}
+ `(`$action commits $mutation with $payloadDescription payload`, data => {
+ const { action, mutation, payload } = data;
+
+ testAction(actions[action], payload, createState(), [{ type: mutation, payload }]);
+ });
+});
diff --git a/spec/frontend/create_cluster/eks_cluster/store/cluster_dropdown/actions_spec.js b/spec/frontend/create_cluster/eks_cluster/store/cluster_dropdown/actions_spec.js
new file mode 100644
index 00000000000..58f8855a64c
--- /dev/null
+++ b/spec/frontend/create_cluster/eks_cluster/store/cluster_dropdown/actions_spec.js
@@ -0,0 +1,95 @@
+import testAction from 'helpers/vuex_action_helper';
+
+import createState from '~/create_cluster/eks_cluster/store/cluster_dropdown/state';
+import * as types from '~/create_cluster/eks_cluster/store/cluster_dropdown/mutation_types';
+import actionsFactory from '~/create_cluster/eks_cluster/store/cluster_dropdown/actions';
+
+describe('Cluster dropdown Store Actions', () => {
+ const items = [{ name: 'item 1' }];
+ let fetchFn;
+ let actions;
+
+ beforeEach(() => {
+ fetchFn = jest.fn();
+ actions = actionsFactory(fetchFn);
+ });
+
+ describe('fetchItems', () => {
+ describe('on success', () => {
+ beforeEach(() => {
+ fetchFn.mockResolvedValueOnce(items);
+ actions = actionsFactory(fetchFn);
+ });
+
+ it('dispatches success with received items', () =>
+ testAction(
+ actions.fetchItems,
+ null,
+ createState(),
+ [],
+ [
+ { type: 'requestItems' },
+ {
+ type: 'receiveItemsSuccess',
+ payload: { items },
+ },
+ ],
+ ));
+ });
+
+ describe('on failure', () => {
+ const error = new Error('Could not fetch items');
+
+ beforeEach(() => {
+ fetchFn.mockRejectedValueOnce(error);
+ });
+
+ it('dispatches success with received items', () =>
+ testAction(
+ actions.fetchItems,
+ null,
+ createState(),
+ [],
+ [
+ { type: 'requestItems' },
+ {
+ type: 'receiveItemsError',
+ payload: { error },
+ },
+ ],
+ ));
+ });
+ });
+
+ describe('requestItems', () => {
+ it(`commits ${types.REQUEST_ITEMS} mutation`, () =>
+ testAction(actions.requestItems, null, createState(), [{ type: types.REQUEST_ITEMS }]));
+ });
+
+ describe('receiveItemsSuccess', () => {
+ it(`commits ${types.RECEIVE_ITEMS_SUCCESS} mutation`, () =>
+ testAction(actions.receiveItemsSuccess, { items }, createState(), [
+ {
+ type: types.RECEIVE_ITEMS_SUCCESS,
+ payload: {
+ items,
+ },
+ },
+ ]));
+ });
+
+ describe('receiveItemsError', () => {
+ it(`commits ${types.RECEIVE_ITEMS_ERROR} mutation`, () => {
+ const error = new Error('Error fetching items');
+
+ testAction(actions.receiveItemsError, { error }, createState(), [
+ {
+ type: types.RECEIVE_ITEMS_ERROR,
+ payload: {
+ error,
+ },
+ },
+ ]);
+ });
+ });
+});
diff --git a/spec/frontend/create_cluster/eks_cluster/store/cluster_dropdown/mutations_spec.js b/spec/frontend/create_cluster/eks_cluster/store/cluster_dropdown/mutations_spec.js
new file mode 100644
index 00000000000..0665047edea
--- /dev/null
+++ b/spec/frontend/create_cluster/eks_cluster/store/cluster_dropdown/mutations_spec.js
@@ -0,0 +1,36 @@
+import {
+ REQUEST_ITEMS,
+ RECEIVE_ITEMS_SUCCESS,
+ RECEIVE_ITEMS_ERROR,
+} from '~/create_cluster/eks_cluster/store/cluster_dropdown/mutation_types';
+import createState from '~/create_cluster/eks_cluster/store/cluster_dropdown/state';
+import mutations from '~/create_cluster/eks_cluster/store/cluster_dropdown/mutations';
+
+describe('Cluster dropdown store mutations', () => {
+ let state;
+ let emptyPayload;
+ let items;
+ let error;
+
+ beforeEach(() => {
+ emptyPayload = {};
+ items = [{ name: 'item 1' }];
+ error = new Error('could not load error');
+ state = createState();
+ });
+
+ it.each`
+ mutation | mutatedProperty | payload | expectedValue | expectedValueDescription
+ ${REQUEST_ITEMS} | ${'isLoadingItems'} | ${emptyPayload} | ${true} | ${true}
+ ${REQUEST_ITEMS} | ${'loadingItemsError'} | ${emptyPayload} | ${null} | ${null}
+ ${RECEIVE_ITEMS_SUCCESS} | ${'isLoadingItems'} | ${{ items }} | ${false} | ${false}
+ ${RECEIVE_ITEMS_SUCCESS} | ${'items'} | ${{ items }} | ${items} | ${'items payload'}
+ ${RECEIVE_ITEMS_ERROR} | ${'isLoadingItems'} | ${{ error }} | ${false} | ${false}
+ ${RECEIVE_ITEMS_ERROR} | ${'error'} | ${{ error }} | ${error} | ${'received error object'}
+ `(`$mutation sets $mutatedProperty to $expectedValueDescription`, data => {
+ const { mutation, mutatedProperty, payload, expectedValue } = data;
+
+ mutations[mutation](state, payload);
+ expect(state[mutatedProperty]).toBe(expectedValue);
+ });
+});
diff --git a/spec/frontend/create_cluster/eks_cluster/store/mutations_spec.js b/spec/frontend/create_cluster/eks_cluster/store/mutations_spec.js
new file mode 100644
index 00000000000..81b65180fb5
--- /dev/null
+++ b/spec/frontend/create_cluster/eks_cluster/store/mutations_spec.js
@@ -0,0 +1,62 @@
+import {
+ SET_CLUSTER_NAME,
+ SET_ENVIRONMENT_SCOPE,
+ SET_KUBERNETES_VERSION,
+ SET_REGION,
+ SET_VPC,
+ SET_KEY_PAIR,
+ SET_SUBNET,
+ SET_ROLE,
+ SET_SECURITY_GROUP,
+ SET_GITLAB_MANAGED_CLUSTER,
+} from '~/create_cluster/eks_cluster/store/mutation_types';
+import createState from '~/create_cluster/eks_cluster/store/state';
+import mutations from '~/create_cluster/eks_cluster/store/mutations';
+
+describe('Create EKS cluster store mutations', () => {
+ let clusterName;
+ let environmentScope;
+ let kubernetesVersion;
+ let state;
+ let region;
+ let vpc;
+ let subnet;
+ let role;
+ let keyPair;
+ let securityGroup;
+ let gitlabManagedCluster;
+
+ beforeEach(() => {
+ clusterName = 'my cluster';
+ environmentScope = 'production';
+ kubernetesVersion = '11.1';
+ region = { name: 'regions-1' };
+ vpc = { name: 'vpc-1' };
+ subnet = { name: 'subnet-1' };
+ role = { name: 'role-1' };
+ keyPair = { name: 'key pair' };
+ securityGroup = { name: 'default group' };
+ gitlabManagedCluster = false;
+
+ state = createState();
+ });
+
+ it.each`
+ mutation | mutatedProperty | payload | expectedValue | expectedValueDescription
+ ${SET_CLUSTER_NAME} | ${'clusterName'} | ${{ clusterName }} | ${clusterName} | ${'cluster name'}
+ ${SET_ENVIRONMENT_SCOPE} | ${'environmentScope'} | ${{ environmentScope }} | ${environmentScope} | ${'environment scope'}
+ ${SET_KUBERNETES_VERSION} | ${'kubernetesVersion'} | ${{ kubernetesVersion }} | ${kubernetesVersion} | ${'kubernetes version'}
+ ${SET_ROLE} | ${'selectedRole'} | ${{ role }} | ${role} | ${'selected role payload'}
+ ${SET_REGION} | ${'selectedRegion'} | ${{ region }} | ${region} | ${'selected region payload'}
+ ${SET_KEY_PAIR} | ${'selectedKeyPair'} | ${{ keyPair }} | ${keyPair} | ${'selected key pair payload'}
+ ${SET_VPC} | ${'selectedVpc'} | ${{ vpc }} | ${vpc} | ${'selected vpc payload'}
+ ${SET_SUBNET} | ${'selectedSubnet'} | ${{ subnet }} | ${subnet} | ${'selected sybnet payload'}
+ ${SET_SECURITY_GROUP} | ${'selectedSecurityGroup'} | ${{ securityGroup }} | ${securityGroup} | ${'selected security group payload'}
+ ${SET_GITLAB_MANAGED_CLUSTER} | ${'gitlabManagedCluster'} | ${{ gitlabManagedCluster }} | ${gitlabManagedCluster} | ${'gitlab managed cluster'}
+ `(`$mutation sets $mutatedProperty to $expectedValueDescription`, data => {
+ const { mutation, mutatedProperty, payload, expectedValue } = data;
+
+ mutations[mutation](state, payload);
+ expect(state[mutatedProperty]).toBe(expectedValue);
+ });
+});
diff --git a/spec/frontend/error_tracking/utils_spec.js b/spec/frontend/error_tracking/utils_spec.js
new file mode 100644
index 00000000000..0e9047cd375
--- /dev/null
+++ b/spec/frontend/error_tracking/utils_spec.js
@@ -0,0 +1,27 @@
+import * as errorTrackingUtils from '~/error_tracking/utils';
+
+const externalUrl = 'https://sentry.io/organizations/test-sentry-nk/issues/1/?project=1';
+
+describe('Error Tracking Events', () => {
+ describe('trackViewInSentryOptions', () => {
+ it('should return correct event options', () => {
+ expect(errorTrackingUtils.trackViewInSentryOptions(externalUrl)).toEqual({
+ category: 'Error Tracking',
+ action: 'click_view_in_sentry',
+ label: 'External Url',
+ property: externalUrl,
+ });
+ });
+ });
+
+ describe('trackClickErrorLinkToSentryOptions', () => {
+ it('should return correct event options', () => {
+ expect(errorTrackingUtils.trackClickErrorLinkToSentryOptions(externalUrl)).toEqual({
+ category: 'Error Tracking',
+ action: 'click_error_link_to_sentry',
+ label: 'Error Link',
+ property: externalUrl,
+ });
+ });
+ });
+});
diff --git a/spec/frontend/fixtures/abuse_reports.rb b/spec/frontend/fixtures/abuse_reports.rb
index 21356390cae..712ed2e8d7e 100644
--- a/spec/frontend/fixtures/abuse_reports.rb
+++ b/spec/frontend/fixtures/abuse_reports.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Admin::AbuseReportsController, '(JavaScript fixtures)', type: :controller do
diff --git a/spec/frontend/fixtures/admin_users.rb b/spec/frontend/fixtures/admin_users.rb
index 0209594dadc..b0f7d69f091 100644
--- a/spec/frontend/fixtures/admin_users.rb
+++ b/spec/frontend/fixtures/admin_users.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Admin::UsersController, '(JavaScript fixtures)', type: :controller do
diff --git a/spec/frontend/fixtures/application_settings.rb b/spec/frontend/fixtures/application_settings.rb
index afe5949ed3b..a16888d8f03 100644
--- a/spec/frontend/fixtures/application_settings.rb
+++ b/spec/frontend/fixtures/application_settings.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Admin::ApplicationSettingsController, '(JavaScript fixtures)', type: :controller do
diff --git a/spec/frontend/fixtures/autocomplete_sources.rb b/spec/frontend/fixtures/autocomplete_sources.rb
index 9e04328e2b9..382eff02b0f 100644
--- a/spec/frontend/fixtures/autocomplete_sources.rb
+++ b/spec/frontend/fixtures/autocomplete_sources.rb
@@ -24,6 +24,10 @@ describe Projects::AutocompleteSourcesController, '(JavaScript fixtures)', type:
create(:label, project: project, title: 'feature')
create(:label, project: project, title: 'documentation')
+ create(:label, project: project, title: 'P1')
+ create(:label, project: project, title: 'P2')
+ create(:label, project: project, title: 'P3')
+ create(:label, project: project, title: 'P4')
get :labels,
format: :json,
diff --git a/spec/frontend/fixtures/blob.rb b/spec/frontend/fixtures/blob.rb
index ce5030efbf8..28a3badaa17 100644
--- a/spec/frontend/fixtures/blob.rb
+++ b/spec/frontend/fixtures/blob.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Projects::BlobController, '(JavaScript fixtures)', type: :controller do
diff --git a/spec/frontend/fixtures/boards.rb b/spec/frontend/fixtures/boards.rb
index f257d80390f..b3c7865a088 100644
--- a/spec/frontend/fixtures/boards.rb
+++ b/spec/frontend/fixtures/boards.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Projects::BoardsController, '(JavaScript fixtures)', type: :controller do
diff --git a/spec/frontend/fixtures/branches.rb b/spec/frontend/fixtures/branches.rb
index 197fe42c52a..2dc8cde625a 100644
--- a/spec/frontend/fixtures/branches.rb
+++ b/spec/frontend/fixtures/branches.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Projects::BranchesController, '(JavaScript fixtures)', type: :controller do
diff --git a/spec/frontend/fixtures/clusters.rb b/spec/frontend/fixtures/clusters.rb
index f15ef010807..fd64d3c0e28 100644
--- a/spec/frontend/fixtures/clusters.rb
+++ b/spec/frontend/fixtures/clusters.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Projects::ClustersController, '(JavaScript fixtures)', type: :controller do
diff --git a/spec/frontend/fixtures/commit.rb b/spec/frontend/fixtures/commit.rb
index a328c455356..2c4bf6fbd3d 100644
--- a/spec/frontend/fixtures/commit.rb
+++ b/spec/frontend/fixtures/commit.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Projects::CommitController, '(JavaScript fixtures)', type: :controller do
diff --git a/spec/frontend/fixtures/deploy_keys.rb b/spec/frontend/fixtures/deploy_keys.rb
index fca233c6f59..f491c424bcf 100644
--- a/spec/frontend/fixtures/deploy_keys.rb
+++ b/spec/frontend/fixtures/deploy_keys.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Projects::DeployKeysController, '(JavaScript fixtures)', type: :controller do
diff --git a/spec/frontend/fixtures/groups.rb b/spec/frontend/fixtures/groups.rb
index c1bb2d43332..237fc711594 100644
--- a/spec/frontend/fixtures/groups.rb
+++ b/spec/frontend/fixtures/groups.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe 'Groups (JavaScript fixtures)', type: :controller do
diff --git a/spec/frontend/fixtures/issues.rb b/spec/frontend/fixtures/issues.rb
index b5eb38e0023..7e524990863 100644
--- a/spec/frontend/fixtures/issues.rb
+++ b/spec/frontend/fixtures/issues.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Projects::IssuesController, '(JavaScript fixtures)', type: :controller do
diff --git a/spec/frontend/fixtures/jobs.rb b/spec/frontend/fixtures/jobs.rb
index a3a7759c85b..787ab517f75 100644
--- a/spec/frontend/fixtures/jobs.rb
+++ b/spec/frontend/fixtures/jobs.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Projects::JobsController, '(JavaScript fixtures)', type: :controller do
diff --git a/spec/frontend/fixtures/labels.rb b/spec/frontend/fixtures/labels.rb
index a312287970f..e4d66dbcd0a 100644
--- a/spec/frontend/fixtures/labels.rb
+++ b/spec/frontend/fixtures/labels.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe 'Labels (JavaScript fixtures)' do
diff --git a/spec/frontend/fixtures/merge_requests.rb b/spec/frontend/fixtures/merge_requests.rb
index 88706e96676..8fbdb534b3d 100644
--- a/spec/frontend/fixtures/merge_requests.rb
+++ b/spec/frontend/fixtures/merge_requests.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Projects::MergeRequestsController, '(JavaScript fixtures)', type: :controller do
diff --git a/spec/frontend/fixtures/merge_requests_diffs.rb b/spec/frontend/fixtures/merge_requests_diffs.rb
index b633a0495a6..9493cba03bb 100644
--- a/spec/frontend/fixtures/merge_requests_diffs.rb
+++ b/spec/frontend/fixtures/merge_requests_diffs.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Projects::MergeRequests::DiffsController, '(JavaScript fixtures)', type: :controller do
diff --git a/spec/frontend/fixtures/pipeline_schedules.rb b/spec/frontend/fixtures/pipeline_schedules.rb
index a70091a3919..e00a35d5362 100644
--- a/spec/frontend/fixtures/pipeline_schedules.rb
+++ b/spec/frontend/fixtures/pipeline_schedules.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Projects::PipelineSchedulesController, '(JavaScript fixtures)', type: :controller do
diff --git a/spec/frontend/fixtures/pipelines.rb b/spec/frontend/fixtures/pipelines.rb
index ed57eb0aa80..83fc13af7d3 100644
--- a/spec/frontend/fixtures/pipelines.rb
+++ b/spec/frontend/fixtures/pipelines.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Projects::PipelinesController, '(JavaScript fixtures)', type: :controller do
diff --git a/spec/frontend/fixtures/projects.rb b/spec/frontend/fixtures/projects.rb
index 91e3b65215a..af5b70fbbeb 100644
--- a/spec/frontend/fixtures/projects.rb
+++ b/spec/frontend/fixtures/projects.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe 'Projects (JavaScript fixtures)', type: :controller do
diff --git a/spec/frontend/fixtures/prometheus_service.rb b/spec/frontend/fixtures/prometheus_service.rb
index 93ee81120d7..c404b8260d2 100644
--- a/spec/frontend/fixtures/prometheus_service.rb
+++ b/spec/frontend/fixtures/prometheus_service.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Projects::ServicesController, '(JavaScript fixtures)', type: :controller do
diff --git a/spec/frontend/fixtures/raw.rb b/spec/frontend/fixtures/raw.rb
index 801c80a0112..9c9fa4ec40b 100644
--- a/spec/frontend/fixtures/raw.rb
+++ b/spec/frontend/fixtures/raw.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe 'Raw files', '(JavaScript fixtures)' do
diff --git a/spec/frontend/fixtures/search.rb b/spec/frontend/fixtures/search.rb
index c26c6998ae9..025cc53c745 100644
--- a/spec/frontend/fixtures/search.rb
+++ b/spec/frontend/fixtures/search.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe SearchController, '(JavaScript fixtures)', type: :controller do
diff --git a/spec/frontend/fixtures/services.rb b/spec/frontend/fixtures/services.rb
index ee1e088f158..1b81a83ca49 100644
--- a/spec/frontend/fixtures/services.rb
+++ b/spec/frontend/fixtures/services.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Projects::ServicesController, '(JavaScript fixtures)', type: :controller do
diff --git a/spec/frontend/fixtures/sessions.rb b/spec/frontend/fixtures/sessions.rb
index 18574ea06b5..a4dc0aef79c 100644
--- a/spec/frontend/fixtures/sessions.rb
+++ b/spec/frontend/fixtures/sessions.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe 'Sessions (JavaScript fixtures)' do
diff --git a/spec/frontend/fixtures/snippet.rb b/spec/frontend/fixtures/snippet.rb
index 23bcdb47ac6..34a6fade9c9 100644
--- a/spec/frontend/fixtures/snippet.rb
+++ b/spec/frontend/fixtures/snippet.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe SnippetsController, '(JavaScript fixtures)', type: :controller do
diff --git a/spec/frontend/fixtures/static/environments_logs.html b/spec/frontend/fixtures/static/environments_logs.html
index 6179d3dbc23..ccf9c364154 100644
--- a/spec/frontend/fixtures/static/environments_logs.html
+++ b/spec/frontend/fixtures/static/environments_logs.html
@@ -1,29 +1,102 @@
-<div class="js-kubernetes-logs" data-logs-path="/root/kubernetes-app/environments/1/logs">
- <div class="build-page">
+<div
+ class="js-kubernetes-logs"
+ data-current-environment-name="production"
+ data-environments-path="/root/my-project/environments.json"
+ data-logs-page="/root/my-project/environments/1/logs"
+ data-logs-path="/root/my-project/environments/1/logs.json"
+>
+ <div class="build-page-pod-logs">
<div class="build-trace-container prepend-top-default">
- <div class="top-bar js-top-bar">
- <div class="truncated-info hidden-xs pull-left"></div>
- <div class="dropdown prepend-left-10 js-pod-dropdown">
- <button aria-expanded="false" class="dropdown-menu-toggle" data-toggle="dropdown" type="button">
- <i class="fa fa-chevron-down"></i>
- </button>
- <div class="dropdown-menu dropdown-menu-selectable dropdown-menu-drop-up"></div>
+ <div class="top-bar js-top-bar d-flex">
+ <div class="row">
+ <div class="form-group col-6" role="group">
+ <label class="d-block col-form-label-sm col-form-label">
+ Environment
+ </label>
+ <div class="dropdown js-environment-dropdown d-flex">
+ <button
+ aria-expanded="false"
+ class="dropdown-menu-toggle d-flex align-content-center align-self-center"
+ data-toggle="dropdown"
+ type="button"
+ >
+ <i aria-hidden="true" data-hidden="true" class="fa fa-chevron-down"></i>
+ <div class="dropdown-toggle-text">
+ &nbsp;
+ </div>
+ </button>
+ <div class="dropdown-menu dropdown-menu-selectable dropdown-menu-drop-up"></div>
+ </div>
+ </div>
+ <div class="form-group col-6" role="group">
+ <label class="d-block col-form-label-sm col-form-label">
+ Pod logs from
+ </label>
+ <div class="dropdown js-pod-dropdown d-flex">
+ <button
+ aria-expanded="false"
+ class="dropdown-menu-toggle d-flex align-content-center align-self-center"
+ data-toggle="dropdown"
+ type="button"
+ >
+ <i aria-hidden="true" data-hidden="true" class="fa fa-chevron-down"></i>
+ <div class="dropdown-toggle-text">
+ &nbsp;
+ </div>
+ </button>
+ <div class="dropdown-menu dropdown-menu-selectable dropdown-menu-drop-up"></div>
+ </div>
+ </div>
</div>
- <div class="controllers pull-right">
- <div class="has-tooltip controllers-buttons" data-container="body" data-placement="top" title="Scroll to top">
- <button class="js-scroll-up btn-scroll btn-transparent btn-blank" disabled type="button"></button>
+ <div class="controllers align-self-end">
+ <div
+ class="has-tooltip controllers-buttons"
+ data-container="body"
+ data-placement="top"
+ title="Scroll to top"
+ >
+ <button
+ class="js-scroll-up btn-scroll btn-transparent btn-blank"
+ disabled
+ type="button"
+ ></button>
</div>
- <div class="has-tooltip controllers-buttons" data-container="body" data-placement="top" title="Scroll to bottom">
- <button class="js-scroll-down btn-scroll btn-transparent btn-blank" disabled type="button"></button>
+ <div
+ class="has-tooltip controllers-buttons"
+ data-container="body"
+ data-placement="top"
+ title="Scroll to bottom"
+ >
+ <button
+ class="js-scroll-down btn-scroll btn-transparent btn-blank"
+ disabled
+ type="button"
+ ></button>
</div>
- <div class="refresh-control pull-right">
- <div class="has-tooltip controllers-buttons" data-container="body" data-placement="top" title="Refresh">
- <button class="js-refresh-log btn-default btn-refresh" disabled type="button"></button>
+ <div class="refresh-control">
+ <div
+ class="has-tooltip controllers-buttons"
+ data-container="body"
+ data-placement="top"
+ title="Refresh"
+ >
+ <button
+ class="js-refresh-log btn btn-default btn-refresh h-32-px"
+ disabled
+ type="button"
+ ></button>
</div>
</div>
</div>
</div>
- <pre class="build-trace" id="build-trace"><code class="bash js-build-output"><div class="build-loader-animation js-build-refresh"></div></code></pre>
+ <pre class="build-trace" id="build-trace">
+ <code class="bash js-build-output"></code>
+ <div class="build-loader-animation js-build-refresh">
+ <div class="dot"></div>
+ <div class="dot"></div>
+ <div class="dot"></div>
+ </div>
+ </pre>
</div>
</div>
</div>
diff --git a/spec/frontend/fixtures/todos.rb b/spec/frontend/fixtures/todos.rb
index a7c183d2414..e5bdb4998ed 100644
--- a/spec/frontend/fixtures/todos.rb
+++ b/spec/frontend/fixtures/todos.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe 'Todos (JavaScript fixtures)' do
diff --git a/spec/frontend/fixtures/u2f.rb b/spec/frontend/fixtures/u2f.rb
index 8ecbc0390cd..dded6ce6380 100644
--- a/spec/frontend/fixtures/u2f.rb
+++ b/spec/frontend/fixtures/u2f.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
context 'U2F' do
diff --git a/spec/frontend/helpers/dom_shims/README.md b/spec/frontend/helpers/dom_shims/README.md
new file mode 100644
index 00000000000..1105e4b0c4c
--- /dev/null
+++ b/spec/frontend/helpers/dom_shims/README.md
@@ -0,0 +1,12 @@
+## Jest DOM shims
+
+This is where we shim parts of JSDom. It is imported in our root `test_setup.js`.
+
+### Why do we need this?
+
+Since JSDom mocks a real DOM environment (which is a good thing), it
+unfortunately does not support some jQuery matchers.
+
+### References
+
+- https://gitlab.com/gitlab-org/gitlab/merge_requests/17906#note_224448120
diff --git a/spec/frontend/helpers/dom_shims/get_client_rects.js b/spec/frontend/helpers/dom_shims/get_client_rects.js
new file mode 100644
index 00000000000..d740c1bf154
--- /dev/null
+++ b/spec/frontend/helpers/dom_shims/get_client_rects.js
@@ -0,0 +1,50 @@
+function hasHiddenStyle(node) {
+ if (!node.style) {
+ return false;
+ } else if (node.style.display === 'none' || node.style.visibility === 'hidden') {
+ return true;
+ }
+
+ return false;
+}
+
+function createDefaultClientRect() {
+ return {
+ bottom: 0,
+ height: 0,
+ left: 0,
+ right: 0,
+ top: 0,
+ width: 0,
+ x: 0,
+ y: 0,
+ };
+}
+
+/**
+ * This is needed to get the `toBeVisible` matcher to work in `jsdom`
+ *
+ * Reference:
+ * - https://github.com/jsdom/jsdom/issues/1322
+ * - https://github.com/unindented/custom-jquery-matchers/blob/v2.1.0/packages/custom-jquery-matchers/src/matchers.js#L157
+ */
+window.Element.prototype.getClientRects = function getClientRects() {
+ let node = this;
+
+ while (node) {
+ if (node === document) {
+ break;
+ }
+
+ if (hasHiddenStyle(node)) {
+ return [];
+ }
+ node = node.parentNode;
+ }
+
+ if (!node) {
+ return [];
+ }
+
+ return [createDefaultClientRect()];
+};
diff --git a/spec/frontend/helpers/dom_shims/get_client_rects_spec.js b/spec/frontend/helpers/dom_shims/get_client_rects_spec.js
new file mode 100644
index 00000000000..e7b8f1e235b
--- /dev/null
+++ b/spec/frontend/helpers/dom_shims/get_client_rects_spec.js
@@ -0,0 +1,71 @@
+const createTestElement = () => {
+ const element = document.createElement('div');
+
+ element.textContent = 'Hello World!';
+
+ return element;
+};
+
+describe('DOM patch for getClientRects', () => {
+ let origHtml;
+ let el;
+
+ beforeEach(() => {
+ origHtml = document.body.innerHTML;
+ el = createTestElement();
+ });
+
+ afterEach(() => {
+ document.body.innerHTML = origHtml;
+ });
+
+ describe('toBeVisible matcher', () => {
+ describe('when not attached to document', () => {
+ it('does not match', () => {
+ expect(el).not.toBeVisible();
+ });
+ });
+
+ describe('when attached to document', () => {
+ beforeEach(() => {
+ document.body.appendChild(el);
+ });
+
+ it('matches', () => {
+ expect(el).toBeVisible();
+ });
+ });
+
+ describe('with parent and attached to document', () => {
+ let parentEl;
+
+ beforeEach(() => {
+ parentEl = createTestElement();
+ parentEl.appendChild(el);
+ document.body.appendChild(parentEl);
+ });
+
+ it('matches', () => {
+ expect(el).toBeVisible();
+ });
+
+ describe.each`
+ style
+ ${{ display: 'none' }}
+ ${{ visibility: 'hidden' }}
+ `('with style $style', ({ style }) => {
+ it('does not match when applied to element', () => {
+ Object.assign(el.style, style);
+
+ expect(el).not.toBeVisible();
+ });
+
+ it('does not match when applied to parent', () => {
+ Object.assign(parentEl.style, style);
+
+ expect(el).not.toBeVisible();
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/helpers/dom_shims/index.js b/spec/frontend/helpers/dom_shims/index.js
new file mode 100644
index 00000000000..40256398e6d
--- /dev/null
+++ b/spec/frontend/helpers/dom_shims/index.js
@@ -0,0 +1 @@
+import './get_client_rects';
diff --git a/spec/frontend/helpers/test_constants.js b/spec/frontend/helpers/test_constants.js
index 8dc4aef87e1..c97d47a6406 100644
--- a/spec/frontend/helpers/test_constants.js
+++ b/spec/frontend/helpers/test_constants.js
@@ -1,2 +1,7 @@
-// eslint-disable-next-line import/prefer-default-export
+export const FIXTURES_PATH = `/fixtures`;
export const TEST_HOST = 'http://test.host';
+
+export const DUMMY_IMAGE_URL = `${FIXTURES_PATH}/static/images/one_white_pixel.png`;
+
+export const GREEN_BOX_IMAGE_URL = `${FIXTURES_PATH}/static/images/green_box.png`;
+export const RED_BOX_IMAGE_URL = `${FIXTURES_PATH}/static/images/red_box.png`;
diff --git a/spec/frontend/helpers/tracking_helper.js b/spec/frontend/helpers/tracking_helper.js
new file mode 100644
index 00000000000..68c1bd2dbca
--- /dev/null
+++ b/spec/frontend/helpers/tracking_helper.js
@@ -0,0 +1,25 @@
+import Tracking from '~/tracking';
+
+export default Tracking;
+
+let document;
+let handlers;
+
+export function mockTracking(category = '_category_', documentOverride, spyMethod) {
+ document = documentOverride || window.document;
+ window.snowplow = () => {};
+ Tracking.bindDocument(category, document);
+ return spyMethod ? spyMethod(Tracking, 'event') : null;
+}
+
+export function unmockTracking() {
+ window.snowplow = undefined;
+ handlers.forEach(event => document.removeEventListener(event.name, event.func));
+}
+
+export function triggerEvent(selectorOrEl, eventName = 'click') {
+ const event = new Event(eventName, { bubbles: true });
+ const el = typeof selectorOrEl === 'string' ? document.querySelector(selectorOrEl) : selectorOrEl;
+
+ el.dispatchEvent(event);
+}
diff --git a/spec/frontend/helpers/vue_resource_helper.js b/spec/frontend/helpers/vue_resource_helper.js
deleted file mode 100644
index 0f58af09933..00000000000
--- a/spec/frontend/helpers/vue_resource_helper.js
+++ /dev/null
@@ -1,11 +0,0 @@
-// eslint-disable-next-line import/prefer-default-export
-export const headersInterceptor = (request, next) => {
- next(response => {
- const headers = {};
- response.headers.forEach((value, key) => {
- headers[key] = value;
- });
- // eslint-disable-next-line no-param-reassign
- response.headers = headers;
- });
-};
diff --git a/spec/frontend/ide/components/branches/search_list_spec.js b/spec/frontend/ide/components/branches/search_list_spec.js
new file mode 100644
index 00000000000..5cfe1c25c6b
--- /dev/null
+++ b/spec/frontend/ide/components/branches/search_list_spec.js
@@ -0,0 +1,81 @@
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+import Vuex from 'vuex';
+import { __ } from '~/locale';
+import List from '~/ide/components/branches/search_list.vue';
+import Item from '~/ide/components/branches/item.vue';
+import { GlLoadingIcon } from '@gitlab/ui';
+import { branches } from '../../mock_data';
+
+const localVue = createLocalVue();
+localVue.use(Vuex);
+
+describe('IDE branches search list', () => {
+ let wrapper;
+ const fetchBranchesMock = jest.fn();
+
+ const createComponent = (state, currentBranchId = 'branch') => {
+ const fakeStore = new Vuex.Store({
+ state: {
+ currentBranchId,
+ currentProjectId: 'project',
+ },
+ modules: {
+ branches: {
+ namespaced: true,
+ state: { isLoading: false, branches: [], ...state },
+ actions: {
+ fetchBranches: fetchBranchesMock,
+ },
+ },
+ },
+ });
+
+ wrapper = shallowMount(List, {
+ localVue,
+ store: fakeStore,
+ sync: false,
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ it('calls fetch on mounted', () => {
+ createComponent();
+ expect(fetchBranchesMock).toHaveBeenCalled();
+ });
+
+ it('renders loading icon when `isLoading` is true', () => {
+ createComponent({ isLoading: true });
+ expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
+ });
+
+ it('renders branches not found when search is not empty and branches list is empty', () => {
+ createComponent({ branches: [] });
+ wrapper.find('input[type="search"]').setValue('something');
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(wrapper.text()).toContain(__('No branches found'));
+ });
+ });
+
+ describe('with branches', () => {
+ it('renders list', () => {
+ createComponent({ branches });
+ const items = wrapper.findAll(Item);
+
+ expect(items.length).toBe(branches.length);
+ });
+
+ it('renders check next to active branch', () => {
+ const activeBranch = 'regular';
+ createComponent({ branches }, activeBranch);
+ const items = wrapper.findAll(Item).filter(w => w.props('isActive'));
+
+ expect(items.length).toBe(1);
+ expect(items.at(0).props('item').name).toBe(activeBranch);
+ });
+ });
+});
diff --git a/spec/frontend/ide/components/error_message_spec.js b/spec/frontend/ide/components/error_message_spec.js
new file mode 100644
index 00000000000..e995c64645e
--- /dev/null
+++ b/spec/frontend/ide/components/error_message_spec.js
@@ -0,0 +1,122 @@
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+import Vuex from 'vuex';
+import { GlLoadingIcon } from '@gitlab/ui';
+import ErrorMessage from '~/ide/components/error_message.vue';
+
+const localVue = createLocalVue();
+localVue.use(Vuex);
+
+describe('IDE error message component', () => {
+ let wrapper;
+
+ const setErrorMessageMock = jest.fn();
+ const createComponent = messageProps => {
+ const fakeStore = new Vuex.Store({
+ actions: { setErrorMessage: setErrorMessageMock },
+ });
+
+ wrapper = shallowMount(ErrorMessage, {
+ propsData: {
+ message: {
+ text: 'some text',
+ actionText: 'test action',
+ actionPayload: 'testActionPayload',
+ ...messageProps,
+ },
+ },
+ store: fakeStore,
+ localVue,
+ sync: false,
+ });
+ };
+
+ beforeEach(() => {
+ setErrorMessageMock.mockReset();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ it('renders error message', () => {
+ const text = 'error message';
+ createComponent({ text });
+ expect(wrapper.text()).toContain(text);
+ });
+
+ it('clears error message on click', () => {
+ createComponent();
+ wrapper.trigger('click');
+
+ expect(setErrorMessageMock).toHaveBeenCalledWith(expect.any(Object), null, undefined);
+ });
+
+ describe('with action', () => {
+ let actionMock;
+
+ const message = {
+ actionText: 'test action',
+ actionPayload: 'testActionPayload',
+ };
+
+ beforeEach(() => {
+ actionMock = jest.fn().mockResolvedValue();
+ createComponent({
+ ...message,
+ action: actionMock,
+ });
+ });
+
+ it('renders action button', () => {
+ const button = wrapper.find('button');
+
+ expect(button.exists()).toBe(true);
+ expect(button.text()).toContain(message.actionText);
+ });
+
+ it('does not clear error message on click', () => {
+ wrapper.trigger('click');
+
+ expect(setErrorMessageMock).not.toHaveBeenCalled();
+ });
+
+ it('dispatches action', () => {
+ wrapper.find('button').trigger('click');
+
+ expect(actionMock).toHaveBeenCalledWith(message.actionPayload);
+ });
+
+ it('does not dispatch action when already loading', () => {
+ wrapper.find('button').trigger('click');
+ actionMock.mockReset();
+ wrapper.find('button').trigger('click');
+ expect(actionMock).not.toHaveBeenCalled();
+ });
+
+ it('shows loading icon when loading', () => {
+ let resolveAction;
+ actionMock.mockImplementation(
+ () =>
+ new Promise(resolve => {
+ resolveAction = resolve;
+ }),
+ );
+ wrapper.find('button').trigger('click');
+
+ return wrapper.vm.$nextTick(() => {
+ expect(wrapper.find(GlLoadingIcon).isVisible()).toBe(true);
+ resolveAction();
+ });
+ });
+
+ it('hides loading icon when operation finishes', () => {
+ wrapper.find('button').trigger('click');
+ return actionMock()
+ .then(() => wrapper.vm.$nextTick())
+ .then(() => {
+ expect(wrapper.find(GlLoadingIcon).isVisible()).toBe(false);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/ide/components/file_templates/dropdown_spec.js b/spec/frontend/ide/components/file_templates/dropdown_spec.js
new file mode 100644
index 00000000000..83d797469ad
--- /dev/null
+++ b/spec/frontend/ide/components/file_templates/dropdown_spec.js
@@ -0,0 +1,175 @@
+import Vuex from 'vuex';
+import $ from 'jquery';
+import { GlLoadingIcon } from '@gitlab/ui';
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+import Dropdown from '~/ide/components/file_templates/dropdown.vue';
+
+const localVue = createLocalVue();
+localVue.use(Vuex);
+
+describe('IDE file templates dropdown component', () => {
+ let wrapper;
+ let element;
+ let fetchTemplateTypesMock;
+
+ const defaultProps = {
+ label: 'label',
+ };
+
+ const findItemButtons = () => wrapper.findAll('button');
+ const findSearch = () => wrapper.find('input[type="search"]');
+ const triggerDropdown = () => $(element).trigger('show.bs.dropdown');
+
+ const createComponent = ({ props, state } = {}) => {
+ fetchTemplateTypesMock = jest.fn();
+ const fakeStore = new Vuex.Store({
+ modules: {
+ fileTemplates: {
+ namespaced: true,
+ state: {
+ templates: [],
+ isLoading: false,
+ ...state,
+ },
+ actions: {
+ fetchTemplateTypes: fetchTemplateTypesMock,
+ },
+ },
+ },
+ });
+
+ wrapper = shallowMount(Dropdown, {
+ propsData: {
+ ...defaultProps,
+ ...props,
+ },
+ store: fakeStore,
+ localVue,
+ sync: false,
+ });
+
+ ({ element } = wrapper);
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ it('calls clickItem on click', () => {
+ const itemData = { name: 'test.yml ' };
+ createComponent({ props: { data: [itemData] } });
+ const item = findItemButtons().at(0);
+ item.trigger('click');
+
+ expect(wrapper.emitted().click[0][0]).toBe(itemData);
+ });
+
+ it('renders dropdown title', () => {
+ const title = 'Test title';
+ createComponent({ props: { title } });
+
+ expect(wrapper.find('.dropdown-title').text()).toContain(title);
+ });
+
+ describe('in async mode', () => {
+ const defaultAsyncProps = { ...defaultProps, isAsyncData: true };
+
+ it('calls `fetchTemplateTypes` on dropdown event', () => {
+ createComponent({ props: defaultAsyncProps });
+
+ triggerDropdown();
+
+ expect(fetchTemplateTypesMock).toHaveBeenCalled();
+ });
+
+ it('does not call `fetchTemplateTypes` on dropdown event if destroyed', () => {
+ createComponent({ props: defaultAsyncProps });
+ wrapper.destroy();
+
+ triggerDropdown();
+
+ expect(fetchTemplateTypesMock).not.toHaveBeenCalled();
+ });
+
+ it('shows loader when isLoading is true', () => {
+ createComponent({ props: defaultAsyncProps, state: { isLoading: true } });
+
+ expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
+ });
+
+ it('renders templates', () => {
+ const templates = [{ name: 'file-1' }, { name: 'file-2' }];
+ createComponent({
+ props: { ...defaultAsyncProps, data: [{ name: 'should-never-appear ' }] },
+ state: {
+ templates,
+ },
+ });
+ const items = findItemButtons();
+
+ expect(items.wrappers.map(x => x.text())).toEqual(templates.map(x => x.name));
+ });
+
+ it('searches template data', () => {
+ const templates = [{ name: 'match 1' }, { name: 'other' }, { name: 'match 2' }];
+ const matches = ['match 1', 'match 2'];
+ createComponent({
+ props: { ...defaultAsyncProps, data: matches, searchable: true },
+ state: { templates },
+ });
+ findSearch().setValue('match');
+ return wrapper.vm.$nextTick().then(() => {
+ const items = findItemButtons();
+
+ expect(items.length).toBe(matches.length);
+ expect(items.wrappers.map(x => x.text())).toEqual(matches);
+ });
+ });
+
+ it('does not render input when `searchable` is true & `showLoading` is true', () => {
+ createComponent({
+ props: { ...defaultAsyncProps, searchable: true },
+ state: { isLoading: true },
+ });
+
+ expect(findSearch().exists()).toBe(false);
+ });
+ });
+
+ describe('in sync mode', () => {
+ it('renders props data', () => {
+ const data = [{ name: 'file-1' }, { name: 'file-2' }];
+ createComponent({
+ props: { data },
+ state: {
+ templates: [{ name: 'should-never-appear ' }],
+ },
+ });
+
+ const items = findItemButtons();
+
+ expect(items.length).toBe(data.length);
+ expect(items.wrappers.map(x => x.text())).toEqual(data.map(x => x.name));
+ });
+
+ it('renders input when `searchable` is true', () => {
+ createComponent({ props: { searchable: true } });
+
+ expect(findSearch().exists()).toBe(true);
+ });
+
+ it('searches data', () => {
+ const data = [{ name: 'match 1' }, { name: 'other' }, { name: 'match 2' }];
+ const matches = ['match 1', 'match 2'];
+ createComponent({ props: { searchable: true, data } });
+ findSearch().setValue('match');
+ return wrapper.vm.$nextTick().then(() => {
+ const items = findItemButtons();
+
+ expect(items.length).toBe(matches.length);
+ expect(items.wrappers.map(x => x.text())).toEqual(matches);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/ide/components/jobs/list_spec.js b/spec/frontend/ide/components/jobs/list_spec.js
new file mode 100644
index 00000000000..ec2e5b05048
--- /dev/null
+++ b/spec/frontend/ide/components/jobs/list_spec.js
@@ -0,0 +1,115 @@
+import { shallowMount, mount, createLocalVue } from '@vue/test-utils';
+import { GlLoadingIcon } from '@gitlab/ui';
+import Vuex from 'vuex';
+import StageList from '~/ide/components/jobs/list.vue';
+import Stage from '~/ide/components/jobs/stage.vue';
+
+const localVue = createLocalVue();
+localVue.use(Vuex);
+const storeActions = {
+ fetchJobs: jest.fn(),
+ toggleStageCollapsed: jest.fn(),
+ setDetailJob: jest.fn(),
+};
+
+const store = new Vuex.Store({
+ modules: {
+ pipelines: {
+ namespaced: true,
+ actions: storeActions,
+ },
+ },
+});
+
+describe('IDE stages list', () => {
+ let wrapper;
+
+ const defaultProps = {
+ stages: [],
+ loading: false,
+ };
+
+ const stages = ['build', 'test', 'deploy'].map((name, id) => ({
+ id,
+ name,
+ jobs: [],
+ status: { icon: 'status_success' },
+ }));
+
+ const createComponent = props => {
+ wrapper = shallowMount(StageList, {
+ propsData: {
+ ...defaultProps,
+ ...props,
+ },
+ localVue,
+ store,
+ sync: false,
+ });
+ };
+
+ afterEach(() => {
+ Object.values(storeActions).forEach(actionMock => actionMock.mockClear());
+ });
+
+ afterAll(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ it('renders loading icon when no stages & loading', () => {
+ createComponent({ loading: true, stages: [] });
+
+ expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
+ });
+
+ it('renders stages components for each stage', () => {
+ createComponent({ stages });
+ expect(wrapper.findAll(Stage).length).toBe(stages.length);
+ });
+
+ it('triggers fetchJobs action when stage emits fetch event', () => {
+ createComponent({ stages });
+ wrapper.find(Stage).vm.$emit('fetch');
+ expect(storeActions.fetchJobs).toHaveBeenCalled();
+ });
+
+ it('triggers toggleStageCollapsed action when stage emits toggleCollapsed event', () => {
+ createComponent({ stages });
+ wrapper.find(Stage).vm.$emit('toggleCollapsed');
+ expect(storeActions.toggleStageCollapsed).toHaveBeenCalled();
+ });
+
+ it('triggers setDetailJob action when stage emits clickViewLog event', () => {
+ createComponent({ stages });
+ wrapper.find(Stage).vm.$emit('clickViewLog');
+ expect(storeActions.setDetailJob).toHaveBeenCalled();
+ });
+
+ describe('integration tests', () => {
+ const findCardHeader = () => wrapper.find('.card-header');
+
+ beforeEach(() => {
+ wrapper = mount(StageList, {
+ propsData: { ...defaultProps, stages },
+ store,
+ sync: false,
+ localVue,
+ });
+ });
+
+ it('calls toggleStageCollapsed when clicking stage header', () => {
+ findCardHeader().trigger('click');
+
+ expect(storeActions.toggleStageCollapsed).toHaveBeenCalledWith(
+ expect.any(Object),
+ 0,
+ undefined,
+ );
+ });
+
+ it('calls fetchJobs when stage is mounted', () => {
+ expect(storeActions.fetchJobs.mock.calls.map(([, stage]) => stage)).toEqual(stages);
+ });
+ });
+});
diff --git a/spec/frontend/ide/components/merge_requests/list_spec.js b/spec/frontend/ide/components/merge_requests/list_spec.js
new file mode 100644
index 00000000000..86a311acad4
--- /dev/null
+++ b/spec/frontend/ide/components/merge_requests/list_spec.js
@@ -0,0 +1,214 @@
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+import Vuex from 'vuex';
+import List from '~/ide/components/merge_requests/list.vue';
+import Item from '~/ide/components/merge_requests/item.vue';
+import TokenedInput from '~/ide/components/shared/tokened_input.vue';
+import { GlLoadingIcon } from '@gitlab/ui';
+import { mergeRequests as mergeRequestsMock } from '../../mock_data';
+
+const localVue = createLocalVue();
+localVue.use(Vuex);
+
+describe('IDE merge requests list', () => {
+ let wrapper;
+ let fetchMergeRequestsMock;
+
+ const findSearchTypeButtons = () => wrapper.findAll('button');
+ const findTokenedInput = () => wrapper.find(TokenedInput);
+
+ const createComponent = (state = {}) => {
+ const { mergeRequests = {}, ...restOfState } = state;
+ const fakeStore = new Vuex.Store({
+ state: {
+ currentMergeRequestId: '1',
+ currentProjectId: 'project/master',
+ ...restOfState,
+ },
+ modules: {
+ mergeRequests: {
+ namespaced: true,
+ state: {
+ isLoading: false,
+ mergeRequests: [],
+ ...mergeRequests,
+ },
+ actions: {
+ fetchMergeRequests: fetchMergeRequestsMock,
+ },
+ },
+ },
+ });
+
+ wrapper = shallowMount(List, {
+ store: fakeStore,
+ localVue,
+ sync: false,
+ });
+ };
+
+ beforeEach(() => {
+ fetchMergeRequestsMock = jest.fn();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ it('calls fetch on mounted', () => {
+ createComponent();
+ expect(fetchMergeRequestsMock).toHaveBeenCalledWith(
+ expect.any(Object),
+ {
+ search: '',
+ type: '',
+ },
+ undefined,
+ );
+ });
+
+ it('renders loading icon when merge request is loading', () => {
+ createComponent({ mergeRequests: { isLoading: true } });
+ expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
+ });
+
+ it('renders no search results text when search is not empty', () => {
+ createComponent();
+ findTokenedInput().vm.$emit('input', 'something');
+ return wrapper.vm.$nextTick().then(() => {
+ expect(wrapper.text()).toContain('No merge requests found');
+ });
+ });
+
+ it('clicking on search type, sets currentSearchType and loads merge requests', () => {
+ createComponent();
+ findTokenedInput().vm.$emit('focus');
+
+ return wrapper.vm
+ .$nextTick()
+ .then(() => {
+ findSearchTypeButtons()
+ .at(0)
+ .trigger('click');
+ return wrapper.vm.$nextTick();
+ })
+ .then(() => {
+ const searchType = wrapper.vm.$options.searchTypes[0];
+
+ expect(findTokenedInput().props('tokens')).toEqual([searchType]);
+ expect(fetchMergeRequestsMock).toHaveBeenCalledWith(
+ expect.any(Object),
+ {
+ type: searchType.type,
+ search: '',
+ },
+ undefined,
+ );
+ });
+ });
+
+ describe('with merge requests', () => {
+ let defaultStateWithMergeRequests;
+
+ beforeAll(() => {
+ defaultStateWithMergeRequests = {
+ mergeRequests: {
+ isLoading: false,
+ mergeRequests: [
+ { ...mergeRequestsMock[0], projectPathWithNamespace: 'gitlab-org/gitlab-foss' },
+ ],
+ },
+ };
+ });
+
+ it('renders list', () => {
+ createComponent(defaultStateWithMergeRequests);
+
+ expect(wrapper.findAll(Item).length).toBe(1);
+ expect(wrapper.find(Item).props('item')).toBe(
+ defaultStateWithMergeRequests.mergeRequests.mergeRequests[0],
+ );
+ });
+
+ describe('when searching merge requests', () => {
+ it('calls `loadMergeRequests` on input in search field', () => {
+ createComponent(defaultStateWithMergeRequests);
+ const input = findTokenedInput();
+ input.vm.$emit('input', 'something');
+ fetchMergeRequestsMock.mockClear();
+
+ jest.runAllTimers();
+ return wrapper.vm.$nextTick().then(() => {
+ expect(fetchMergeRequestsMock).toHaveBeenCalledWith(
+ expect.any(Object),
+ {
+ search: 'something',
+ type: '',
+ },
+ undefined,
+ );
+ });
+ });
+ });
+ });
+
+ describe('on search focus', () => {
+ let input;
+
+ beforeEach(() => {
+ createComponent();
+ input = findTokenedInput();
+ });
+
+ describe('without search value', () => {
+ beforeEach(() => {
+ input.vm.$emit('focus');
+ return wrapper.vm.$nextTick();
+ });
+
+ it('shows search types', () => {
+ const buttons = findSearchTypeButtons();
+ expect(buttons.wrappers.map(x => x.text().trim())).toEqual(
+ wrapper.vm.$options.searchTypes.map(x => x.label),
+ );
+ });
+
+ it('hides search types when search changes', () => {
+ input.vm.$emit('input', 'something');
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(findSearchTypeButtons().exists()).toBe(false);
+ });
+ });
+
+ describe('with search type', () => {
+ beforeEach(() => {
+ findSearchTypeButtons()
+ .at(0)
+ .trigger('click');
+
+ return wrapper.vm
+ .$nextTick()
+ .then(() => input.vm.$emit('focus'))
+ .then(() => wrapper.vm.$nextTick());
+ });
+
+ it('does not show search types', () => {
+ expect(findSearchTypeButtons().exists()).toBe(false);
+ });
+ });
+ });
+
+ describe('with search value', () => {
+ beforeEach(() => {
+ input.vm.$emit('input', 'something');
+ input.vm.$emit('focus');
+ return wrapper.vm.$nextTick();
+ });
+
+ it('does not show search types', () => {
+ expect(findSearchTypeButtons().exists()).toBe(false);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/ide/components/pipelines/__snapshots__/list_spec.js.snap b/spec/frontend/ide/components/pipelines/__snapshots__/list_spec.js.snap
new file mode 100644
index 00000000000..5fbe6af750d
--- /dev/null
+++ b/spec/frontend/ide/components/pipelines/__snapshots__/list_spec.js.snap
@@ -0,0 +1,15 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`IDE pipelines list when loaded renders empty state when no latestPipeline 1`] = `
+<div
+ class="ide-pipeline"
+>
+ <!---->
+
+ <emptystate-stub
+ cansetci="true"
+ emptystatesvgpath="http://test.host"
+ helppagepath="http://test.host"
+ />
+</div>
+`;
diff --git a/spec/frontend/ide/components/pipelines/list_spec.js b/spec/frontend/ide/components/pipelines/list_spec.js
new file mode 100644
index 00000000000..a974139a8f9
--- /dev/null
+++ b/spec/frontend/ide/components/pipelines/list_spec.js
@@ -0,0 +1,193 @@
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+import Vuex from 'vuex';
+import List from '~/ide/components/pipelines/list.vue';
+import JobsList from '~/ide/components/jobs/list.vue';
+import Tab from '~/vue_shared/components/tabs/tab.vue';
+import CiIcon from '~/vue_shared/components/ci_icon.vue';
+import { GlLoadingIcon } from '@gitlab/ui';
+import { TEST_HOST } from 'helpers/test_constants';
+import { pipelines } from '../../../../javascripts/ide/mock_data';
+
+const localVue = createLocalVue();
+localVue.use(Vuex);
+
+describe('IDE pipelines list', () => {
+ let wrapper;
+
+ const defaultState = {
+ links: { ciHelpPagePath: TEST_HOST },
+ pipelinesEmptyStateSvgPath: TEST_HOST,
+ pipelines: {
+ stages: [],
+ failedStages: [],
+ isLoadingJobs: false,
+ },
+ };
+
+ const fetchLatestPipelineMock = jest.fn();
+ const failedStagesGetterMock = jest.fn().mockReturnValue([]);
+
+ const createComponent = (state = {}) => {
+ const { pipelines: pipelinesState, ...restOfState } = state;
+ const { defaultPipelines, ...defaultRestOfState } = defaultState;
+
+ const fakeStore = new Vuex.Store({
+ getters: { currentProject: () => ({ web_url: 'some/url ' }) },
+ state: {
+ ...defaultRestOfState,
+ ...restOfState,
+ },
+ modules: {
+ pipelines: {
+ namespaced: true,
+ state: {
+ ...defaultPipelines,
+ ...pipelinesState,
+ },
+ actions: {
+ fetchLatestPipeline: fetchLatestPipelineMock,
+ },
+ getters: {
+ jobsCount: () => 1,
+ failedJobsCount: () => 1,
+ failedStages: failedStagesGetterMock,
+ pipelineFailed: () => false,
+ },
+ methods: {
+ fetchLatestPipeline: jest.fn(),
+ },
+ },
+ },
+ });
+
+ wrapper = shallowMount(List, {
+ localVue,
+ store: fakeStore,
+ sync: false,
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ it('fetches latest pipeline', () => {
+ createComponent();
+
+ expect(fetchLatestPipelineMock).toHaveBeenCalled();
+ });
+
+ describe('when loading', () => {
+ let defaultPipelinesLoadingState;
+ beforeAll(() => {
+ defaultPipelinesLoadingState = {
+ ...defaultState.pipelines,
+ isLoadingPipeline: true,
+ };
+ });
+
+ it('does not render when pipeline has loaded before', () => {
+ createComponent({
+ pipelines: {
+ ...defaultPipelinesLoadingState,
+ hasLoadedPipeline: true,
+ },
+ });
+
+ expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
+ });
+
+ it('renders loading state', () => {
+ createComponent({
+ pipelines: {
+ ...defaultPipelinesLoadingState,
+ hasLoadedPipeline: false,
+ },
+ });
+
+ expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
+ });
+ });
+
+ describe('when loaded', () => {
+ let defaultPipelinesLoadedState;
+ beforeAll(() => {
+ defaultPipelinesLoadedState = {
+ ...defaultState.pipelines,
+ isLoadingPipeline: false,
+ hasLoadedPipeline: true,
+ };
+ });
+
+ it('renders empty state when no latestPipeline', () => {
+ createComponent({ pipelines: { ...defaultPipelinesLoadedState, latestPipeline: null } });
+ expect(wrapper.element).toMatchSnapshot();
+ });
+
+ describe('with latest pipeline loaded', () => {
+ let withLatestPipelineState;
+ beforeAll(() => {
+ withLatestPipelineState = {
+ ...defaultPipelinesLoadedState,
+ latestPipeline: pipelines[0],
+ };
+ });
+
+ it('renders ci icon', () => {
+ createComponent({ pipelines: withLatestPipelineState });
+ expect(wrapper.find(CiIcon).exists()).toBe(true);
+ });
+
+ it('renders pipeline data', () => {
+ createComponent({ pipelines: withLatestPipelineState });
+
+ expect(wrapper.text()).toContain('#1');
+ });
+
+ it('renders list of jobs', () => {
+ const stages = [];
+ const isLoadingJobs = true;
+ createComponent({ pipelines: { ...withLatestPipelineState, stages, isLoadingJobs } });
+
+ const jobProps = wrapper
+ .findAll(Tab)
+ .at(0)
+ .find(JobsList)
+ .props();
+ expect(jobProps.stages).toBe(stages);
+ expect(jobProps.loading).toBe(isLoadingJobs);
+ });
+
+ it('renders list of failed jobs', () => {
+ const failedStages = [];
+ failedStagesGetterMock.mockReset().mockReturnValue(failedStages);
+ const isLoadingJobs = true;
+ createComponent({ pipelines: { ...withLatestPipelineState, isLoadingJobs } });
+
+ const jobProps = wrapper
+ .findAll(Tab)
+ .at(1)
+ .find(JobsList)
+ .props();
+ expect(jobProps.stages).toBe(failedStages);
+ expect(jobProps.loading).toBe(isLoadingJobs);
+ });
+
+ describe('with YAML error', () => {
+ it('renders YAML error', () => {
+ const yamlError = 'test yaml error';
+ createComponent({
+ pipelines: {
+ ...defaultPipelinesLoadedState,
+ latestPipeline: { ...pipelines[0], yamlError },
+ },
+ });
+
+ expect(wrapper.text()).toContain('Found errors in your .gitlab-ci.yml:');
+ expect(wrapper.text()).toContain(yamlError);
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/ide/lib/files_spec.js b/spec/frontend/ide/lib/files_spec.js
index 08a31318544..654dc6c13c8 100644
--- a/spec/frontend/ide/lib/files_spec.js
+++ b/spec/frontend/ide/lib/files_spec.js
@@ -1,6 +1,6 @@
import { viewerInformationForPath } from '~/vue_shared/components/content_viewer/lib/viewer_utils';
-import { decorateFiles, splitParent, escapeFileUrl } from '~/ide/lib/files';
-import { decorateData } from '~/ide/stores/utils';
+import { decorateFiles, splitParent } from '~/ide/lib/files';
+import { decorateData, escapeFileUrl } from '~/ide/stores/utils';
const TEST_BRANCH_ID = 'lorem-ipsum';
const TEST_PROJECT_ID = 10;
diff --git a/spec/frontend/ide/mock_data.js b/spec/frontend/ide/mock_data.js
new file mode 100644
index 00000000000..80eb15fe5a6
--- /dev/null
+++ b/spec/frontend/ide/mock_data.js
@@ -0,0 +1,228 @@
+import { TEST_HOST } from 'spec/test_constants';
+
+export const projectData = {
+ id: 1,
+ name: 'abcproject',
+ web_url: '',
+ avatar_url: '',
+ path: '',
+ name_with_namespace: 'namespace/abcproject',
+ branches: {
+ master: {
+ treeId: 'abcproject/master',
+ can_push: true,
+ commit: {
+ id: '123',
+ },
+ },
+ },
+ mergeRequests: {},
+ merge_requests_enabled: true,
+ default_branch: 'master',
+};
+
+export const pipelines = [
+ {
+ id: 1,
+ ref: 'master',
+ sha: '123',
+ details: {
+ status: {
+ icon: 'status_failed',
+ group: 'failed',
+ text: 'Failed',
+ },
+ },
+ commit: { id: '123' },
+ },
+ {
+ id: 2,
+ ref: 'master',
+ sha: '213',
+ details: {
+ status: {
+ icon: 'status_failed',
+ group: 'failed',
+ text: 'Failed',
+ },
+ },
+ commit: { id: '213' },
+ },
+];
+
+export const stages = [
+ {
+ dropdown_path: `${TEST_HOST}/testing`,
+ name: 'build',
+ status: {
+ icon: 'status_failed',
+ group: 'failed',
+ text: 'failed',
+ },
+ },
+ {
+ dropdown_path: 'testing',
+ name: 'test',
+ status: {
+ icon: 'status_failed',
+ group: 'failed',
+ text: 'failed',
+ },
+ },
+];
+
+export const jobs = [
+ {
+ id: 1,
+ name: 'test',
+ path: 'testing',
+ status: {
+ icon: 'status_success',
+ text: 'passed',
+ },
+ stage: 'test',
+ duration: 1,
+ started: new Date(),
+ },
+ {
+ id: 2,
+ name: 'test 2',
+ path: 'testing2',
+ status: {
+ icon: 'status_success',
+ text: 'passed',
+ },
+ stage: 'test',
+ duration: 1,
+ started: new Date(),
+ },
+ {
+ id: 3,
+ name: 'test 3',
+ path: 'testing3',
+ status: {
+ icon: 'status_success',
+ text: 'passed',
+ },
+ stage: 'test',
+ duration: 1,
+ started: new Date(),
+ },
+ {
+ id: 4,
+ name: 'test 4',
+ path: 'testing4',
+ status: {
+ icon: 'status_failed',
+ text: 'failed',
+ },
+ stage: 'build',
+ duration: 1,
+ started: new Date(),
+ },
+];
+
+export const fullPipelinesResponse = {
+ data: {
+ count: {
+ all: 2,
+ },
+ pipelines: [
+ {
+ id: '51',
+ path: 'test',
+ commit: {
+ id: '123',
+ },
+ details: {
+ status: {
+ icon: 'status_failed',
+ text: 'failed',
+ },
+ stages: [...stages],
+ },
+ },
+ {
+ id: '50',
+ commit: {
+ id: 'abc123def456ghi789jkl',
+ },
+ details: {
+ status: {
+ icon: 'status_success',
+ text: 'passed',
+ },
+ stages: [...stages],
+ },
+ },
+ ],
+ },
+};
+
+export const mergeRequests = [
+ {
+ id: 1,
+ iid: 1,
+ title: 'Test merge request',
+ project_id: 1,
+ web_url: `${TEST_HOST}/namespace/project-path/merge_requests/1`,
+ },
+];
+
+export const branches = [
+ {
+ id: 1,
+ name: 'master',
+ commit: {
+ message: 'Update master branch',
+ committed_date: '2018-08-01T00:20:05Z',
+ },
+ can_push: true,
+ protected: true,
+ default: true,
+ },
+ {
+ id: 2,
+ name: 'protected/no-access',
+ commit: {
+ message: 'Update some stuff',
+ committed_date: '2018-08-02T00:00:05Z',
+ },
+ can_push: false,
+ protected: true,
+ default: false,
+ },
+ {
+ id: 3,
+ name: 'protected/access',
+ commit: {
+ message: 'Update some stuff',
+ committed_date: '2018-08-02T00:00:05Z',
+ },
+ can_push: true,
+ protected: true,
+ default: false,
+ },
+ {
+ id: 4,
+ name: 'regular',
+ commit: {
+ message: 'Update some more stuff',
+ committed_date: '2018-06-30T00:20:05Z',
+ },
+ can_push: true,
+ protected: false,
+ default: false,
+ },
+ {
+ id: 5,
+ name: 'regular/no-access',
+ commit: {
+ message: 'Update some more stuff',
+ committed_date: '2018-06-30T00:20:05Z',
+ },
+ can_push: false,
+ protected: false,
+ default: false,
+ },
+];
diff --git a/spec/frontend/ide/stores/integration_spec.js b/spec/frontend/ide/stores/integration_spec.js
new file mode 100644
index 00000000000..443de18f288
--- /dev/null
+++ b/spec/frontend/ide/stores/integration_spec.js
@@ -0,0 +1,100 @@
+import { decorateFiles } from '~/ide/lib/files';
+import { createStore } from '~/ide/stores';
+
+const TEST_BRANCH = 'test_branch';
+const TEST_NAMESPACE = 'test_namespace';
+const TEST_PROJECT_ID = `${TEST_NAMESPACE}/test_project`;
+const TEST_PATH_DIR = 'src';
+const TEST_PATH = `${TEST_PATH_DIR}/foo.js`;
+const TEST_CONTENT = `Lorem ipsum dolar sit
+Lorem ipsum dolar
+Lorem ipsum
+Lorem
+`;
+
+jest.mock('~/ide/ide_router');
+
+describe('IDE store integration', () => {
+ let store;
+
+ beforeEach(() => {
+ store = createStore();
+ store.replaceState({
+ ...store.state,
+ projects: {
+ [TEST_PROJECT_ID]: {
+ web_url: 'test_web_url',
+ branches: [],
+ },
+ },
+ currentProjectId: TEST_PROJECT_ID,
+ currentBranchId: TEST_BRANCH,
+ });
+ });
+
+ describe('with project and files', () => {
+ beforeEach(() => {
+ const { entries, treeList } = decorateFiles({
+ data: [`${TEST_PATH_DIR}/`, TEST_PATH, 'README.md'],
+ projectId: TEST_PROJECT_ID,
+ branchId: TEST_BRANCH,
+ });
+
+ Object.assign(entries[TEST_PATH], {
+ raw: TEST_CONTENT,
+ });
+
+ store.replaceState({
+ ...store.state,
+ trees: {
+ [`${TEST_PROJECT_ID}/${TEST_BRANCH}`]: {
+ tree: treeList,
+ },
+ },
+ entries,
+ });
+ });
+
+ describe('when a file is deleted and readded', () => {
+ beforeEach(() => {
+ store.dispatch('deleteEntry', TEST_PATH);
+ store.dispatch('createTempEntry', { name: TEST_PATH, type: 'blob' });
+ });
+
+ it('has changed and staged', () => {
+ expect(store.state.changedFiles).toEqual([
+ expect.objectContaining({
+ path: TEST_PATH,
+ tempFile: true,
+ deleted: false,
+ }),
+ ]);
+
+ expect(store.state.stagedFiles).toEqual([
+ expect.objectContaining({
+ path: TEST_PATH,
+ deleted: true,
+ }),
+ ]);
+ });
+
+ it('cleans up after commit', () => {
+ const expected = expect.objectContaining({
+ path: TEST_PATH,
+ staged: false,
+ changed: false,
+ tempFile: false,
+ deleted: false,
+ });
+ store.dispatch('stageChange', TEST_PATH);
+
+ store.dispatch('commit/updateFilesAfterCommit', { data: {} });
+
+ expect(store.state.entries[TEST_PATH]).toEqual(expected);
+ expect(store.state.entries[TEST_PATH_DIR].tree.find(x => x.path === TEST_PATH)).toEqual(
+ expected,
+ );
+ });
+ });
+ });
+});
diff --git a/spec/frontend/import_projects/components/import_projects_table_spec.js b/spec/frontend/import_projects/components/import_projects_table_spec.js
index 17a998d0174..708f2758083 100644
--- a/spec/frontend/import_projects/components/import_projects_table_spec.js
+++ b/spec/frontend/import_projects/components/import_projects_table_spec.js
@@ -93,7 +93,7 @@ describe('ImportProjectsTable', () => {
return vm.$nextTick().then(() => {
expect(vm.$el.querySelector('.js-loading-button-icon')).toBeNull();
expect(vm.$el.querySelector('.table')).toBeNull();
- expect(vm.$el.innerText).toMatch(`No ${providerTitle} repositories available to import`);
+ expect(vm.$el.innerText).toMatch(`No ${providerTitle} repositories found`);
});
});
@@ -182,4 +182,10 @@ describe('ImportProjectsTable', () => {
expect(vm.$el.querySelector(`.ic-status_${statusObject.icon}`)).not.toBeNull();
});
});
+
+ it('renders filtering input field', () => {
+ expect(
+ vm.$el.querySelector('input[data-qa-selector="githubish_import_filter_field"]'),
+ ).not.toBeNull();
+ });
});
diff --git a/spec/frontend/import_projects/store/actions_spec.js b/spec/frontend/import_projects/store/actions_spec.js
index 6a7b90788dd..340b6f02d93 100644
--- a/spec/frontend/import_projects/store/actions_spec.js
+++ b/spec/frontend/import_projects/store/actions_spec.js
@@ -97,6 +97,7 @@ describe('import_projects store actions', () => {
describe('fetchRepos', () => {
let mock;
+ const payload = { imported_projects: [{}], provider_repos: [{}], namespaces: [{}] };
beforeEach(() => {
localState.reposPath = `${TEST_HOST}/endpoint.json`;
@@ -105,8 +106,7 @@ describe('import_projects store actions', () => {
afterEach(() => mock.restore());
- it('dispatches requestRepos and receiveReposSuccess actions on a successful request', done => {
- const payload = { imported_projects: [{}], provider_repos: [{}], namespaces: [{}] };
+ it('dispatches stopJobsPolling, requestRepos and receiveReposSuccess actions on a successful request', done => {
mock.onGet(`${TEST_HOST}/endpoint.json`).reply(200, payload);
testAction(
@@ -115,6 +115,7 @@ describe('import_projects store actions', () => {
localState,
[],
[
+ { type: 'stopJobsPolling' },
{ type: 'requestRepos' },
{
type: 'receiveReposSuccess',
@@ -128,7 +129,7 @@ describe('import_projects store actions', () => {
);
});
- it('dispatches requestRepos and receiveReposSuccess actions on an unsuccessful request', done => {
+ it('dispatches stopJobsPolling, requestRepos and receiveReposError actions on an unsuccessful request', done => {
mock.onGet(`${TEST_HOST}/endpoint.json`).reply(500);
testAction(
@@ -136,10 +137,39 @@ describe('import_projects store actions', () => {
null,
localState,
[],
- [{ type: 'requestRepos' }, { type: 'receiveReposError' }],
+ [{ type: 'stopJobsPolling' }, { type: 'requestRepos' }, { type: 'receiveReposError' }],
done,
);
});
+
+ describe('when filtered', () => {
+ beforeEach(() => {
+ localState.filter = 'filter';
+ });
+
+ it('fetches repos with filter applied', done => {
+ mock.onGet(`${TEST_HOST}/endpoint.json?filter=filter`).reply(200, payload);
+
+ testAction(
+ fetchRepos,
+ null,
+ localState,
+ [],
+ [
+ { type: 'stopJobsPolling' },
+ { type: 'requestRepos' },
+ {
+ type: 'receiveReposSuccess',
+ payload: convertObjectPropsToCamelCase(payload, { deep: true }),
+ },
+ {
+ type: 'fetchJobs',
+ },
+ ],
+ done,
+ );
+ });
+ });
});
describe('requestImport', () => {
@@ -249,6 +279,7 @@ describe('import_projects store actions', () => {
describe('fetchJobs', () => {
let mock;
+ const updatedProjects = [{ name: 'imported/project' }, { name: 'provider/repo' }];
beforeEach(() => {
localState.jobsPath = `${TEST_HOST}/endpoint.json`;
@@ -263,7 +294,6 @@ describe('import_projects store actions', () => {
afterEach(() => mock.restore());
it('dispatches requestJobs and receiveJobsSuccess actions on a successful request', done => {
- const updatedProjects = [{ name: 'imported/project' }, { name: 'provider/repo' }];
mock.onGet(`${TEST_HOST}/endpoint.json`).reply(200, updatedProjects);
testAction(
@@ -280,5 +310,29 @@ describe('import_projects store actions', () => {
done,
);
});
+
+ describe('when filtered', () => {
+ beforeEach(() => {
+ localState.filter = 'filter';
+ });
+
+ it('fetches realtime changes with filter applied', done => {
+ mock.onGet(`${TEST_HOST}/endpoint.json?filter=filter`).reply(200, updatedProjects);
+
+ testAction(
+ fetchJobs,
+ null,
+ localState,
+ [],
+ [
+ {
+ type: 'receiveJobsSuccess',
+ payload: convertObjectPropsToCamelCase(updatedProjects, { deep: true }),
+ },
+ ],
+ done,
+ );
+ });
+ });
});
});
diff --git a/spec/frontend/issue_show/store_spec.js b/spec/frontend/issue_show/store_spec.js
new file mode 100644
index 00000000000..b7fd70bf00e
--- /dev/null
+++ b/spec/frontend/issue_show/store_spec.js
@@ -0,0 +1,39 @@
+import Store from '~/issue_show/stores';
+import updateDescription from '~/issue_show/utils/update_description';
+
+jest.mock('~/issue_show/utils/update_description');
+
+describe('Store', () => {
+ let store;
+
+ beforeEach(() => {
+ store = new Store({
+ descriptionHtml: '<p>This is a description</p>',
+ });
+ });
+
+ describe('updateState', () => {
+ beforeEach(() => {
+ document.body.innerHTML = `
+ <div class="detail-page-description content-block">
+ <details open>
+ <summary>One</summary>
+ </details>
+ <details>
+ <summary>Two</summary>
+ </details>
+ </div>
+ `;
+ });
+
+ afterEach(() => {
+ document.getElementsByTagName('html')[0].innerHTML = '';
+ });
+
+ it('calls updateDetailsState', () => {
+ store.updateState({ description: '' });
+
+ expect(updateDescription).toHaveBeenCalledTimes(1);
+ });
+ });
+});
diff --git a/spec/frontend/issue_show/utils/update_description_spec.js b/spec/frontend/issue_show/utils/update_description_spec.js
new file mode 100644
index 00000000000..b2c6bd3c302
--- /dev/null
+++ b/spec/frontend/issue_show/utils/update_description_spec.js
@@ -0,0 +1,24 @@
+import updateDescription from '~/issue_show/utils/update_description';
+
+describe('updateDescription', () => {
+ it('returns the correct value to be set as descriptionHtml', () => {
+ const actual = updateDescription(
+ '<details><summary>One</summary></details><details><summary>Two</summary></details>',
+ [{ open: true }, { open: false }], // mocking NodeList from the dom.
+ );
+
+ expect(actual).toEqual(
+ '<details open="true"><summary>One</summary></details><details><summary>Two</summary></details>',
+ );
+ });
+
+ describe('when description details returned from api is different then whats currently on the dom', () => {
+ it('returns the description from the api', () => {
+ const dataDescription = '<details><summary>One</summary></details>';
+
+ const actual = updateDescription(dataDescription, []);
+
+ expect(actual).toEqual(dataDescription);
+ });
+ });
+});
diff --git a/spec/frontend/jobs/components/log/collapsible_section_spec.js b/spec/frontend/jobs/components/log/collapsible_section_spec.js
new file mode 100644
index 00000000000..01184a51193
--- /dev/null
+++ b/spec/frontend/jobs/components/log/collapsible_section_spec.js
@@ -0,0 +1,73 @@
+import { mount } from '@vue/test-utils';
+import CollpasibleSection from '~/jobs/components/log/collapsible_section.vue';
+import { collapsibleSectionClosed, collapsibleSectionOpened } from './mock_data';
+
+describe('Job Log Collapsible Section', () => {
+ let wrapper;
+
+ const traceEndpoint = 'jobs/335';
+
+ const findCollapsibleLine = () => wrapper.find('.collapsible-line');
+ const findCollapsibleLineSvg = () => wrapper.find('.collapsible-line svg');
+
+ const createComponent = (props = {}) => {
+ wrapper = mount(CollpasibleSection, {
+ sync: true,
+ propsData: {
+ ...props,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('with closed section', () => {
+ beforeEach(() => {
+ createComponent({
+ section: collapsibleSectionClosed,
+ traceEndpoint,
+ });
+ });
+
+ it('renders clickable header line', () => {
+ expect(findCollapsibleLine().attributes('role')).toBe('button');
+ });
+
+ it('renders an icon with the closed state', () => {
+ expect(findCollapsibleLineSvg().classes()).toContain('ic-angle-right');
+ });
+ });
+
+ describe('with opened section', () => {
+ beforeEach(() => {
+ createComponent({
+ section: collapsibleSectionOpened,
+ traceEndpoint,
+ });
+ });
+
+ it('renders clickable header line', () => {
+ expect(findCollapsibleLine().attributes('role')).toBe('button');
+ });
+
+ it('renders an icon with the open state', () => {
+ expect(findCollapsibleLineSvg().classes()).toContain('ic-angle-down');
+ });
+
+ it('renders collapsible lines content', () => {
+ expect(wrapper.findAll('.js-line').length).toEqual(collapsibleSectionOpened.lines.length);
+ });
+ });
+
+ it('emits onClickCollapsibleLine on click', () => {
+ createComponent({
+ section: collapsibleSectionOpened,
+ traceEndpoint,
+ });
+
+ findCollapsibleLine().trigger('click');
+ expect(wrapper.emitted('onClickCollapsibleLine').length).toBe(1);
+ });
+});
diff --git a/spec/frontend/jobs/components/log/mock_data.js b/spec/frontend/jobs/components/log/mock_data.js
index db42644de77..d375d82d3ca 100644
--- a/spec/frontend/jobs/components/log/mock_data.js
+++ b/spec/frontend/jobs/components/log/mock_data.js
@@ -14,13 +14,13 @@ export const jobLog = [
text: 'Using Docker executor with image dev.gitlab.org3',
},
],
- sections: ['prepare-executor'],
+ section: 'prepare-executor',
section_header: true,
},
{
offset: 1003,
content: [{ text: 'Starting service postgres:9.6.14 ...', style: 'text-green' }],
- sections: ['prepare-executor'],
+ section: 'prepare-executor',
},
];
@@ -37,23 +37,23 @@ export const utilsMockData = [
'Using Docker executor with image dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.6.3-golang-1.11-git-2.22-chrome-73.0-node-12.x-yarn-1.16-postgresql-9.6-graphicsmagick-1.3.33',
},
],
- sections: ['prepare-executor'],
+ section: 'prepare-executor',
section_header: true,
},
{
offset: 1003,
content: [{ text: 'Starting service postgres:9.6.14 ...' }],
- sections: ['prepare-executor'],
+ section: 'prepare-executor',
},
{
offset: 1004,
content: [{ text: 'Pulling docker image postgres:9.6.14 ...', style: 'term-fg-l-green' }],
- sections: ['prepare-executor'],
+ section: 'prepare-executor',
},
{
offset: 1005,
content: [],
- sections: ['prepare-executor'],
+ section: 'prepare-executor',
section_duration: '10:00',
},
];
@@ -100,7 +100,7 @@ export const headerTrace = [
text: 'log line',
},
],
- sections: ['section'],
+ section: 'section',
},
];
@@ -113,7 +113,7 @@ export const headerTraceIncremental = [
text: 'updated log line',
},
],
- sections: ['section'],
+ section: 'section',
},
];
@@ -126,7 +126,7 @@ export const collapsibleTrace = [
text: 'log line',
},
],
- sections: ['section'],
+ section: 'section',
},
{
offset: 2,
@@ -135,7 +135,7 @@ export const collapsibleTrace = [
text: 'log line',
},
],
- sections: ['section'],
+ section: 'section',
},
];
@@ -147,6 +147,48 @@ export const collapsibleTraceIncremental = [
text: 'updated log line',
},
],
- sections: ['section'],
+ section: 'section',
},
];
+
+export const collapsibleSectionClosed = {
+ offset: 5,
+ section_header: true,
+ isHeader: true,
+ isClosed: true,
+ line: {
+ content: [{ text: 'foo' }],
+ section: 'prepare-script',
+ lineNumber: 1,
+ },
+ section_duration: '00:03',
+ lines: [
+ {
+ offset: 80,
+ content: [{ text: 'this is a collapsible nested section' }],
+ section: 'prepare-script',
+ lineNumber: 3,
+ },
+ ],
+};
+
+export const collapsibleSectionOpened = {
+ offset: 5,
+ section_header: true,
+ isHeader: true,
+ isClosed: false,
+ line: {
+ content: [{ text: 'foo' }],
+ section: 'prepare-script',
+ lineNumber: 1,
+ },
+ section_duration: '00:03',
+ lines: [
+ {
+ offset: 80,
+ content: [{ text: 'this is a collapsible nested section' }],
+ section: 'prepare-script',
+ lineNumber: 3,
+ },
+ ],
+};
diff --git a/spec/frontend/jobs/store/mutations_spec.js b/spec/frontend/jobs/store/mutations_spec.js
index 8e5ab4b229a..d1ab152330e 100644
--- a/spec/frontend/jobs/store/mutations_spec.js
+++ b/spec/frontend/jobs/store/mutations_spec.js
@@ -73,12 +73,88 @@ describe('Jobs Store Mutations', () => {
html,
size: 511846,
complete: true,
+ lines: [],
});
expect(stateCopy.trace).toEqual(html);
expect(stateCopy.traceSize).toEqual(511846);
expect(stateCopy.isTraceComplete).toEqual(true);
});
+
+ describe('with new job log', () => {
+ let stateWithNewLog;
+ beforeEach(() => {
+ gon.features = gon.features || {};
+ gon.features.jobLogJson = true;
+
+ stateWithNewLog = state();
+ });
+
+ afterEach(() => {
+ gon.features.jobLogJson = false;
+ });
+
+ describe('log.lines', () => {
+ describe('when append is true', () => {
+ it('sets the parsed log ', () => {
+ mutations[types.RECEIVE_TRACE_SUCCESS](stateWithNewLog, {
+ append: true,
+ size: 511846,
+ complete: true,
+ lines: [
+ {
+ offset: 1,
+ content: [{ text: 'Running with gitlab-runner 11.12.1 (5a147c92)' }],
+ },
+ ],
+ });
+
+ expect(stateWithNewLog.trace).toEqual([
+ {
+ offset: 1,
+ content: [{ text: 'Running with gitlab-runner 11.12.1 (5a147c92)' }],
+ lineNumber: 0,
+ },
+ ]);
+ });
+ });
+
+ describe('when it is defined', () => {
+ it('sets the parsed log ', () => {
+ mutations[types.RECEIVE_TRACE_SUCCESS](stateWithNewLog, {
+ append: false,
+ size: 511846,
+ complete: true,
+ lines: [
+ { offset: 0, content: [{ text: 'Running with gitlab-runner 11.11.1 (5a147c92)' }] },
+ ],
+ });
+
+ expect(stateWithNewLog.trace).toEqual([
+ {
+ offset: 0,
+ content: [{ text: 'Running with gitlab-runner 11.11.1 (5a147c92)' }],
+ lineNumber: 0,
+ },
+ ]);
+ });
+ });
+
+ describe('when it is null', () => {
+ it('sets the default value', () => {
+ mutations[types.RECEIVE_TRACE_SUCCESS](stateWithNewLog, {
+ append: true,
+ html,
+ size: 511846,
+ complete: false,
+ lines: null,
+ });
+
+ expect(stateWithNewLog.trace).toEqual([]);
+ });
+ });
+ });
+ });
});
describe('STOP_POLLING_TRACE', () => {
diff --git a/spec/frontend/jobs/store/utils_spec.js b/spec/frontend/jobs/store/utils_spec.js
index 780d42fd6a1..43dacfe622c 100644
--- a/spec/frontend/jobs/store/utils_spec.js
+++ b/spec/frontend/jobs/store/utils_spec.js
@@ -1,4 +1,13 @@
-import { logLinesParser, updateIncrementalTrace } from '~/jobs/store/utils';
+import {
+ logLinesParser,
+ updateIncrementalTrace,
+ parseHeaderLine,
+ parseLine,
+ addDurationToHeader,
+ isCollapsibleSection,
+ findOffsetAndRemove,
+ getIncrementalLineNumber,
+} from '~/jobs/store/utils';
import {
utilsMockData,
originalTrace,
@@ -11,6 +20,153 @@ import {
} from '../components/log/mock_data';
describe('Jobs Store Utils', () => {
+ describe('parseHeaderLine', () => {
+ it('returns a new object with the header keys and the provided line parsed', () => {
+ const headerLine = { content: [{ text: 'foo' }] };
+ const parsedHeaderLine = parseHeaderLine(headerLine, 2);
+
+ expect(parsedHeaderLine).toEqual({
+ isClosed: true,
+ isHeader: true,
+ line: {
+ ...headerLine,
+ lineNumber: 2,
+ },
+ lines: [],
+ });
+ });
+ });
+
+ describe('parseLine', () => {
+ it('returns a new object with the lineNumber key added to the provided line object', () => {
+ const line = { content: [{ text: 'foo' }] };
+ const parsed = parseLine(line, 1);
+ expect(parsed.content).toEqual(line.content);
+ expect(parsed.lineNumber).toEqual(1);
+ });
+ });
+
+ describe('addDurationToHeader', () => {
+ const duration = {
+ offset: 106,
+ content: [],
+ section: 'prepare-script',
+ section_duration: '00:03',
+ };
+
+ it('adds the section duration to the correct header', () => {
+ const parsed = [
+ {
+ isClosed: true,
+ isHeader: true,
+ line: {
+ section: 'prepare-script',
+ content: [{ text: 'foo' }],
+ },
+ lines: [],
+ },
+ {
+ isClosed: true,
+ isHeader: true,
+ line: {
+ section: 'foo-bar',
+ content: [{ text: 'foo' }],
+ },
+ lines: [],
+ },
+ ];
+
+ addDurationToHeader(parsed, duration);
+
+ expect(parsed[0].line.section_duration).toEqual(duration.section_duration);
+ expect(parsed[1].line.section_duration).toEqual(undefined);
+ });
+
+ it('does not add the section duration when the headers do not match', () => {
+ const parsed = [
+ {
+ isClosed: true,
+ isHeader: true,
+ line: {
+ section: 'bar-foo',
+ content: [{ text: 'foo' }],
+ },
+ lines: [],
+ },
+ {
+ isClosed: true,
+ isHeader: true,
+ line: {
+ section: 'foo-bar',
+ content: [{ text: 'foo' }],
+ },
+ lines: [],
+ },
+ ];
+ addDurationToHeader(parsed, duration);
+
+ expect(parsed[0].line.section_duration).toEqual(undefined);
+ expect(parsed[1].line.section_duration).toEqual(undefined);
+ });
+
+ it('does not add when content has no headers', () => {
+ const parsed = [
+ {
+ section: 'bar-foo',
+ content: [{ text: 'foo' }],
+ lineNumber: 1,
+ },
+ {
+ section: 'foo-bar',
+ content: [{ text: 'foo' }],
+ lineNumber: 2,
+ },
+ ];
+
+ addDurationToHeader(parsed, duration);
+
+ expect(parsed[0].line).toEqual(undefined);
+ expect(parsed[1].line).toEqual(undefined);
+ });
+ });
+
+ describe('isCollapsibleSection', () => {
+ const header = {
+ isHeader: true,
+ line: {
+ section: 'foo',
+ },
+ };
+ const line = {
+ lineNumber: 1,
+ section: 'foo',
+ content: [],
+ };
+
+ it('returns true when line belongs to the last section', () => {
+ expect(isCollapsibleSection([header], header, { section: 'foo', content: [] })).toEqual(true);
+ });
+
+ it('returns false when last line was not an header', () => {
+ expect(isCollapsibleSection([line], line, { section: 'bar' })).toEqual(false);
+ });
+
+ it('returns false when accumulator is empty', () => {
+ expect(isCollapsibleSection([], { isHeader: true }, { section: 'bar' })).toEqual(false);
+ });
+
+ it('returns false when section_duration is defined', () => {
+ expect(isCollapsibleSection([header], header, { section_duration: '10:00' })).toEqual(false);
+ });
+
+ it('returns false when `section` is not a match', () => {
+ expect(isCollapsibleSection([header], header, { section: 'bar' })).toEqual(false);
+ });
+
+ it('returns false when no parameters are provided', () => {
+ expect(isCollapsibleSection()).toEqual(false);
+ });
+ });
describe('logLinesParser', () => {
let result;
@@ -43,7 +199,7 @@ describe('Jobs Store Utils', () => {
describe('section duration', () => {
it('adds the section information to the header section', () => {
- expect(result[1].section_duration).toEqual(utilsMockData[4].section_duration);
+ expect(result[1].line.section_duration).toEqual(utilsMockData[4].section_duration);
});
it('does not add section duration as a line', () => {
@@ -52,11 +208,183 @@ describe('Jobs Store Utils', () => {
});
});
+ describe('findOffsetAndRemove', () => {
+ describe('when last item is header', () => {
+ const existingLog = [
+ {
+ isHeader: true,
+ isClosed: true,
+ line: { content: [{ text: 'bar' }], offset: 10, lineNumber: 1 },
+ },
+ ];
+
+ describe('and matches the offset', () => {
+ it('returns an array with the item removed', () => {
+ const newData = [{ offset: 10, content: [{ text: 'foobar' }] }];
+ const result = findOffsetAndRemove(newData, existingLog);
+
+ expect(result).toEqual([]);
+ });
+ });
+
+ describe('and does not match the offset', () => {
+ it('returns the provided existing log', () => {
+ const newData = [{ offset: 110, content: [{ text: 'foobar' }] }];
+ const result = findOffsetAndRemove(newData, existingLog);
+
+ expect(result).toEqual(existingLog);
+ });
+ });
+ });
+
+ describe('when last item is a regular line', () => {
+ const existingLog = [{ content: [{ text: 'bar' }], offset: 10, lineNumber: 1 }];
+
+ describe('and matches the offset', () => {
+ it('returns an array with the item removed', () => {
+ const newData = [{ offset: 10, content: [{ text: 'foobar' }] }];
+ const result = findOffsetAndRemove(newData, existingLog);
+
+ expect(result).toEqual([]);
+ });
+ });
+
+ describe('and does not match the fofset', () => {
+ it('returns the provided old log', () => {
+ const newData = [{ offset: 101, content: [{ text: 'foobar' }] }];
+ const result = findOffsetAndRemove(newData, existingLog);
+
+ expect(result).toEqual(existingLog);
+ });
+ });
+ });
+
+ describe('when last item is nested', () => {
+ const existingLog = [
+ {
+ isHeader: true,
+ isClosed: true,
+ lines: [{ offset: 101, content: [{ text: 'foobar' }], lineNumber: 2 }],
+ line: {
+ offset: 10,
+ lineNumber: 1,
+ section_duration: '10:00',
+ },
+ },
+ ];
+
+ describe('and matches the offset', () => {
+ it('returns an array with the last nested line item removed', () => {
+ const newData = [{ offset: 101, content: [{ text: 'foobar' }] }];
+
+ const result = findOffsetAndRemove(newData, existingLog);
+ expect(result[0].lines).toEqual([]);
+ });
+ });
+
+ describe('and does not match the offset', () => {
+ it('returns the provided old log', () => {
+ const newData = [{ offset: 120, content: [{ text: 'foobar' }] }];
+
+ const result = findOffsetAndRemove(newData, existingLog);
+ expect(result).toEqual(existingLog);
+ });
+ });
+ });
+
+ describe('when no data is provided', () => {
+ it('returns an empty array', () => {
+ const result = findOffsetAndRemove();
+ expect(result).toEqual([]);
+ });
+ });
+ });
+
+ describe('getIncrementalLineNumber', () => {
+ describe('when last line is 0', () => {
+ it('returns 1', () => {
+ const log = [
+ {
+ content: [],
+ lineNumber: 0,
+ },
+ ];
+
+ expect(getIncrementalLineNumber(log)).toEqual(1);
+ });
+ });
+
+ describe('with unnested line', () => {
+ it('returns the lineNumber of the last item in the array', () => {
+ const log = [
+ {
+ content: [],
+ lineNumber: 10,
+ },
+ {
+ content: [],
+ lineNumber: 101,
+ },
+ ];
+
+ expect(getIncrementalLineNumber(log)).toEqual(102);
+ });
+ });
+
+ describe('when last line is the header section', () => {
+ it('returns the lineNumber of the last item in the array', () => {
+ const log = [
+ {
+ content: [],
+ lineNumber: 10,
+ },
+ {
+ isHeader: true,
+ line: {
+ lineNumber: 101,
+ content: [],
+ },
+ lines: [],
+ },
+ ];
+
+ expect(getIncrementalLineNumber(log)).toEqual(102);
+ });
+ });
+
+ describe('when last line is a nested line', () => {
+ it('returns the lineNumber of the last item in the nested array', () => {
+ const log = [
+ {
+ content: [],
+ lineNumber: 10,
+ },
+ {
+ isHeader: true,
+ line: {
+ lineNumber: 101,
+ content: [],
+ },
+ lines: [
+ {
+ lineNumber: 102,
+ content: [],
+ },
+ { lineNumber: 103, content: [] },
+ ],
+ },
+ ];
+
+ expect(getIncrementalLineNumber(log)).toEqual(104);
+ });
+ });
+ });
+
describe('updateIncrementalTrace', () => {
describe('without repeated section', () => {
it('concats and parses both arrays', () => {
const oldLog = logLinesParser(originalTrace);
- const result = updateIncrementalTrace(originalTrace, oldLog, regularIncremental);
+ const result = updateIncrementalTrace(regularIncremental, oldLog);
expect(result).toEqual([
{
@@ -84,7 +412,7 @@ describe('Jobs Store Utils', () => {
describe('with regular line repeated offset', () => {
it('updates the last line and formats with the incremental part', () => {
const oldLog = logLinesParser(originalTrace);
- const result = updateIncrementalTrace(originalTrace, oldLog, regularIncrementalRepeated);
+ const result = updateIncrementalTrace(regularIncrementalRepeated, oldLog);
expect(result).toEqual([
{
@@ -103,7 +431,7 @@ describe('Jobs Store Utils', () => {
describe('with header line repeated', () => {
it('updates the header line and formats with the incremental part', () => {
const oldLog = logLinesParser(headerTrace);
- const result = updateIncrementalTrace(headerTrace, oldLog, headerTraceIncremental);
+ const result = updateIncrementalTrace(headerTraceIncremental, oldLog);
expect(result).toEqual([
{
@@ -117,7 +445,7 @@ describe('Jobs Store Utils', () => {
text: 'updated log line',
},
],
- sections: ['section'],
+ section: 'section',
lineNumber: 0,
},
lines: [],
@@ -129,11 +457,7 @@ describe('Jobs Store Utils', () => {
describe('with collapsible line repeated', () => {
it('updates the collapsible line and formats with the incremental part', () => {
const oldLog = logLinesParser(collapsibleTrace);
- const result = updateIncrementalTrace(
- collapsibleTrace,
- oldLog,
- collapsibleTraceIncremental,
- );
+ const result = updateIncrementalTrace(collapsibleTraceIncremental, oldLog);
expect(result).toEqual([
{
@@ -147,7 +471,7 @@ describe('Jobs Store Utils', () => {
text: 'log line',
},
],
- sections: ['section'],
+ section: 'section',
lineNumber: 0,
},
lines: [
@@ -158,7 +482,7 @@ describe('Jobs Store Utils', () => {
text: 'updated log line',
},
],
- sections: ['section'],
+ section: 'section',
lineNumber: 1,
},
],
diff --git a/spec/frontend/lib/utils/datetime_utility_spec.js b/spec/frontend/lib/utils/datetime_utility_spec.js
index 9f1700bb243..e2e71229320 100644
--- a/spec/frontend/lib/utils/datetime_utility_spec.js
+++ b/spec/frontend/lib/utils/datetime_utility_spec.js
@@ -388,20 +388,6 @@ describe('prettyTime methods', () => {
expect(datetimeUtility.stringifyTime(timeObject, true)).toEqual('1 week 1 hour');
});
});
-
- describe('abbreviateTime', () => {
- it('should abbreviate stringified times for weeks', () => {
- const fullTimeString = '1w 3d 4h 5m';
-
- expect(datetimeUtility.abbreviateTime(fullTimeString)).toBe('1w');
- });
-
- it('should abbreviate stringified times for non-weeks', () => {
- const fullTimeString = '0w 3d 4h 5m';
-
- expect(datetimeUtility.abbreviateTime(fullTimeString)).toBe('3d');
- });
- });
});
describe('calculateRemainingMilliseconds', () => {
@@ -440,3 +426,18 @@ describe('newDate', () => {
expect(initialDate instanceof Date).toBe(true);
});
});
+
+describe('getDateInPast', () => {
+ const date = new Date(1563235200000); // 2019-07-16T00:00:00.000Z;
+ const daysInPast = 90;
+
+ it('returns the correct date in the past', () => {
+ const dateInPast = datetimeUtility.getDateInPast(date, daysInPast);
+ expect(dateInPast).toBe('2019-04-17T00:00:00.000Z');
+ });
+
+ it('does not modifiy the original date', () => {
+ datetimeUtility.getDateInPast(date, daysInPast);
+ expect(date).toStrictEqual(new Date(1563235200000));
+ });
+});
diff --git a/spec/frontend/lib/utils/number_utility_spec.js b/spec/frontend/lib/utils/number_utility_spec.js
index 77d7478d317..381d7c6f8d9 100644
--- a/spec/frontend/lib/utils/number_utility_spec.js
+++ b/spec/frontend/lib/utils/number_utility_spec.js
@@ -6,6 +6,7 @@ import {
numberToHumanSize,
sum,
isOdd,
+ median,
} from '~/lib/utils/number_utils';
describe('Number Utils', () => {
@@ -109,4 +110,16 @@ describe('Number Utils', () => {
expect(isOdd(1)).toEqual(1);
});
});
+
+ describe('median', () => {
+ it('computes the median for a given array with odd length', () => {
+ const items = [10, 27, 20, 5, 19];
+ expect(median(items)).toBe(19);
+ });
+
+ it('computes the median for a given array with even length', () => {
+ const items = [10, 27, 20, 5, 19, 4];
+ expect(median(items)).toBe(14.5);
+ });
+ });
});
diff --git a/spec/frontend/lib/utils/set_spec.js b/spec/frontend/lib/utils/set_spec.js
new file mode 100644
index 00000000000..7636a1c634c
--- /dev/null
+++ b/spec/frontend/lib/utils/set_spec.js
@@ -0,0 +1,19 @@
+import { isSubset } from '~/lib/utils/set';
+
+describe('utils/set', () => {
+ describe('isSubset', () => {
+ it.each`
+ subset | superset | expected
+ ${new Set()} | ${new Set()} | ${true}
+ ${new Set()} | ${new Set([1])} | ${true}
+ ${new Set([1])} | ${new Set([1])} | ${true}
+ ${new Set([1, 3])} | ${new Set([1, 2, 3])} | ${true}
+ ${new Set([1])} | ${new Set()} | ${false}
+ ${new Set([1])} | ${new Set([2])} | ${false}
+ ${new Set([7, 8, 9])} | ${new Set([1, 2, 3])} | ${false}
+ ${new Set([1, 2, 3, 4])} | ${new Set([1, 2, 3])} | ${false}
+ `('isSubset($subset, $superset) === $expected', ({ subset, superset, expected }) => {
+ expect(isSubset(subset, superset)).toBe(expected);
+ });
+ });
+});
diff --git a/spec/frontend/lib/utils/suppress_ajax_errors_during_navigation_spec.js b/spec/frontend/lib/utils/suppress_ajax_errors_during_navigation_spec.js
new file mode 100644
index 00000000000..89e8459d594
--- /dev/null
+++ b/spec/frontend/lib/utils/suppress_ajax_errors_during_navigation_spec.js
@@ -0,0 +1,37 @@
+import suppressAjaxErrorsDuringNavigation from '~/lib/utils/suppress_ajax_errors_during_navigation';
+import waitForPromises from 'helpers/wait_for_promises';
+
+describe('suppressAjaxErrorsDuringNavigation', () => {
+ const OTHER_ERR_CODE = 'foo';
+ const NAV_ERR_CODE = 'ECONNABORTED';
+
+ it.each`
+ isFeatureFlagEnabled | isUserNavigating | code
+ ${false} | ${false} | ${OTHER_ERR_CODE}
+ ${false} | ${false} | ${NAV_ERR_CODE}
+ ${false} | ${true} | ${OTHER_ERR_CODE}
+ ${false} | ${true} | ${NAV_ERR_CODE}
+ ${true} | ${false} | ${OTHER_ERR_CODE}
+ ${true} | ${false} | ${NAV_ERR_CODE}
+ ${true} | ${true} | ${OTHER_ERR_CODE}
+ `('should return a rejected Promise', ({ isFeatureFlagEnabled, isUserNavigating, code }) => {
+ const err = { code };
+ const actual = suppressAjaxErrorsDuringNavigation(err, isUserNavigating, isFeatureFlagEnabled);
+
+ return expect(actual).rejects.toBe(err);
+ });
+
+ it('should return a Promise that never resolves', () => {
+ const err = { code: NAV_ERR_CODE };
+ const actual = suppressAjaxErrorsDuringNavigation(err, true, true);
+
+ const thenCallback = jest.fn();
+ const catchCallback = jest.fn();
+ actual.then(thenCallback).catch(catchCallback);
+
+ return waitForPromises().then(() => {
+ expect(thenCallback).not.toHaveBeenCalled();
+ expect(catchCallback).not.toHaveBeenCalled();
+ });
+ });
+});
diff --git a/spec/frontend/lib/utils/url_utility_spec.js b/spec/frontend/lib/utils/url_utility_spec.js
index 41df93c9a48..6edb2e2dce2 100644
--- a/spec/frontend/lib/utils/url_utility_spec.js
+++ b/spec/frontend/lib/utils/url_utility_spec.js
@@ -136,6 +136,24 @@ describe('URL utility', () => {
});
});
+ describe('doesHashExistInUrl', () => {
+ it('should return true when the given string exists in the URL hash', () => {
+ setWindowLocation({
+ href: 'https://gitlab.com/gitlab-org/gitlab-test/issues/1#note_1',
+ });
+
+ expect(urlUtils.doesHashExistInUrl('note_')).toBe(true);
+ });
+
+ it('should return false when the given string does not exist in the URL hash', () => {
+ setWindowLocation({
+ href: 'https://gitlab.com/gitlab-org/gitlab-test/issues/1#note_1',
+ });
+
+ expect(urlUtils.doesHashExistInUrl('doesnotexist')).toBe(false);
+ });
+ });
+
describe('setUrlFragment', () => {
it('should set fragment when url has no fragment', () => {
const url = urlUtils.setUrlFragment('/home/feature', 'usage');
diff --git a/spec/frontend/monitoring/components/date_time_picker/date_time_picker_input_spec.js b/spec/frontend/monitoring/components/date_time_picker/date_time_picker_input_spec.js
new file mode 100644
index 00000000000..1315e1226a4
--- /dev/null
+++ b/spec/frontend/monitoring/components/date_time_picker/date_time_picker_input_spec.js
@@ -0,0 +1,66 @@
+import { mount } from '@vue/test-utils';
+import DateTimePickerInput from '~/monitoring/components/date_time_picker/date_time_picker_input.vue';
+
+const inputLabel = 'This is a label';
+const inputValue = 'something';
+
+describe('DateTimePickerInput', () => {
+ let wrapper;
+
+ const createComponent = (propsData = {}) => {
+ wrapper = mount(DateTimePickerInput, {
+ propsData: {
+ state: null,
+ value: '',
+ label: '',
+ ...propsData,
+ },
+ sync: false,
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders label above the input', () => {
+ createComponent({
+ label: inputLabel,
+ });
+
+ expect(wrapper.find('.gl-form-group label').text()).toBe(inputLabel);
+ });
+
+ it('renders the same `ID` for input and `for` for label', () => {
+ createComponent({ label: inputLabel });
+
+ expect(wrapper.find('.gl-form-group label').attributes('for')).toBe(
+ wrapper.find('input').attributes('id'),
+ );
+ });
+
+ it('renders valid input in gray color instead of green', () => {
+ createComponent({
+ state: true,
+ });
+
+ expect(wrapper.find('input').classes('is-valid')).toBe(false);
+ });
+
+ it('renders invalid input in red color', () => {
+ createComponent({
+ state: false,
+ });
+
+ expect(wrapper.find('input').classes('is-invalid')).toBe(true);
+ });
+
+ it('input event is emitted when focus is lost', () => {
+ createComponent();
+ jest.spyOn(wrapper.vm, '$emit');
+ wrapper.find('input').setValue(inputValue);
+ wrapper.find('input').trigger('blur');
+
+ expect(wrapper.vm.$emit).toHaveBeenCalledWith('input', inputValue);
+ });
+});
diff --git a/spec/frontend/monitoring/components/date_time_picker/date_time_picker_spec.js b/spec/frontend/monitoring/components/date_time_picker/date_time_picker_spec.js
new file mode 100644
index 00000000000..be544435671
--- /dev/null
+++ b/spec/frontend/monitoring/components/date_time_picker/date_time_picker_spec.js
@@ -0,0 +1,157 @@
+import { mount } from '@vue/test-utils';
+import DateTimePicker from '~/monitoring/components/date_time_picker/date_time_picker.vue';
+import { timeWindows } from '~/monitoring/constants';
+
+const timeWindowsCount = Object.keys(timeWindows).length;
+const selectedTimeWindow = {
+ start: '2019-10-10T07:00:00.000Z',
+ end: '2019-10-13T07:00:00.000Z',
+};
+const selectedTimeWindowText = `3 days`;
+
+describe('DateTimePicker', () => {
+ let dateTimePicker;
+
+ const dropdownToggle = () => dateTimePicker.find('.dropdown-toggle');
+ const dropdownMenu = () => dateTimePicker.find('.dropdown-menu');
+ const applyButtonElement = () => dateTimePicker.find('button[variant="success"]').element;
+ const cancelButtonElement = () => dateTimePicker.find('button.btn-secondary').element;
+ const fillInputAndBlur = (input, val) => {
+ dateTimePicker.find(input).setValue(val);
+ dateTimePicker.find(input).trigger('blur');
+ };
+
+ const createComponent = props => {
+ dateTimePicker = mount(DateTimePicker, {
+ propsData: {
+ timeWindows,
+ selectedTimeWindow,
+ ...props,
+ },
+ sync: false,
+ });
+ };
+
+ afterEach(() => {
+ dateTimePicker.destroy();
+ });
+
+ it('renders dropdown toggle button with selected text', done => {
+ createComponent();
+ dateTimePicker.vm.$nextTick(() => {
+ expect(dropdownToggle().text()).toBe(selectedTimeWindowText);
+ done();
+ });
+ });
+
+ it('renders dropdown with 2 custom time range inputs', () => {
+ createComponent();
+ dateTimePicker.vm.$nextTick(() => {
+ expect(dateTimePicker.findAll('input').length).toBe(2);
+ });
+ });
+
+ it('renders inputs with h/m/s truncated if its all 0s', done => {
+ createComponent({
+ selectedTimeWindow: {
+ start: '2019-10-10T00:00:00.000Z',
+ end: '2019-10-14T00:10:00.000Z',
+ },
+ });
+ dateTimePicker.vm.$nextTick(() => {
+ expect(dateTimePicker.find('#custom-time-from').element.value).toBe('2019-10-10');
+ expect(dateTimePicker.find('#custom-time-to').element.value).toBe('2019-10-14 00:10:00');
+ done();
+ });
+ });
+
+ it(`renders dropdown with ${timeWindowsCount} items in quick range`, done => {
+ createComponent();
+ dropdownToggle().trigger('click');
+ dateTimePicker.vm.$nextTick(() => {
+ expect(dateTimePicker.findAll('.dropdown-item').length).toBe(timeWindowsCount);
+ done();
+ });
+ });
+
+ it(`renders dropdown with correct quick range item selected`, done => {
+ createComponent();
+ dropdownToggle().trigger('click');
+ dateTimePicker.vm.$nextTick(() => {
+ expect(dateTimePicker.find('.dropdown-item.active').text()).toBe(selectedTimeWindowText);
+
+ expect(dateTimePicker.find('.dropdown-item.active svg').isVisible()).toBe(true);
+ done();
+ });
+ });
+
+ it('renders a disabled apply button on load', () => {
+ createComponent();
+
+ expect(applyButtonElement().getAttribute('disabled')).toBe('disabled');
+ });
+
+ it('displays inline error message if custom time range inputs are invalid', done => {
+ createComponent();
+ fillInputAndBlur('#custom-time-from', '2019-10-01abc');
+ fillInputAndBlur('#custom-time-to', '2019-10-10abc');
+
+ dateTimePicker.vm.$nextTick(() => {
+ expect(dateTimePicker.findAll('.invalid-feedback').length).toBe(2);
+ done();
+ });
+ });
+
+ it('keeps apply button disabled with invalid custom time range inputs', done => {
+ createComponent();
+ fillInputAndBlur('#custom-time-from', '2019-10-01abc');
+ fillInputAndBlur('#custom-time-to', '2019-09-19');
+
+ dateTimePicker.vm.$nextTick(() => {
+ expect(applyButtonElement().getAttribute('disabled')).toBe('disabled');
+ done();
+ });
+ });
+
+ it('enables apply button with valid custom time range inputs', done => {
+ createComponent();
+ fillInputAndBlur('#custom-time-from', '2019-10-01');
+ fillInputAndBlur('#custom-time-to', '2019-10-19');
+
+ dateTimePicker.vm.$nextTick(() => {
+ expect(applyButtonElement().getAttribute('disabled')).toBeNull();
+ done();
+ });
+ });
+
+ it('returns an object when apply is clicked', done => {
+ createComponent();
+ fillInputAndBlur('#custom-time-from', '2019-10-01');
+ fillInputAndBlur('#custom-time-to', '2019-10-19');
+
+ dateTimePicker.vm.$nextTick(() => {
+ jest.spyOn(dateTimePicker.vm, '$emit');
+ applyButtonElement().click();
+
+ expect(dateTimePicker.vm.$emit).toHaveBeenCalledWith('onApply', {
+ end: '2019-10-19T00:00:00Z',
+ start: '2019-10-01T00:00:00Z',
+ });
+ done();
+ });
+ });
+
+ it('hides the popover with cancel button', done => {
+ createComponent();
+ dropdownToggle().trigger('click');
+
+ dateTimePicker.vm.$nextTick(() => {
+ cancelButtonElement().click();
+
+ dateTimePicker.vm.$nextTick(() => {
+ expect(dropdownMenu().classes('show')).toBe(false);
+ done();
+ });
+ });
+ });
+});
diff --git a/spec/frontend/monitoring/embed/embed_spec.js b/spec/frontend/monitoring/embed/embed_spec.js
index 1ce14e2418a..5de1a7c4c3b 100644
--- a/spec/frontend/monitoring/embed/embed_spec.js
+++ b/spec/frontend/monitoring/embed/embed_spec.js
@@ -74,5 +74,9 @@ describe('Embed', () => {
expect(wrapper.find(MonitorTimeSeriesChart).exists()).toBe(true);
expect(wrapper.findAll(MonitorTimeSeriesChart).length).toBe(2);
});
+
+ it('includes groupId with dashboardUrl', () => {
+ expect(wrapper.find(MonitorTimeSeriesChart).props('groupId')).toBe(TEST_HOST);
+ });
});
});
diff --git a/spec/frontend/monitoring/utils_spec.js b/spec/frontend/monitoring/utils_spec.js
new file mode 100644
index 00000000000..1e8d5753885
--- /dev/null
+++ b/spec/frontend/monitoring/utils_spec.js
@@ -0,0 +1,54 @@
+import * as monitoringUtils from '~/monitoring/utils';
+
+describe('Snowplow Events', () => {
+ const generatedLink = 'http://chart.link.com';
+ const chartTitle = 'Some metric chart';
+
+ describe('trackGenerateLinkToChartEventOptions', () => {
+ it('should return Cluster Monitoring options if located on Cluster Health Dashboard', () => {
+ document.body.dataset.page = 'groups:clusters:show';
+
+ expect(monitoringUtils.generateLinkToChartOptions(generatedLink)).toEqual({
+ category: 'Cluster Monitoring',
+ action: 'generate_link_to_cluster_metric_chart',
+ label: 'Chart link',
+ property: generatedLink,
+ });
+ });
+
+ it('should return Incident Management event options if located on Metrics Dashboard', () => {
+ document.body.dataset.page = 'metrics:show';
+
+ expect(monitoringUtils.generateLinkToChartOptions(generatedLink)).toEqual({
+ category: 'Incident Management::Embedded metrics',
+ action: 'generate_link_to_metrics_chart',
+ label: 'Chart link',
+ property: generatedLink,
+ });
+ });
+ });
+
+ describe('trackDownloadCSVEvent', () => {
+ it('should return Cluster Monitoring options if located on Cluster Health Dashboard', () => {
+ document.body.dataset.page = 'groups:clusters:show';
+
+ expect(monitoringUtils.downloadCSVOptions(chartTitle)).toEqual({
+ category: 'Cluster Monitoring',
+ action: 'download_csv_of_cluster_metric_chart',
+ label: 'Chart title',
+ property: chartTitle,
+ });
+ });
+
+ it('should return Incident Management event options if located on Metrics Dashboard', () => {
+ document.body.dataset.page = 'metriss:show';
+
+ expect(monitoringUtils.downloadCSVOptions(chartTitle)).toEqual({
+ category: 'Incident Management::Embedded metrics',
+ action: 'download_csv_of_metrics_dashboard_chart',
+ label: 'Chart title',
+ property: chartTitle,
+ });
+ });
+ });
+});
diff --git a/spec/frontend/notes/components/__snapshots__/discussion_jump_to_next_button_spec.js.snap b/spec/frontend/notes/components/__snapshots__/discussion_jump_to_next_button_spec.js.snap
index 11d65ced180..b29d093130a 100644
--- a/spec/frontend/notes/components/__snapshots__/discussion_jump_to_next_button_spec.js.snap
+++ b/spec/frontend/notes/components/__snapshots__/discussion_jump_to_next_button_spec.js.snap
@@ -11,7 +11,6 @@ exports[`JumpToNextDiscussionButton matches the snapshot 1`] = `
title=""
>
<icon-stub
- cssclasses=""
name="comment-next"
size="16"
/>
diff --git a/spec/frontend/pages/admin/users/components/__snapshots__/delete_user_modal_spec.js.snap b/spec/frontend/pages/admin/users/components/__snapshots__/delete_user_modal_spec.js.snap
new file mode 100644
index 00000000000..78a736a9060
--- /dev/null
+++ b/spec/frontend/pages/admin/users/components/__snapshots__/delete_user_modal_spec.js.snap
@@ -0,0 +1,63 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`User Operation confirmation modal renders modal with form included 1`] = `
+<div>
+ <p>
+ content
+ </p>
+
+ <p>
+ To confirm, type
+ <code>
+ username
+ </code>
+ </p>
+
+ <form
+ action="delete-url"
+ method="post"
+ >
+ <input
+ name="_method"
+ type="hidden"
+ value="delete"
+ />
+
+ <input
+ name="authenticity_token"
+ type="hidden"
+ value="csrf"
+ />
+
+ <glforminput-stub
+ autocomplete="off"
+ autofocus=""
+ name="username"
+ type="text"
+ value=""
+ />
+ </form>
+
+ <glbutton-stub
+ variant="secondary"
+ >
+ Cancel
+ </glbutton-stub>
+
+ <glbutton-stub
+ disabled="true"
+ variant="warning"
+ >
+
+ secondaryAction
+
+ </glbutton-stub>
+
+ <glbutton-stub
+ disabled="true"
+ variant="danger"
+ >
+ action
+ </glbutton-stub>
+</div>
+`;
diff --git a/spec/frontend/pages/admin/users/components/__snapshots__/user_operation_confirmation_modal_spec.js.snap b/spec/frontend/pages/admin/users/components/__snapshots__/user_operation_confirmation_modal_spec.js.snap
new file mode 100644
index 00000000000..4a3989f5192
--- /dev/null
+++ b/spec/frontend/pages/admin/users/components/__snapshots__/user_operation_confirmation_modal_spec.js.snap
@@ -0,0 +1,33 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`User Operation confirmation modal renders modal with form included 1`] = `
+<glmodal-stub
+ modalclass=""
+ modalid="user-operation-modal"
+ ok-title="action"
+ ok-variant="warning"
+ title="title"
+ titletag="h4"
+>
+ <form
+ action="/url"
+ method="post"
+ >
+ <span>
+ content
+ </span>
+
+ <input
+ name="_method"
+ type="hidden"
+ value="method"
+ />
+
+ <input
+ name="authenticity_token"
+ type="hidden"
+ value="csrf"
+ />
+ </form>
+</glmodal-stub>
+`;
diff --git a/spec/frontend/pages/admin/users/components/delete_user_modal_spec.js b/spec/frontend/pages/admin/users/components/delete_user_modal_spec.js
new file mode 100644
index 00000000000..57802a41bb5
--- /dev/null
+++ b/spec/frontend/pages/admin/users/components/delete_user_modal_spec.js
@@ -0,0 +1,85 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlButton, GlFormInput } from '@gitlab/ui';
+import DeleteUserModal from '~/pages/admin/users/components/delete_user_modal.vue';
+import ModalStub from './stubs/modal_stub';
+
+describe('User Operation confirmation modal', () => {
+ let wrapper;
+
+ const findButton = variant =>
+ wrapper
+ .findAll(GlButton)
+ .filter(w => w.attributes('variant') === variant)
+ .at(0);
+
+ const createComponent = (props = {}) => {
+ wrapper = shallowMount(DeleteUserModal, {
+ propsData: {
+ title: 'title',
+ content: 'content',
+ action: 'action',
+ secondaryAction: 'secondaryAction',
+ deleteUserUrl: 'delete-url',
+ blockUserUrl: 'block-url',
+ username: 'username',
+ csrfToken: 'csrf',
+ ...props,
+ },
+ stubs: {
+ GlModal: ModalStub,
+ },
+ sync: false,
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ it('renders modal with form included', () => {
+ createComponent();
+ expect(wrapper.element).toMatchSnapshot();
+ });
+
+ it.each`
+ variant | prop | action
+ ${'danger'} | ${'deleteUserUrl'} | ${'delete'}
+ ${'warning'} | ${'blockUserUrl'} | ${'block'}
+ `('closing modal with $variant button triggers $action', ({ variant, prop }) => {
+ createComponent();
+ const form = wrapper.find('form');
+ jest.spyOn(form.element, 'submit').mockReturnValue();
+ const modalButton = findButton(variant);
+ modalButton.vm.$emit('click');
+ return wrapper.vm.$nextTick().then(() => {
+ expect(form.element.submit).toHaveBeenCalled();
+ expect(form.element.action).toContain(wrapper.props(prop));
+ expect(new FormData(form.element).get('authenticity_token')).toEqual(
+ wrapper.props('csrfToken'),
+ );
+ });
+ });
+
+ it('disables buttons by default', () => {
+ createComponent();
+ const blockButton = findButton('warning');
+ const deleteButton = findButton('danger');
+ expect(blockButton.attributes().disabled).toBeTruthy();
+ expect(deleteButton.attributes().disabled).toBeTruthy();
+ });
+
+ it('enables button when username is typed', () => {
+ createComponent({
+ username: 'some-username',
+ });
+ wrapper.find(GlFormInput).vm.$emit('input', 'some-username');
+ const blockButton = findButton('warning');
+ const deleteButton = findButton('danger');
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(blockButton.attributes().disabled).toBeFalsy();
+ expect(deleteButton.attributes().disabled).toBeFalsy();
+ });
+ });
+});
diff --git a/spec/frontend/pages/admin/users/components/stubs/modal_stub.js b/spec/frontend/pages/admin/users/components/stubs/modal_stub.js
new file mode 100644
index 00000000000..4dc55e909a0
--- /dev/null
+++ b/spec/frontend/pages/admin/users/components/stubs/modal_stub.js
@@ -0,0 +1,23 @@
+const ModalStub = {
+ inheritAttrs: false,
+ name: 'glmodal-stub',
+ data() {
+ return {
+ showWasCalled: false,
+ };
+ },
+ methods: {
+ show() {
+ this.showWasCalled = true;
+ },
+ hide() {},
+ },
+ render(h) {
+ const children = [this.$slots.default, this.$slots['modal-footer']]
+ .filter(Boolean)
+ .reduce((acc, nodes) => acc.concat(nodes), []);
+ return h('div', children);
+ },
+};
+
+export default ModalStub;
diff --git a/spec/frontend/pages/admin/users/components/user_modal_manager_spec.js b/spec/frontend/pages/admin/users/components/user_modal_manager_spec.js
new file mode 100644
index 00000000000..7653fffc502
--- /dev/null
+++ b/spec/frontend/pages/admin/users/components/user_modal_manager_spec.js
@@ -0,0 +1,148 @@
+import { shallowMount } from '@vue/test-utils';
+import UserModalManager from '~/pages/admin/users/components/user_modal_manager.vue';
+import ModalStub from './stubs/modal_stub';
+
+describe('Users admin page Modal Manager', () => {
+ const modalConfiguration = {
+ action1: {
+ title: 'action1',
+ content: 'Action Modal 1',
+ },
+ action2: {
+ title: 'action2',
+ content: 'Action Modal 2',
+ },
+ };
+
+ const actionModals = {
+ action1: ModalStub,
+ action2: ModalStub,
+ };
+
+ let wrapper;
+
+ const createComponent = (props = {}) => {
+ wrapper = shallowMount(UserModalManager, {
+ propsData: {
+ actionModals,
+ modalConfiguration,
+ csrfToken: 'dummyCSRF',
+ ...props,
+ },
+ stubs: {
+ dummyComponent1: true,
+ dummyComponent2: true,
+ },
+ sync: false,
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ describe('render behavior', () => {
+ it('does not renders modal when initialized', () => {
+ createComponent();
+ expect(wrapper.find({ ref: 'modal' }).exists()).toBeFalsy();
+ });
+
+ it('throws if non-existing action is requested', () => {
+ createComponent();
+ expect(() => wrapper.vm.show({ glModalAction: 'non-existing' })).toThrow();
+ });
+
+ it('throws if action has no proper configuration', () => {
+ createComponent({
+ modalConfiguration: {},
+ });
+ expect(() => wrapper.vm.show({ glModalAction: 'action1' })).toThrow();
+ });
+
+ it('renders modal with expected props when valid configuration is passed', () => {
+ createComponent();
+ wrapper.vm.show({
+ glModalAction: 'action1',
+ extraProp: 'extraPropValue',
+ });
+
+ return wrapper.vm.$nextTick().then(() => {
+ const modal = wrapper.find({ ref: 'modal' });
+ expect(modal.exists()).toBeTruthy();
+ expect(modal.vm.$attrs.csrfToken).toEqual('dummyCSRF');
+ expect(modal.vm.$attrs.extraProp).toEqual('extraPropValue');
+ expect(modal.vm.showWasCalled).toBeTruthy();
+ });
+ });
+ });
+
+ describe('global listener', () => {
+ beforeEach(() => {
+ jest.spyOn(document, 'addEventListener');
+ jest.spyOn(document, 'removeEventListener');
+ });
+
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
+ afterAll(() => {
+ jest.restoreAllMocks();
+ });
+
+ it('registers global listener on mount', () => {
+ createComponent();
+ expect(document.addEventListener).toHaveBeenCalledWith('click', expect.any(Function));
+ });
+
+ it('removes global listener on destroy', () => {
+ createComponent();
+ wrapper.destroy();
+ expect(document.removeEventListener).toHaveBeenCalledWith('click', expect.any(Function));
+ });
+ });
+
+ describe('click handling', () => {
+ let node;
+
+ beforeEach(() => {
+ node = document.createElement('div');
+ document.body.appendChild(node);
+ });
+
+ afterEach(() => {
+ node.remove();
+ node = null;
+ });
+
+ it('ignores wrong clicks', () => {
+ createComponent();
+ const event = new window.MouseEvent('click', {
+ bubbles: true,
+ cancellable: true,
+ });
+ jest.spyOn(event, 'preventDefault');
+ node.dispatchEvent(event);
+ expect(event.preventDefault).not.toHaveBeenCalled();
+ });
+
+ it('captures click with glModalAction', () => {
+ createComponent();
+ node.dataset.glModalAction = 'action1';
+ const event = new window.MouseEvent('click', {
+ bubbles: true,
+ cancellable: true,
+ });
+ jest.spyOn(event, 'preventDefault');
+ node.dispatchEvent(event);
+
+ expect(event.preventDefault).toHaveBeenCalled();
+ return wrapper.vm.$nextTick().then(() => {
+ const modal = wrapper.find({ ref: 'modal' });
+ expect(modal.exists()).toBeTruthy();
+ expect(modal.vm.showWasCalled).toBeTruthy();
+ });
+ });
+ });
+});
diff --git a/spec/frontend/pages/admin/users/components/user_operation_confirmation_modal_spec.js b/spec/frontend/pages/admin/users/components/user_operation_confirmation_modal_spec.js
new file mode 100644
index 00000000000..0ecdae2618c
--- /dev/null
+++ b/spec/frontend/pages/admin/users/components/user_operation_confirmation_modal_spec.js
@@ -0,0 +1,47 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlModal } from '@gitlab/ui';
+import UserOperationConfirmationModal from '~/pages/admin/users/components/user_operation_confirmation_modal.vue';
+
+describe('User Operation confirmation modal', () => {
+ let wrapper;
+
+ const createComponent = (props = {}) => {
+ wrapper = shallowMount(UserOperationConfirmationModal, {
+ propsData: {
+ title: 'title',
+ content: 'content',
+ action: 'action',
+ url: '/url',
+ username: 'username',
+ csrfToken: 'csrf',
+ method: 'method',
+ ...props,
+ },
+ sync: false,
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ it('renders modal with form included', () => {
+ createComponent();
+ expect(wrapper.element).toMatchSnapshot();
+ });
+
+ it('closing modal with ok button triggers form submit', () => {
+ createComponent();
+ const form = wrapper.find('form');
+ jest.spyOn(form.element, 'submit').mockReturnValue();
+ wrapper.find(GlModal).vm.$emit('ok');
+ return wrapper.vm.$nextTick().then(() => {
+ expect(form.element.submit).toHaveBeenCalled();
+ expect(form.element.action).toContain(wrapper.props('url'));
+ expect(new FormData(form.element).get('authenticity_token')).toEqual(
+ wrapper.props('csrfToken'),
+ );
+ });
+ });
+});
diff --git a/spec/frontend/performance_bar/components/detailed_metric_spec.js b/spec/frontend/performance_bar/components/detailed_metric_spec.js
new file mode 100644
index 00000000000..74f242431a1
--- /dev/null
+++ b/spec/frontend/performance_bar/components/detailed_metric_spec.js
@@ -0,0 +1,111 @@
+import DetailedMetric from '~/performance_bar/components/detailed_metric.vue';
+import RequestWarning from '~/performance_bar/components/request_warning.vue';
+import { shallowMount } from '@vue/test-utils';
+
+describe('detailedMetric', () => {
+ const createComponent = props =>
+ shallowMount(DetailedMetric, {
+ propsData: {
+ ...props,
+ },
+ });
+
+ describe('when the current request has no details', () => {
+ const wrapper = createComponent({
+ currentRequest: {},
+ metric: 'gitaly',
+ header: 'Gitaly calls',
+ details: 'details',
+ keys: ['feature', 'request'],
+ });
+
+ it('does not render the element', () => {
+ expect(wrapper.isEmpty()).toBe(true);
+ });
+ });
+
+ describe('when the current request has details', () => {
+ const requestDetails = [
+ { duration: '100', feature: 'find_commit', request: 'abcdef', backtrace: ['hello', 'world'] },
+ { duration: '23', feature: 'rebase_in_progress', request: '', backtrace: ['world', 'hello'] },
+ ];
+
+ describe('with a default metric name', () => {
+ const wrapper = createComponent({
+ currentRequest: {
+ details: {
+ gitaly: {
+ duration: '123ms',
+ calls: '456',
+ details: requestDetails,
+ warnings: ['gitaly calls: 456 over 30'],
+ },
+ },
+ },
+ metric: 'gitaly',
+ header: 'Gitaly calls',
+ keys: ['feature', 'request'],
+ });
+
+ it('displays details', () => {
+ expect(wrapper.text().replace(/\s+/g, ' ')).toContain('123ms / 456');
+ });
+
+ it('adds a modal with a table of the details', () => {
+ wrapper
+ .findAll('.performance-bar-modal td:nth-child(1)')
+ .wrappers.forEach((duration, index) => {
+ expect(duration.text()).toContain(requestDetails[index].duration);
+ });
+
+ wrapper
+ .findAll('.performance-bar-modal td:nth-child(2)')
+ .wrappers.forEach((feature, index) => {
+ expect(feature.text()).toContain(requestDetails[index].feature);
+ });
+
+ wrapper
+ .findAll('.performance-bar-modal td:nth-child(2)')
+ .wrappers.forEach((request, index) => {
+ expect(request.text()).toContain(requestDetails[index].request);
+ });
+
+ expect(wrapper.find('.text-expander.js-toggle-button')).not.toBeNull();
+
+ wrapper.findAll('.performance-bar-modal td:nth-child(2)').wrappers.forEach(request => {
+ expect(request.text()).toContain('world');
+ });
+ });
+
+ it('displays the metric title', () => {
+ expect(wrapper.text()).toContain('gitaly');
+ });
+
+ it('displays request warnings', () => {
+ expect(wrapper.find(RequestWarning).exists()).toBe(true);
+ });
+ });
+
+ describe('when using a custom metric title', () => {
+ const wrapper = createComponent({
+ currentRequest: {
+ details: {
+ gitaly: {
+ duration: '123ms',
+ calls: '456',
+ details: requestDetails,
+ },
+ },
+ },
+ metric: 'gitaly',
+ title: 'custom',
+ header: 'Gitaly calls',
+ keys: ['feature', 'request'],
+ });
+
+ it('displays the custom title', () => {
+ expect(wrapper.text()).toContain('custom');
+ });
+ });
+ });
+});
diff --git a/spec/frontend/performance_bar/components/performance_bar_app_spec.js b/spec/frontend/performance_bar/components/performance_bar_app_spec.js
new file mode 100644
index 00000000000..ba403dd6209
--- /dev/null
+++ b/spec/frontend/performance_bar/components/performance_bar_app_spec.js
@@ -0,0 +1,20 @@
+import PerformanceBarApp from '~/performance_bar/components/performance_bar_app.vue';
+import PerformanceBarStore from '~/performance_bar/stores/performance_bar_store';
+import { shallowMount } from '@vue/test-utils';
+
+describe('performance bar app', () => {
+ const store = new PerformanceBarStore();
+ const wrapper = shallowMount(PerformanceBarApp, {
+ propsData: {
+ store,
+ env: 'development',
+ requestId: '123',
+ peekUrl: '/-/peek/results',
+ profileUrl: '?lineprofiler=true',
+ },
+ });
+
+ it('sets the class to match the environment', () => {
+ expect(wrapper.element.getAttribute('class')).toContain('development');
+ });
+});
diff --git a/spec/frontend/performance_bar/components/request_selector_spec.js b/spec/frontend/performance_bar/components/request_selector_spec.js
new file mode 100644
index 00000000000..a4ed55fbf15
--- /dev/null
+++ b/spec/frontend/performance_bar/components/request_selector_spec.js
@@ -0,0 +1,64 @@
+import RequestSelector from '~/performance_bar/components/request_selector.vue';
+import { shallowMount } from '@vue/test-utils';
+
+describe('request selector', () => {
+ const requests = [
+ {
+ id: '123',
+ url: 'https://gitlab.com/',
+ hasWarnings: false,
+ },
+ {
+ id: '456',
+ url: 'https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/1',
+ hasWarnings: false,
+ },
+ {
+ id: '789',
+ url: 'https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/1.json?serializer=widget',
+ hasWarnings: false,
+ },
+ {
+ id: 'abc',
+ url: 'https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/1/discussions.json',
+ hasWarnings: true,
+ },
+ ];
+
+ const wrapper = shallowMount(RequestSelector, {
+ propsData: {
+ requests,
+ currentRequest: requests[1],
+ },
+ });
+
+ function optionText(requestId) {
+ return wrapper
+ .find(`[value='${requestId}']`)
+ .text()
+ .trim();
+ }
+
+ it('displays the last component of the path', () => {
+ expect(optionText(requests[2].id)).toEqual('1.json?serializer=widget');
+ });
+
+ it('keeps the last two components of the path when the last component is numeric', () => {
+ expect(optionText(requests[1].id)).toEqual('merge_requests/1');
+ });
+
+ it('ignores trailing slashes', () => {
+ expect(optionText(requests[0].id)).toEqual('gitlab.com');
+ });
+
+ it('has a warning icon if any requests have warnings', () => {
+ expect(wrapper.find('span > gl-emoji').element.dataset.name).toEqual('warning');
+ });
+
+ it('adds a warning glyph to requests with warnings', () => {
+ const requestValue = wrapper.find('[value="abc"]').text();
+
+ expect(requestValue).toContain('discussions.json');
+ expect(requestValue).toContain('(!)');
+ });
+});
diff --git a/spec/frontend/performance_bar/components/request_warning_spec.js b/spec/frontend/performance_bar/components/request_warning_spec.js
new file mode 100644
index 00000000000..6d8bfba56f6
--- /dev/null
+++ b/spec/frontend/performance_bar/components/request_warning_spec.js
@@ -0,0 +1,33 @@
+import RequestWarning from '~/performance_bar/components/request_warning.vue';
+import { shallowMount } from '@vue/test-utils';
+
+describe('request warning', () => {
+ const htmlId = 'request-123';
+
+ describe('when the request has warnings', () => {
+ const wrapper = shallowMount(RequestWarning, {
+ propsData: {
+ htmlId,
+ warnings: ['gitaly calls: 30 over 10', 'gitaly duration: 1500 over 1000'],
+ },
+ });
+
+ it('adds a warning emoji with the correct ID', () => {
+ expect(wrapper.find('span[id]').attributes('id')).toEqual(htmlId);
+ expect(wrapper.find('span[id] gl-emoji').element.dataset.name).toEqual('warning');
+ });
+ });
+
+ describe('when the request does not have warnings', () => {
+ const wrapper = shallowMount(RequestWarning, {
+ propsData: {
+ htmlId,
+ warnings: [],
+ },
+ });
+
+ it('does nothing', () => {
+ expect(wrapper.isEmpty()).toBe(true);
+ });
+ });
+});
diff --git a/spec/frontend/registry/components/__snapshots__/group_empty_state_spec.js.snap b/spec/frontend/registry/components/__snapshots__/group_empty_state_spec.js.snap
new file mode 100644
index 00000000000..3f13b7d4d76
--- /dev/null
+++ b/spec/frontend/registry/components/__snapshots__/group_empty_state_spec.js.snap
@@ -0,0 +1,61 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Registry Group Empty state to match the default snapshot 1`] = `
+<div
+ class="row container-message empty-state"
+>
+ <div
+ class="col-12"
+ >
+ <div
+ class="svg-250 svg-content"
+ >
+ <img
+ alt="There are no container images available in this group"
+ class=""
+ src="imageUrl"
+ />
+ </div>
+ </div>
+
+ <div
+ class="col-12"
+ >
+ <div
+ class="text-content"
+ >
+ <h4
+ class="center"
+ style=""
+ >
+ There are no container images available in this group
+ </h4>
+
+ <p
+ class="center"
+ style=""
+ >
+ <p
+ class="js-no-container-images-text"
+ >
+ With the Container Registry, every project can have its own space to store its Docker images. Push at least one Docker image in one of this group's projects in order to show up here.
+ <a
+ href="help"
+ target="_blank"
+ >
+ More Information
+ </a>
+ </p>
+ </p>
+
+ <div
+ class="text-center"
+ >
+ <!---->
+
+ <!---->
+ </div>
+ </div>
+ </div>
+</div>
+`;
diff --git a/spec/frontend/registry/components/__snapshots__/project_empty_state_spec.js.snap b/spec/frontend/registry/components/__snapshots__/project_empty_state_spec.js.snap
new file mode 100644
index 00000000000..3084462f5ae
--- /dev/null
+++ b/spec/frontend/registry/components/__snapshots__/project_empty_state_spec.js.snap
@@ -0,0 +1,186 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Registry Project Empty state to match the default snapshot 1`] = `
+<div
+ class="row container-message empty-state"
+>
+ <div
+ class="col-12"
+ >
+ <div
+ class="svg-250 svg-content"
+ >
+ <img
+ alt="There are no container images stored for this project"
+ class=""
+ src="imageUrl"
+ />
+ </div>
+ </div>
+
+ <div
+ class="col-12"
+ >
+ <div
+ class="text-content"
+ >
+ <h4
+ class="center"
+ style=""
+ >
+ There are no container images stored for this project
+ </h4>
+
+ <p
+ class="center"
+ style=""
+ >
+ <p
+ class="js-no-container-images-text"
+ >
+ With the Container Registry, every project can have its own space to store its Docker images.
+ <a
+ href="help"
+ target="_blank"
+ >
+ More Information
+ </a>
+ </p>
+
+ <h5>
+ Quick Start
+ </h5>
+
+ <p
+ class="js-not-logged-in-to-registry-text"
+ >
+ If you are not already logged in, you need to authenticate to the Container Registry by using your GitLab username and password. If you have
+ <a
+ href="help_link"
+ target="_blank"
+ >
+ Two-Factor Authentication
+ </a>
+ enabled, use a
+ <a
+ href="personal_token"
+ target="_blank"
+ >
+ Personal Access Token
+ </a>
+ instead of a password.
+ </p>
+
+ <div
+ class="input-group append-bottom-10"
+ >
+ <input
+ class="form-control monospace"
+ readonly="readonly"
+ type="text"
+ />
+
+ <span
+ class="input-group-append"
+ >
+ <button
+ class="btn input-group-text btn-secondary btn-default"
+ data-clipboard-text="docker login host"
+ data-original-title="Copy login command"
+ title=""
+ type="button"
+ >
+ <svg
+ aria-hidden="true"
+ class="s16 ic-duplicate"
+ >
+ <use
+ xlink:href="#duplicate"
+ />
+ </svg>
+ </button>
+ </span>
+ </div>
+
+ <p />
+
+ <p>
+
+ You can add an image to this registry with the following commands:
+
+ </p>
+
+ <div
+ class="input-group append-bottom-10"
+ >
+ <input
+ class="form-control monospace"
+ readonly="readonly"
+ type="text"
+ />
+
+ <span
+ class="input-group-append"
+ >
+ <button
+ class="btn input-group-text btn-secondary btn-default"
+ data-clipboard-text="docker build -t url ."
+ data-original-title="Copy build command"
+ title=""
+ type="button"
+ >
+ <svg
+ aria-hidden="true"
+ class="s16 ic-duplicate"
+ >
+ <use
+ xlink:href="#duplicate"
+ />
+ </svg>
+ </button>
+ </span>
+ </div>
+
+ <div
+ class="input-group"
+ >
+ <input
+ class="form-control monospace"
+ readonly="readonly"
+ type="text"
+ />
+
+ <span
+ class="input-group-append"
+ >
+ <button
+ class="btn input-group-text btn-secondary btn-default"
+ data-clipboard-text="docker push url"
+ data-original-title="Copy push command"
+ title=""
+ type="button"
+ >
+ <svg
+ aria-hidden="true"
+ class="s16 ic-duplicate"
+ >
+ <use
+ xlink:href="#duplicate"
+ />
+ </svg>
+ </button>
+ </span>
+ </div>
+ </p>
+
+ <div
+ class="text-center"
+ >
+ <!---->
+
+ <!---->
+ </div>
+ </div>
+ </div>
+</div>
+`;
diff --git a/spec/frontend/registry/components/app_spec.js b/spec/frontend/registry/components/app_spec.js
new file mode 100644
index 00000000000..a69c33c246d
--- /dev/null
+++ b/spec/frontend/registry/components/app_spec.js
@@ -0,0 +1,160 @@
+import Vue from 'vue';
+import { mount } from '@vue/test-utils';
+import registry from '~/registry/components/app.vue';
+import { TEST_HOST } from '../../helpers/test_constants';
+import { reposServerResponse, parsedReposServerResponse } from '../mock_data';
+
+describe('Registry List', () => {
+ let wrapper;
+
+ const findCollapsibleContainer = w => w.findAll({ name: 'CollapsibeContainerRegisty' });
+ const findProjectEmptyState = w => w.find({ name: 'ProjectEmptyState' });
+ const findGroupEmptyState = w => w.find({ name: 'GroupEmptyState' });
+ const findSpinner = w => w.find('.gl-spinner');
+ const findCharacterErrorText = w => w.find('.js-character-error-text');
+
+ const propsData = {
+ endpoint: `${TEST_HOST}/foo`,
+ helpPagePath: 'foo',
+ noContainersImage: 'foo',
+ containersErrorImage: 'foo',
+ repositoryUrl: 'foo',
+ registryHostUrlWithPort: 'foo',
+ personalAccessTokensHelpLink: 'foo',
+ twoFactorAuthHelpLink: 'foo',
+ };
+
+ const setMainEndpoint = jest.fn();
+ const fetchRepos = jest.fn();
+ const setIsDeleteDisabled = jest.fn();
+
+ const methods = {
+ setMainEndpoint,
+ fetchRepos,
+ setIsDeleteDisabled,
+ };
+
+ beforeEach(() => {
+ // This is needed due to console.error called by vue to emit a warning that stop the tests.
+ // See https://github.com/vuejs/vue-test-utils/issues/532.
+ Vue.config.silent = true;
+ wrapper = mount(registry, {
+ propsData,
+ computed: {
+ repos() {
+ return parsedReposServerResponse;
+ },
+ },
+ methods,
+ });
+ });
+
+ afterEach(() => {
+ jest.clearAllMocks();
+ Vue.config.silent = false;
+ wrapper.destroy();
+ });
+
+ describe('with data', () => {
+ it('should render a list of CollapsibeContainerRegisty', () => {
+ const containers = findCollapsibleContainer(wrapper);
+ expect(wrapper.vm.repos.length).toEqual(reposServerResponse.length);
+ expect(containers.length).toEqual(reposServerResponse.length);
+ });
+ });
+
+ describe('without data', () => {
+ let localWrapper;
+ beforeEach(() => {
+ localWrapper = mount(registry, {
+ propsData,
+ computed: {
+ repos() {
+ return [];
+ },
+ },
+ methods,
+ });
+ });
+
+ it('should render project empty message', () => {
+ const projectEmptyState = findProjectEmptyState(localWrapper);
+ expect(projectEmptyState.exists()).toBe(true);
+ });
+ });
+
+ describe('while loading data', () => {
+ let localWrapper;
+
+ beforeEach(() => {
+ localWrapper = mount(registry, {
+ propsData,
+ computed: {
+ repos() {
+ return [];
+ },
+ isLoading() {
+ return true;
+ },
+ },
+ methods,
+ });
+ });
+
+ it('should render a loading spinner', () => {
+ const spinner = findSpinner(localWrapper);
+ expect(spinner.exists()).toBe(true);
+ });
+ });
+
+ describe('invalid characters in path', () => {
+ let localWrapper;
+
+ beforeEach(() => {
+ localWrapper = mount(registry, {
+ propsData: {
+ ...propsData,
+ characterError: true,
+ },
+ computed: {
+ repos() {
+ return [];
+ },
+ },
+ methods,
+ });
+ });
+
+ it('should render invalid characters error message', () => {
+ const characterErrorText = findCharacterErrorText(localWrapper);
+ expect(characterErrorText.text()).toEqual(
+ 'We are having trouble connecting to Docker, which could be due to an issue with your project name or path. More Information',
+ );
+ });
+ });
+
+ describe('with groupId set', () => {
+ const isGroupPage = true;
+
+ beforeEach(() => {
+ wrapper = mount(registry, {
+ propsData: {
+ ...propsData,
+ endpoint: null,
+ isGroupPage,
+ },
+ methods,
+ });
+ });
+
+ it('call the right vuex setters', () => {
+ expect(methods.setMainEndpoint).toHaveBeenLastCalledWith(null);
+ expect(methods.setIsDeleteDisabled).toHaveBeenLastCalledWith(true);
+ });
+
+ it('should render groups empty message', () => {
+ const groupEmptyState = findGroupEmptyState(wrapper);
+ expect(groupEmptyState.exists()).toBe(true);
+ });
+ });
+});
diff --git a/spec/frontend/registry/components/collapsible_container_spec.js b/spec/frontend/registry/components/collapsible_container_spec.js
new file mode 100644
index 00000000000..f93ebab1a4d
--- /dev/null
+++ b/spec/frontend/registry/components/collapsible_container_spec.js
@@ -0,0 +1,127 @@
+import Vue from 'vue';
+import Vuex from 'vuex';
+import { mount, createLocalVue } from '@vue/test-utils';
+import collapsibleComponent from '~/registry/components/collapsible_container.vue';
+import { repoPropsData } from '../mock_data';
+import createFlash from '~/flash';
+import * as getters from '~/registry/stores/getters';
+
+jest.mock('~/flash.js');
+
+const localVue = createLocalVue();
+
+localVue.use(Vuex);
+
+describe('collapsible registry container', () => {
+ let wrapper;
+ let store;
+
+ const findDeleteBtn = w => w.find('.js-remove-repo');
+ const findContainerImageTags = w => w.find('.container-image-tags');
+ const findToggleRepos = w => w.findAll('.js-toggle-repo');
+
+ const mountWithStore = config => mount(collapsibleComponent, { ...config, store, localVue });
+
+ beforeEach(() => {
+ createFlash.mockClear();
+ // This is needed due to console.error called by vue to emit a warning that stop the tests
+ // see https://github.com/vuejs/vue-test-utils/issues/532
+ Vue.config.silent = true;
+ store = new Vuex.Store({
+ state: {
+ isDeleteDisabled: false,
+ },
+ getters,
+ });
+
+ wrapper = mountWithStore({
+ propsData: {
+ repo: repoPropsData,
+ },
+ });
+ });
+
+ afterEach(() => {
+ Vue.config.silent = false;
+ wrapper.destroy();
+ });
+
+ describe('toggle', () => {
+ beforeEach(() => {
+ const fetchList = jest.fn();
+ wrapper.setMethods({ fetchList });
+ });
+
+ const expectIsClosed = () => {
+ const container = findContainerImageTags(wrapper);
+ expect(container.exists()).toBe(false);
+ expect(wrapper.vm.iconName).toEqual('angle-right');
+ };
+
+ it('should be closed by default', () => {
+ expectIsClosed();
+ });
+ it('should be open when user clicks on closed repo', () => {
+ const toggleRepos = findToggleRepos(wrapper);
+ toggleRepos.at(0).trigger('click');
+ const container = findContainerImageTags(wrapper);
+ expect(container.exists()).toBe(true);
+ expect(wrapper.vm.fetchList).toHaveBeenCalled();
+ });
+ it('should be closed when the user clicks on an opened repo', done => {
+ const toggleRepos = findToggleRepos(wrapper);
+ toggleRepos.at(0).trigger('click');
+ Vue.nextTick(() => {
+ toggleRepos.at(0).trigger('click');
+ Vue.nextTick(() => {
+ expectIsClosed();
+ done();
+ });
+ });
+ });
+ });
+
+ describe('delete repo', () => {
+ it('should be possible to delete a repo', () => {
+ const deleteBtn = findDeleteBtn(wrapper);
+ expect(deleteBtn.exists()).toBe(true);
+ });
+
+ it('should call deleteItem when confirming deletion', () => {
+ const deleteItem = jest.fn().mockResolvedValue();
+ const fetchRepos = jest.fn().mockResolvedValue();
+ wrapper.setMethods({ deleteItem, fetchRepos });
+ wrapper.vm.handleDeleteRepository();
+ expect(wrapper.vm.deleteItem).toHaveBeenCalledWith(wrapper.vm.repo);
+ });
+
+ it('should show an error when there is API error', () => {
+ const deleteItem = jest.fn().mockRejectedValue('error');
+ wrapper.setMethods({ deleteItem });
+ return wrapper.vm.handleDeleteRepository().then(() => {
+ expect(createFlash).toHaveBeenCalled();
+ });
+ });
+ });
+
+ describe('disabled delete', () => {
+ beforeEach(() => {
+ store = new Vuex.Store({
+ state: {
+ isDeleteDisabled: true,
+ },
+ getters,
+ });
+ wrapper = mountWithStore({
+ propsData: {
+ repo: repoPropsData,
+ },
+ });
+ });
+
+ it('should not render delete button', () => {
+ const deleteBtn = findDeleteBtn(wrapper);
+ expect(deleteBtn.exists()).toBe(false);
+ });
+ });
+});
diff --git a/spec/frontend/registry/components/group_empty_state_spec.js b/spec/frontend/registry/components/group_empty_state_spec.js
new file mode 100644
index 00000000000..f71074b5154
--- /dev/null
+++ b/spec/frontend/registry/components/group_empty_state_spec.js
@@ -0,0 +1,23 @@
+import { mount } from '@vue/test-utils';
+import groupEmptyState from '~/registry/components/group_empty_state.vue';
+
+describe('Registry Group Empty state', () => {
+ let wrapper;
+
+ beforeEach(() => {
+ wrapper = mount(groupEmptyState, {
+ propsData: {
+ noContainersImage: 'imageUrl',
+ helpPagePath: 'help',
+ },
+ });
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('to match the default snapshot', () => {
+ expect(wrapper.element).toMatchSnapshot();
+ });
+});
diff --git a/spec/frontend/registry/components/project_empty_state_spec.js b/spec/frontend/registry/components/project_empty_state_spec.js
new file mode 100644
index 00000000000..913524db3aa
--- /dev/null
+++ b/spec/frontend/registry/components/project_empty_state_spec.js
@@ -0,0 +1,27 @@
+import { mount } from '@vue/test-utils';
+import projectEmptyState from '~/registry/components/project_empty_state.vue';
+
+describe('Registry Project Empty state', () => {
+ let wrapper;
+
+ beforeEach(() => {
+ wrapper = mount(projectEmptyState, {
+ propsData: {
+ noContainersImage: 'imageUrl',
+ helpPagePath: 'help',
+ repositoryUrl: 'url',
+ twoFactorAuthHelpLink: 'help_link',
+ personalAccessTokensHelpLink: 'personal_token',
+ registryHostUrlWithPort: 'host',
+ },
+ });
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('to match the default snapshot', () => {
+ expect(wrapper.element).toMatchSnapshot();
+ });
+});
diff --git a/spec/frontend/registry/components/table_registry_spec.js b/spec/frontend/registry/components/table_registry_spec.js
new file mode 100644
index 00000000000..7cb7c012d9d
--- /dev/null
+++ b/spec/frontend/registry/components/table_registry_spec.js
@@ -0,0 +1,268 @@
+import Vue from 'vue';
+import Vuex from 'vuex';
+import tableRegistry from '~/registry/components/table_registry.vue';
+import { mount, createLocalVue } from '@vue/test-utils';
+import { repoPropsData } from '../mock_data';
+import * as getters from '~/registry/stores/getters';
+
+const [firstImage, secondImage] = repoPropsData.list;
+
+const localVue = createLocalVue();
+
+localVue.use(Vuex);
+
+describe('table registry', () => {
+ let wrapper;
+ let store;
+
+ const findSelectAllCheckbox = w => w.find('.js-select-all-checkbox > input');
+ const findSelectCheckboxes = w => w.findAll('.js-select-checkbox > input');
+ const findDeleteButton = w => w.find('.js-delete-registry');
+ const findDeleteButtonsRow = w => w.findAll('.js-delete-registry-row');
+ const findPagination = w => w.find('.js-registry-pagination');
+ const bulkDeletePath = 'path';
+
+ const mountWithStore = config => mount(tableRegistry, { ...config, store, localVue });
+
+ beforeEach(() => {
+ // This is needed due to console.error called by vue to emit a warning that stop the tests
+ // see https://github.com/vuejs/vue-test-utils/issues/532
+ Vue.config.silent = true;
+
+ store = new Vuex.Store({
+ state: {
+ isDeleteDisabled: false,
+ },
+ getters,
+ });
+
+ wrapper = mountWithStore({
+ propsData: {
+ repo: repoPropsData,
+ canDeleteRepo: true,
+ },
+ });
+ });
+
+ afterEach(() => {
+ Vue.config.silent = false;
+ wrapper.destroy();
+ });
+
+ describe('rendering', () => {
+ it('should render a table with the registry list', () => {
+ expect(wrapper.findAll('.registry-image-row').length).toEqual(repoPropsData.list.length);
+ });
+
+ it('should render registry tag', () => {
+ const tds = wrapper.findAll('.registry-image-row td');
+ expect(tds.at(0).classes()).toContain('check');
+ expect(tds.at(1).html()).toContain(repoPropsData.list[0].tag);
+ expect(tds.at(2).html()).toContain(repoPropsData.list[0].shortRevision);
+ expect(tds.at(3).html()).toContain(repoPropsData.list[0].layers);
+ expect(tds.at(3).html()).toContain(repoPropsData.list[0].size);
+ expect(tds.at(4).html()).toContain(wrapper.vm.timeFormated(repoPropsData.list[0].createdAt));
+ });
+ });
+
+ describe('multi select', () => {
+ it('selecting a row should enable delete button', done => {
+ const deleteBtn = findDeleteButton(wrapper);
+ const checkboxes = findSelectCheckboxes(wrapper);
+
+ expect(deleteBtn.attributes('disabled')).toBe('disabled');
+
+ checkboxes.at(0).trigger('click');
+ Vue.nextTick(() => {
+ expect(deleteBtn.attributes('disabled')).toEqual(undefined);
+ done();
+ });
+ });
+
+ it('selecting all checkbox should select all rows and enable delete button', done => {
+ const selectAll = findSelectAllCheckbox(wrapper);
+ const checkboxes = findSelectCheckboxes(wrapper);
+ selectAll.trigger('click');
+
+ Vue.nextTick(() => {
+ const checked = checkboxes.filter(w => w.element.checked);
+ expect(checked.length).toBe(checkboxes.length);
+ done();
+ });
+ });
+
+ it('deselecting select all checkbox should deselect all rows and disable delete button', done => {
+ const checkboxes = findSelectCheckboxes(wrapper);
+ const selectAll = findSelectAllCheckbox(wrapper);
+ selectAll.trigger('click');
+ selectAll.trigger('click');
+
+ Vue.nextTick(() => {
+ const checked = checkboxes.filter(w => !w.element.checked);
+ expect(checked.length).toBe(checkboxes.length);
+ done();
+ });
+ });
+
+ it('should delete multiple items when multiple items are selected', done => {
+ const multiDeleteItems = jest.fn().mockResolvedValue();
+ wrapper.setMethods({ multiDeleteItems });
+ const selectAll = findSelectAllCheckbox(wrapper);
+ selectAll.trigger('click');
+
+ Vue.nextTick(() => {
+ const deleteBtn = findDeleteButton(wrapper);
+ expect(wrapper.vm.selectedItems).toEqual([0, 1]);
+ expect(deleteBtn.attributes('disabled')).toEqual(undefined);
+ wrapper.setData({ itemsToBeDeleted: [...wrapper.vm.selectedItems] });
+ wrapper.vm.handleMultipleDelete();
+
+ Vue.nextTick(() => {
+ expect(wrapper.vm.selectedItems).toEqual([]);
+ expect(wrapper.vm.itemsToBeDeleted).toEqual([]);
+ expect(wrapper.vm.multiDeleteItems).toHaveBeenCalledWith({
+ path: bulkDeletePath,
+ items: [firstImage.tag, secondImage.tag],
+ });
+ done();
+ });
+ });
+ });
+
+ it('should show an error message if bulkDeletePath is not set', () => {
+ const showError = jest.fn();
+ wrapper.setMethods({ showError });
+ wrapper.setProps({
+ repo: {
+ ...repoPropsData,
+ tagsPath: null,
+ },
+ });
+ wrapper.vm.handleMultipleDelete();
+ expect(wrapper.vm.showError).toHaveBeenCalled();
+ });
+ });
+
+ describe('delete registry', () => {
+ beforeEach(() => {
+ wrapper.setData({ selectedItems: [0] });
+ });
+
+ it('should be possible to delete a registry', () => {
+ const deleteBtn = findDeleteButton(wrapper);
+ const deleteBtns = findDeleteButtonsRow(wrapper);
+ expect(wrapper.vm.selectedItems).toEqual([0]);
+ expect(deleteBtn).toBeDefined();
+ expect(deleteBtn.attributes('disable')).toBe(undefined);
+ expect(deleteBtns.is('button')).toBe(true);
+ });
+
+ it('should allow deletion row by row', () => {
+ const deleteBtns = findDeleteButtonsRow(wrapper);
+ const deleteSingleItem = jest.fn();
+ const deleteItem = jest.fn().mockResolvedValue();
+ wrapper.setMethods({ deleteSingleItem, deleteItem });
+ deleteBtns.at(0).trigger('click');
+ expect(wrapper.vm.deleteSingleItem).toHaveBeenCalledWith(0);
+ wrapper.vm.handleSingleDelete(1);
+ expect(wrapper.vm.deleteItem).toHaveBeenCalledWith(1);
+ });
+ });
+
+ describe('pagination', () => {
+ const repo = {
+ repoPropsData,
+ pagination: {
+ total: 20,
+ perPage: 2,
+ nextPage: 2,
+ },
+ };
+
+ beforeEach(() => {
+ wrapper = mount(tableRegistry, {
+ propsData: {
+ repo,
+ },
+ });
+ });
+
+ it('should exist', () => {
+ const pagination = findPagination(wrapper);
+ expect(pagination.exists()).toBe(true);
+ });
+ it('should be visible when pagination is needed', () => {
+ const pagination = findPagination(wrapper);
+ expect(pagination.isVisible()).toBe(true);
+ wrapper.setProps({
+ repo: {
+ pagination: {
+ total: 0,
+ perPage: 10,
+ },
+ },
+ });
+ expect(wrapper.vm.shouldRenderPagination).toBe(false);
+ });
+ it('should have a change function that update the list when run', () => {
+ const fetchList = jest.fn().mockResolvedValue();
+ wrapper.setMethods({ fetchList });
+ wrapper.vm.onPageChange(1);
+ expect(wrapper.vm.fetchList).toHaveBeenCalledWith({ repo, page: 1 });
+ });
+ });
+
+ describe('modal content', () => {
+ it('should show the singular title and image name when deleting a single image', () => {
+ wrapper.setData({ selectedItems: [1, 2, 3] });
+ wrapper.vm.deleteSingleItem(0);
+ expect(wrapper.vm.modalAction).toBe('Remove tag');
+ expect(wrapper.vm.modalDescription).toContain(firstImage.tag);
+ });
+
+ it('should show the plural title and image count when deleting more than one image', () => {
+ wrapper.setData({ selectedItems: [1, 2] });
+ wrapper.vm.deleteMultipleItems();
+
+ expect(wrapper.vm.modalAction).toBe('Remove tags');
+ expect(wrapper.vm.modalDescription).toContain('<b>2</b> tags');
+ });
+ });
+
+ describe('disabled delete', () => {
+ beforeEach(() => {
+ store = new Vuex.Store({
+ state: {
+ isDeleteDisabled: true,
+ },
+ getters,
+ });
+ wrapper = mountWithStore({
+ propsData: {
+ repo: repoPropsData,
+ canDeleteRepo: false,
+ },
+ });
+ });
+
+ it('should not render select all', () => {
+ const selectAll = findSelectAllCheckbox(wrapper);
+ expect(selectAll.exists()).toBe(false);
+ });
+
+ it('should not render any select checkbox', () => {
+ const selects = findSelectCheckboxes(wrapper);
+ expect(selects.length).toBe(0);
+ });
+
+ it('should not render delete registry button', () => {
+ const deleteBtn = findDeleteButton(wrapper);
+ expect(deleteBtn.exists()).toBe(false);
+ });
+
+ it('should not render delete row button', () => {
+ const deleteBtns = findDeleteButtonsRow(wrapper);
+ expect(deleteBtns.length).toBe(0);
+ });
+ });
+});
diff --git a/spec/javascripts/registry/mock_data.js b/spec/frontend/registry/mock_data.js
index 130ab298e89..130ab298e89 100644
--- a/spec/javascripts/registry/mock_data.js
+++ b/spec/frontend/registry/mock_data.js
diff --git a/spec/frontend/registry/stores/actions_spec.js b/spec/frontend/registry/stores/actions_spec.js
new file mode 100644
index 00000000000..7937fa82e80
--- /dev/null
+++ b/spec/frontend/registry/stores/actions_spec.js
@@ -0,0 +1,203 @@
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
+import * as actions from '~/registry/stores/actions';
+import * as types from '~/registry/stores/mutation_types';
+import { TEST_HOST } from '../../helpers/test_constants';
+import testAction from '../../helpers/vuex_action_helper';
+import createFlash from '~/flash';
+
+import {
+ reposServerResponse,
+ registryServerResponse,
+ parsedReposServerResponse,
+} from '../mock_data';
+
+jest.mock('~/flash.js');
+
+describe('Actions Registry Store', () => {
+ let mock;
+ let state;
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ state = {
+ endpoint: `${TEST_HOST}/endpoint.json`,
+ };
+ });
+
+ afterEach(() => {
+ mock.restore();
+ });
+
+ describe('fetchRepos', () => {
+ beforeEach(() => {
+ mock.onGet(`${TEST_HOST}/endpoint.json`).replyOnce(200, reposServerResponse, {});
+ });
+
+ it('should set received repos', done => {
+ testAction(
+ actions.fetchRepos,
+ null,
+ state,
+ [
+ { type: types.TOGGLE_MAIN_LOADING },
+ { type: types.TOGGLE_MAIN_LOADING },
+ { type: types.SET_REPOS_LIST, payload: reposServerResponse },
+ ],
+ [],
+ done,
+ );
+ });
+
+ it('should create flash on API error', done => {
+ testAction(
+ actions.fetchRepos,
+ null,
+ {
+ endpoint: null,
+ },
+ [{ type: types.TOGGLE_MAIN_LOADING }, { type: types.TOGGLE_MAIN_LOADING }],
+ [],
+ () => {
+ expect(createFlash).toHaveBeenCalled();
+ done();
+ },
+ );
+ });
+ });
+
+ describe('fetchList', () => {
+ let repo;
+ beforeEach(() => {
+ state.repos = parsedReposServerResponse;
+ [, repo] = state.repos;
+ });
+
+ it('should set received list', done => {
+ mock.onGet(repo.tagsPath).replyOnce(200, registryServerResponse, {});
+ testAction(
+ actions.fetchList,
+ { repo },
+ state,
+ [
+ { type: types.TOGGLE_REGISTRY_LIST_LOADING, payload: repo },
+ { type: types.TOGGLE_REGISTRY_LIST_LOADING, payload: repo },
+ {
+ type: types.SET_REGISTRY_LIST,
+ payload: {
+ repo,
+ resp: registryServerResponse,
+ headers: expect.anything(),
+ },
+ },
+ ],
+ [],
+ done,
+ );
+ });
+
+ it('should create flash on API error', done => {
+ mock.onGet(repo.tagsPath).replyOnce(400);
+ const updatedRepo = {
+ ...repo,
+ tagsPath: null,
+ };
+ testAction(
+ actions.fetchList,
+ {
+ repo: updatedRepo,
+ },
+ state,
+ [
+ { type: types.TOGGLE_REGISTRY_LIST_LOADING, payload: updatedRepo },
+ { type: types.TOGGLE_REGISTRY_LIST_LOADING, payload: updatedRepo },
+ ],
+ [],
+ () => {
+ expect(createFlash).toHaveBeenCalled();
+ done();
+ },
+ );
+ });
+ });
+
+ describe('setMainEndpoint', () => {
+ it('should commit set main endpoint', done => {
+ testAction(
+ actions.setMainEndpoint,
+ 'endpoint',
+ state,
+ [{ type: types.SET_MAIN_ENDPOINT, payload: 'endpoint' }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('setIsDeleteDisabled', () => {
+ it('should commit set is delete disabled', done => {
+ testAction(
+ actions.setIsDeleteDisabled,
+ true,
+ state,
+ [{ type: types.SET_IS_DELETE_DISABLED, payload: true }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('toggleLoading', () => {
+ it('should commit toggle main loading', done => {
+ testAction(
+ actions.toggleLoading,
+ null,
+ state,
+ [{ type: types.TOGGLE_MAIN_LOADING }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('deleteItem and multiDeleteItems', () => {
+ let deleted;
+ const destroyPath = `${TEST_HOST}/mygroup/myproject/container_registry/1.json`;
+
+ const expectDelete = done => {
+ expect(mock.history.delete.length).toBe(1);
+ expect(deleted).toBe(true);
+ done();
+ };
+
+ beforeEach(() => {
+ deleted = false;
+ mock.onDelete(destroyPath).replyOnce(() => {
+ deleted = true;
+ return [200];
+ });
+ });
+
+ it('deleteItem should perform DELETE request on destroyPath', done => {
+ testAction(
+ actions.deleteItem,
+ {
+ destroyPath,
+ },
+ state,
+ )
+ .then(() => {
+ expectDelete(done);
+ })
+ .catch(done.fail);
+ });
+
+ it('multiDeleteItems should perform DELETE request on path', done => {
+ testAction(actions.multiDeleteItems, { path: destroyPath, items: [1] }, state)
+ .then(() => {
+ expectDelete(done);
+ })
+ .catch(done.fail);
+ });
+ });
+});
diff --git a/spec/frontend/registry/getters_spec.js b/spec/frontend/registry/stores/getters_spec.js
index 839aa718997..c16f520223b 100644
--- a/spec/frontend/registry/getters_spec.js
+++ b/spec/frontend/registry/stores/getters_spec.js
@@ -7,6 +7,7 @@ describe('Getters Registry Store', () => {
state = {
isLoading: false,
endpoint: '/root/empty-project/container_registry.json',
+ isDeleteDisabled: false,
repos: [
{
canDelete: true,
@@ -43,4 +44,9 @@ describe('Getters Registry Store', () => {
expect(getters.repos(state)).toEqual(state.repos);
});
});
+ describe('isDeleteDisabled', () => {
+ it('should return isDeleteDisabled', () => {
+ expect(getters.isDeleteDisabled(state)).toEqual(state.isDeleteDisabled);
+ });
+ });
});
diff --git a/spec/javascripts/registry/stores/mutations_spec.js b/spec/frontend/registry/stores/mutations_spec.js
index e19fe7a27cf..1d583028ca6 100644
--- a/spec/javascripts/registry/stores/mutations_spec.js
+++ b/spec/frontend/registry/stores/mutations_spec.js
@@ -19,7 +19,16 @@ describe('Mutations Registry Store', () => {
const expectedState = Object.assign({}, mockState, { endpoint: 'foo' });
mutations[types.SET_MAIN_ENDPOINT](mockState, 'foo');
- expect(mockState).toEqual(expectedState);
+ expect(mockState.endpoint).toEqual(expectedState.endpoint);
+ });
+ });
+
+ describe('SET_IS_DELETE_DISABLED', () => {
+ it('should set the is delete disabled', () => {
+ const expectedState = Object.assign({}, mockState, { isDeleteDisabled: true });
+ mutations[types.SET_IS_DELETE_DISABLED](mockState, true);
+
+ expect(mockState.isDeleteDisabled).toEqual(expectedState.isDeleteDisabled);
});
});
diff --git a/spec/frontend/releases/components/milestone_list_spec.js b/spec/frontend/releases/components/milestone_list_spec.js
deleted file mode 100644
index f267177ddab..00000000000
--- a/spec/frontend/releases/components/milestone_list_spec.js
+++ /dev/null
@@ -1,56 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-import { GlLink } from '@gitlab/ui';
-import MilestoneList from '~/releases/components/milestone_list.vue';
-import Icon from '~/vue_shared/components/icon.vue';
-import _ from 'underscore';
-import { milestones } from '../mock_data';
-
-describe('Milestone list', () => {
- let wrapper;
-
- const factory = milestonesProp => {
- wrapper = shallowMount(MilestoneList, {
- propsData: {
- milestones: milestonesProp,
- },
- sync: false,
- });
- };
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- it('renders the milestone icon', () => {
- factory(milestones);
-
- expect(wrapper.find(Icon).exists()).toBe(true);
- });
-
- it('renders the label as "Milestone" if only a single milestone is passed in', () => {
- factory(milestones.slice(0, 1));
-
- expect(wrapper.find('.js-label-text').text()).toEqual('Milestone');
- });
-
- it('renders the label as "Milestones" if more than one milestone is passed in', () => {
- factory(milestones);
-
- expect(wrapper.find('.js-label-text').text()).toEqual('Milestones');
- });
-
- it('renders a link to the milestone with a tooltip', () => {
- const milestone = _.first(milestones);
- factory([milestone]);
-
- const milestoneLink = wrapper.find(GlLink);
-
- expect(milestoneLink.exists()).toBe(true);
-
- expect(milestoneLink.text()).toBe(milestone.title);
-
- expect(milestoneLink.attributes('href')).toBe(milestone.web_url);
-
- expect(milestoneLink.attributes('data-original-title')).toBe(milestone.description);
- });
-});
diff --git a/spec/frontend/releases/components/release_block_spec.js b/spec/frontend/releases/components/release_block_spec.js
deleted file mode 100644
index 4be5d500fd9..00000000000
--- a/spec/frontend/releases/components/release_block_spec.js
+++ /dev/null
@@ -1,120 +0,0 @@
-import { mount } from '@vue/test-utils';
-import ReleaseBlock from '~/releases/components/release_block.vue';
-import timeagoMixin from '~/vue_shared/mixins/timeago';
-import { first } from 'underscore';
-import { release } from '../mock_data';
-
-describe('Release block', () => {
- let wrapper;
-
- const factory = releaseProp => {
- wrapper = mount(ReleaseBlock, {
- propsData: {
- release: releaseProp,
- },
- sync: false,
- });
- };
-
- const milestoneListExists = () => wrapper.find('.js-milestone-list').exists();
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- describe('with default props', () => {
- beforeEach(() => {
- factory(release);
- });
-
- it("renders the block with an id equal to the release's tag name", () => {
- expect(wrapper.attributes().id).toBe('v0.3');
- });
-
- it('renders release name', () => {
- expect(wrapper.text()).toContain(release.name);
- });
-
- it('renders commit sha', () => {
- expect(wrapper.text()).toContain(release.commit.short_id);
- });
-
- it('renders tag name', () => {
- expect(wrapper.text()).toContain(release.tag_name);
- });
-
- it('renders release date', () => {
- expect(wrapper.text()).toContain(timeagoMixin.methods.timeFormated(release.released_at));
- });
-
- it('renders number of assets provided', () => {
- expect(wrapper.find('.js-assets-count').text()).toContain(release.assets.count);
- });
-
- it('renders dropdown with the sources', () => {
- expect(wrapper.findAll('.js-sources-dropdown li').length).toEqual(
- release.assets.sources.length,
- );
-
- expect(wrapper.find('.js-sources-dropdown li a').attributes().href).toEqual(
- first(release.assets.sources).url,
- );
-
- expect(wrapper.find('.js-sources-dropdown li a').text()).toContain(
- first(release.assets.sources).format,
- );
- });
-
- it('renders list with the links provided', () => {
- expect(wrapper.findAll('.js-assets-list li').length).toEqual(release.assets.links.length);
-
- expect(wrapper.find('.js-assets-list li a').attributes().href).toEqual(
- first(release.assets.links).url,
- );
-
- expect(wrapper.find('.js-assets-list li a').text()).toContain(
- first(release.assets.links).name,
- );
- });
-
- it('renders author avatar', () => {
- expect(wrapper.find('.user-avatar-link').exists()).toBe(true);
- });
-
- describe('external label', () => {
- it('renders external label when link is external', () => {
- expect(wrapper.find('.js-assets-list li a').text()).toContain('external source');
- });
-
- it('does not render external label when link is not external', () => {
- expect(wrapper.find('.js-assets-list li:nth-child(2) a').text()).not.toContain(
- 'external source',
- );
- });
- });
-
- it('renders the milestone list if at least one milestone is associated to the release', () => {
- factory(release);
-
- expect(milestoneListExists()).toBe(true);
- });
- });
-
- it('does not render the milestone list if no milestones are associated to the release', () => {
- const releaseClone = JSON.parse(JSON.stringify(release));
- delete releaseClone.milestone;
-
- factory(releaseClone);
-
- expect(milestoneListExists()).toBe(false);
- });
-
- it('renders upcoming release badge', () => {
- const releaseClone = JSON.parse(JSON.stringify(release));
- releaseClone.upcoming_release = true;
-
- factory(releaseClone);
-
- expect(wrapper.text()).toContain('Upcoming Release');
- });
-});
diff --git a/spec/frontend/releases/detail/components/app_spec.js b/spec/frontend/releases/detail/components/app_spec.js
new file mode 100644
index 00000000000..f8eb33a69a8
--- /dev/null
+++ b/spec/frontend/releases/detail/components/app_spec.js
@@ -0,0 +1,70 @@
+import Vuex from 'vuex';
+import { mount } from '@vue/test-utils';
+import ReleaseDetailApp from '~/releases/detail/components/app';
+import { release } from '../../mock_data';
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+
+describe('Release detail component', () => {
+ let wrapper;
+ let releaseClone;
+ let actions;
+
+ beforeEach(() => {
+ gon.api_version = 'v4';
+
+ releaseClone = JSON.parse(JSON.stringify(convertObjectPropsToCamelCase(release)));
+
+ const state = {
+ release: releaseClone,
+ markdownDocsPath: 'path/to/markdown/docs',
+ };
+
+ actions = {
+ fetchRelease: jest.fn(),
+ updateRelease: jest.fn(),
+ navigateToReleasesPage: jest.fn(),
+ };
+
+ const store = new Vuex.Store({ actions, state });
+
+ wrapper = mount(ReleaseDetailApp, { store });
+
+ return wrapper.vm.$nextTick();
+ });
+
+ it('calls fetchRelease when the component is created', () => {
+ expect(actions.fetchRelease).toHaveBeenCalledTimes(1);
+ });
+
+ it('renders the description text at the top of the page', () => {
+ expect(wrapper.find('.js-subtitle-text').text()).toBe(
+ 'Releases are based on Git tags. We recommend naming tags that fit within semantic versioning, for example v1.0, v2.0-pre.',
+ );
+ });
+
+ it('renders the correct tag name in the "Tag name" field', () => {
+ expect(wrapper.find('#git-ref').element.value).toBe(releaseClone.tagName);
+ });
+
+ it('renders the correct release title in the "Release title" field', () => {
+ expect(wrapper.find('#release-title').element.value).toBe(releaseClone.name);
+ });
+
+ it('renders the release notes in the "Release notes" textarea', () => {
+ expect(wrapper.find('#release-notes').element.value).toBe(releaseClone.description);
+ });
+
+ it('renders the "Save changes" button as type="submit"', () => {
+ expect(wrapper.find('.js-submit-button').attributes('type')).toBe('submit');
+ });
+
+ it('calls updateRelease when the form is submitted', () => {
+ wrapper.find('form').trigger('submit');
+ expect(actions.updateRelease).toHaveBeenCalledTimes(1);
+ });
+
+ it('calls navigateToReleasesPage when the "Cancel" button is clicked', () => {
+ wrapper.find('.js-cancel-button').vm.$emit('click');
+ expect(actions.navigateToReleasesPage).toHaveBeenCalledTimes(1);
+ });
+});
diff --git a/spec/frontend/releases/detail/store/actions_spec.js b/spec/frontend/releases/detail/store/actions_spec.js
new file mode 100644
index 00000000000..f1c7f3c1048
--- /dev/null
+++ b/spec/frontend/releases/detail/store/actions_spec.js
@@ -0,0 +1,217 @@
+import axios from 'axios';
+import MockAdapter from 'axios-mock-adapter';
+import * as actions from '~/releases/detail/store/actions';
+import testAction from 'helpers/vuex_action_helper';
+import * as types from '~/releases/detail/store/mutation_types';
+import { release } from '../../mock_data';
+import state from '~/releases/detail/store/state';
+import createFlash from '~/flash';
+import { redirectTo } from '~/lib/utils/url_utility';
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+
+jest.mock('~/flash', () => jest.fn());
+
+jest.mock('~/lib/utils/url_utility', () => ({
+ redirectTo: jest.fn(),
+ joinPaths: jest.requireActual('~/lib/utils/url_utility').joinPaths,
+}));
+
+describe('Release detail actions', () => {
+ let stateClone;
+ let releaseClone;
+ let mock;
+ let error;
+
+ beforeEach(() => {
+ stateClone = state();
+ releaseClone = JSON.parse(JSON.stringify(release));
+ mock = new MockAdapter(axios);
+ gon.api_version = 'v4';
+ error = { message: 'An error occurred' };
+ createFlash.mockClear();
+ });
+
+ afterEach(() => {
+ mock.restore();
+ });
+
+ describe('setInitialState', () => {
+ it(`commits ${types.SET_INITIAL_STATE} with the provided object`, () => {
+ const initialState = {};
+
+ return testAction(actions.setInitialState, initialState, stateClone, [
+ { type: types.SET_INITIAL_STATE, payload: initialState },
+ ]);
+ });
+ });
+
+ describe('requestRelease', () => {
+ it(`commits ${types.REQUEST_RELEASE}`, () =>
+ testAction(actions.requestRelease, undefined, stateClone, [{ type: types.REQUEST_RELEASE }]));
+ });
+
+ describe('receiveReleaseSuccess', () => {
+ it(`commits ${types.RECEIVE_RELEASE_SUCCESS}`, () =>
+ testAction(actions.receiveReleaseSuccess, releaseClone, stateClone, [
+ { type: types.RECEIVE_RELEASE_SUCCESS, payload: releaseClone },
+ ]));
+ });
+
+ describe('receiveReleaseError', () => {
+ it(`commits ${types.RECEIVE_RELEASE_ERROR}`, () =>
+ testAction(actions.receiveReleaseError, error, stateClone, [
+ { type: types.RECEIVE_RELEASE_ERROR, payload: error },
+ ]));
+
+ it('shows a flash with an error message', () => {
+ actions.receiveReleaseError({ commit: jest.fn() }, error);
+
+ expect(createFlash).toHaveBeenCalledTimes(1);
+ expect(createFlash).toHaveBeenCalledWith(
+ 'Something went wrong while getting the release details',
+ );
+ });
+ });
+
+ describe('fetchRelease', () => {
+ let getReleaseUrl;
+
+ beforeEach(() => {
+ stateClone.projectId = '18';
+ stateClone.tagName = 'v1.3';
+ getReleaseUrl = `/api/v4/projects/${stateClone.projectId}/releases/${stateClone.tagName}`;
+ });
+
+ it(`dispatches requestRelease and receiveReleaseSuccess with the camel-case'd release object`, () => {
+ mock.onGet(getReleaseUrl).replyOnce(200, releaseClone);
+
+ return testAction(
+ actions.fetchRelease,
+ undefined,
+ stateClone,
+ [],
+ [
+ { type: 'requestRelease' },
+ {
+ type: 'receiveReleaseSuccess',
+ payload: convertObjectPropsToCamelCase(releaseClone, { deep: true }),
+ },
+ ],
+ );
+ });
+
+ it(`dispatches requestRelease and receiveReleaseError with an error object`, () => {
+ mock.onGet(getReleaseUrl).replyOnce(500);
+
+ return testAction(
+ actions.fetchRelease,
+ undefined,
+ stateClone,
+ [],
+ [{ type: 'requestRelease' }, { type: 'receiveReleaseError', payload: expect.anything() }],
+ );
+ });
+ });
+
+ describe('updateReleaseTitle', () => {
+ it(`commits ${types.UPDATE_RELEASE_TITLE} with the updated release title`, () => {
+ const newTitle = 'The new release title';
+ return testAction(actions.updateReleaseTitle, newTitle, stateClone, [
+ { type: types.UPDATE_RELEASE_TITLE, payload: newTitle },
+ ]);
+ });
+ });
+
+ describe('updateReleaseNotes', () => {
+ it(`commits ${types.UPDATE_RELEASE_NOTES} with the updated release notes`, () => {
+ const newReleaseNotes = 'The new release notes';
+ return testAction(actions.updateReleaseNotes, newReleaseNotes, stateClone, [
+ { type: types.UPDATE_RELEASE_NOTES, payload: newReleaseNotes },
+ ]);
+ });
+ });
+
+ describe('requestUpdateRelease', () => {
+ it(`commits ${types.REQUEST_UPDATE_RELEASE}`, () =>
+ testAction(actions.requestUpdateRelease, undefined, stateClone, [
+ { type: types.REQUEST_UPDATE_RELEASE },
+ ]));
+ });
+
+ describe('receiveUpdateReleaseSuccess', () => {
+ it(`commits ${types.RECEIVE_UPDATE_RELEASE_SUCCESS}`, () =>
+ testAction(
+ actions.receiveUpdateReleaseSuccess,
+ undefined,
+ stateClone,
+ [{ type: types.RECEIVE_UPDATE_RELEASE_SUCCESS }],
+ [{ type: 'navigateToReleasesPage' }],
+ ));
+ });
+
+ describe('receiveUpdateReleaseError', () => {
+ it(`commits ${types.RECEIVE_UPDATE_RELEASE_ERROR}`, () =>
+ testAction(actions.receiveUpdateReleaseError, error, stateClone, [
+ { type: types.RECEIVE_UPDATE_RELEASE_ERROR, payload: error },
+ ]));
+
+ it('shows a flash with an error message', () => {
+ actions.receiveUpdateReleaseError({ commit: jest.fn() }, error);
+
+ expect(createFlash).toHaveBeenCalledTimes(1);
+ expect(createFlash).toHaveBeenCalledWith(
+ 'Something went wrong while saving the release details',
+ );
+ });
+ });
+
+ describe('updateRelease', () => {
+ let getReleaseUrl;
+
+ beforeEach(() => {
+ stateClone.release = releaseClone;
+ stateClone.projectId = '18';
+ stateClone.tagName = 'v1.3';
+ getReleaseUrl = `/api/v4/projects/${stateClone.projectId}/releases/${stateClone.tagName}`;
+ });
+
+ it(`dispatches requestUpdateRelease and receiveUpdateReleaseSuccess`, () => {
+ mock.onPut(getReleaseUrl).replyOnce(200);
+
+ return testAction(
+ actions.updateRelease,
+ undefined,
+ stateClone,
+ [],
+ [{ type: 'requestUpdateRelease' }, { type: 'receiveUpdateReleaseSuccess' }],
+ );
+ });
+
+ it(`dispatches requestUpdateRelease and receiveUpdateReleaseError with an error object`, () => {
+ mock.onPut(getReleaseUrl).replyOnce(500);
+
+ return testAction(
+ actions.updateRelease,
+ undefined,
+ stateClone,
+ [],
+ [
+ { type: 'requestUpdateRelease' },
+ { type: 'receiveUpdateReleaseError', payload: expect.anything() },
+ ],
+ );
+ });
+ });
+
+ describe('navigateToReleasesPage', () => {
+ it(`calls redirectTo() with the URL to the releases page`, () => {
+ const releasesPagePath = 'path/to/releases/page';
+ stateClone.releasesPagePath = releasesPagePath;
+
+ actions.navigateToReleasesPage({ state: stateClone });
+
+ expect(redirectTo).toHaveBeenCalledTimes(1);
+ expect(redirectTo).toHaveBeenCalledWith(releasesPagePath);
+ });
+ });
+});
diff --git a/spec/frontend/releases/detail/store/mutations_spec.js b/spec/frontend/releases/detail/store/mutations_spec.js
new file mode 100644
index 00000000000..106a40c812e
--- /dev/null
+++ b/spec/frontend/releases/detail/store/mutations_spec.js
@@ -0,0 +1,119 @@
+/* eslint-disable jest/valid-describe */
+/*
+ * ESLint disable directive ↑ can be removed once
+ * https://github.com/jest-community/eslint-plugin-jest/issues/203
+ * is resolved
+ */
+
+import state from '~/releases/detail/store/state';
+import mutations from '~/releases/detail/store/mutations';
+import * as types from '~/releases/detail/store/mutation_types';
+import { release } from '../../mock_data';
+
+describe('Release detail mutations', () => {
+ let stateClone;
+ let releaseClone;
+
+ beforeEach(() => {
+ stateClone = state();
+ releaseClone = JSON.parse(JSON.stringify(release));
+ });
+
+ describe(types.SET_INITIAL_STATE, () => {
+ it('populates the state with initial values', () => {
+ const initialState = {
+ projectId: '18',
+ tagName: 'v1.3',
+ releasesPagePath: 'path/to/releases/page',
+ markdownDocsPath: 'path/to/markdown/docs',
+ markdownPreviewPath: 'path/to/markdown/preview',
+ };
+
+ mutations[types.SET_INITIAL_STATE](stateClone, initialState);
+
+ expect(stateClone).toEqual(expect.objectContaining(initialState));
+ });
+ });
+
+ describe(types.REQUEST_RELEASE, () => {
+ it('set state.isFetchingRelease to true', () => {
+ mutations[types.REQUEST_RELEASE](stateClone);
+
+ expect(stateClone.isFetchingRelease).toEqual(true);
+ });
+ });
+
+ describe(types.RECEIVE_RELEASE_SUCCESS, () => {
+ it('handles a successful response from the server', () => {
+ mutations[types.RECEIVE_RELEASE_SUCCESS](stateClone, releaseClone);
+
+ expect(stateClone.fetchError).toEqual(undefined);
+
+ expect(stateClone.isFetchingRelease).toEqual(false);
+
+ expect(stateClone.release).toEqual(releaseClone);
+ });
+ });
+
+ describe(types.RECEIVE_RELEASE_ERROR, () => {
+ it('handles an unsuccessful response from the server', () => {
+ const error = { message: 'An error occurred!' };
+ mutations[types.RECEIVE_RELEASE_ERROR](stateClone, error);
+
+ expect(stateClone.isFetchingRelease).toEqual(false);
+
+ expect(stateClone.release).toBeUndefined();
+
+ expect(stateClone.fetchError).toEqual(error);
+ });
+ });
+
+ describe(types.UPDATE_RELEASE_TITLE, () => {
+ it("updates the release's title", () => {
+ stateClone.release = releaseClone;
+ const newTitle = 'The new release title';
+ mutations[types.UPDATE_RELEASE_TITLE](stateClone, newTitle);
+
+ expect(stateClone.release.name).toEqual(newTitle);
+ });
+ });
+
+ describe(types.UPDATE_RELEASE_NOTES, () => {
+ it("updates the release's notes", () => {
+ stateClone.release = releaseClone;
+ const newNotes = 'The new release notes';
+ mutations[types.UPDATE_RELEASE_NOTES](stateClone, newNotes);
+
+ expect(stateClone.release.description).toEqual(newNotes);
+ });
+ });
+
+ describe(types.REQUEST_UPDATE_RELEASE, () => {
+ it('set state.isUpdatingRelease to true', () => {
+ mutations[types.REQUEST_UPDATE_RELEASE](stateClone);
+
+ expect(stateClone.isUpdatingRelease).toEqual(true);
+ });
+ });
+
+ describe(types.RECEIVE_UPDATE_RELEASE_SUCCESS, () => {
+ it('handles a successful response from the server', () => {
+ mutations[types.RECEIVE_UPDATE_RELEASE_SUCCESS](stateClone, releaseClone);
+
+ expect(stateClone.updateError).toEqual(undefined);
+
+ expect(stateClone.isUpdatingRelease).toEqual(false);
+ });
+ });
+
+ describe(types.RECEIVE_UPDATE_RELEASE_ERROR, () => {
+ it('handles an unsuccessful response from the server', () => {
+ const error = { message: 'An error occurred!' };
+ mutations[types.RECEIVE_UPDATE_RELEASE_ERROR](stateClone, error);
+
+ expect(stateClone.isUpdatingRelease).toEqual(false);
+
+ expect(stateClone.updateError).toEqual(error);
+ });
+ });
+});
diff --git a/spec/frontend/releases/list/components/__snapshots__/release_block_spec.js.snap b/spec/frontend/releases/list/components/__snapshots__/release_block_spec.js.snap
new file mode 100644
index 00000000000..8f2c0427c83
--- /dev/null
+++ b/spec/frontend/releases/list/components/__snapshots__/release_block_spec.js.snap
@@ -0,0 +1,332 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Release block with default props matches the snapshot 1`] = `
+<div
+ class="card release-block"
+ id="v0.3"
+>
+ <div
+ class="card-body"
+ >
+ <div
+ class="d-flex align-items-start"
+ >
+ <h2
+ class="card-title mt-0 mr-auto"
+ >
+
+ New release
+
+ <!---->
+ </h2>
+
+ <a
+ class="btn btn-default js-edit-button ml-2"
+ data-original-title="Edit this release"
+ href="http://0.0.0.0:3001/root/release-test/-/releases/v0.3/edit"
+ title=""
+ >
+ <svg
+ aria-hidden="true"
+ class="s16 ic-pencil"
+ >
+ <use
+ xlink:href="#pencil"
+ />
+ </svg>
+ </a>
+ </div>
+
+ <div
+ class="card-subtitle d-flex flex-wrap text-secondary"
+ >
+ <div
+ class="append-right-8"
+ >
+ <svg
+ aria-hidden="true"
+ class="align-middle s16 ic-commit"
+ >
+ <use
+ xlink:href="#commit"
+ />
+ </svg>
+
+ <span
+ data-original-title="Initial commit"
+ title=""
+ >
+ c22b0728
+ </span>
+ </div>
+
+ <div
+ class="append-right-8"
+ >
+ <svg
+ aria-hidden="true"
+ class="align-middle s16 ic-tag"
+ >
+ <use
+ xlink:href="#tag"
+ />
+ </svg>
+
+ <span
+ data-original-title="Tag"
+ title=""
+ >
+ v0.3
+ </span>
+ </div>
+
+ <div
+ class="js-milestone-list-label"
+ >
+ <svg
+ aria-hidden="true"
+ class="align-middle s16 ic-flag"
+ >
+ <use
+ xlink:href="#flag"
+ />
+ </svg>
+
+ <span
+ class="js-label-text"
+ >
+ Milestones
+ </span>
+ </div>
+
+ <a
+ class="append-right-4 prepend-left-4 js-milestone-link"
+ data-original-title="The 13.6 milestone!"
+ href="http://0.0.0.0:3001/root/release-test/-/milestones/2"
+ title=""
+ >
+
+ 13.6
+
+ </a>
+
+ •
+
+ <a
+ class="append-right-4 prepend-left-4 js-milestone-link"
+ data-original-title="The 13.5 milestone!"
+ href="http://0.0.0.0:3001/root/release-test/-/milestones/1"
+ title=""
+ >
+
+ 13.5
+
+ </a>
+
+ <!---->
+
+ <div
+ class="append-right-4"
+ >
+
+ •
+
+ <span
+ data-original-title="Aug 26, 2019 5:54pm GMT+0000"
+ title=""
+ >
+
+ released 1 month ago
+
+ </span>
+ </div>
+
+ <div
+ class="d-flex"
+ >
+
+ by
+
+ <a
+ class="user-avatar-link prepend-left-4"
+ href=""
+ >
+ <span>
+ <img
+ alt="root's avatar"
+ class="avatar s20 "
+ data-original-title=""
+ data-src="https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon"
+ height="20"
+ src="https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon"
+ title=""
+ width="20"
+ />
+
+ <div
+ aria-hidden="true"
+ class="js-user-avatar-image-toolip d-none"
+ style="display: none;"
+ >
+ <div>
+ root
+ </div>
+ </div>
+ </span>
+ <!---->
+ </a>
+ </div>
+ </div>
+
+ <div
+ class="card-text prepend-top-default"
+ >
+ <b>
+
+ Assets
+
+ <span
+ class="js-assets-count badge badge-pill"
+ >
+ 5
+ </span>
+ </b>
+
+ <ul
+ class="pl-0 mb-0 prepend-top-8 list-unstyled js-assets-list"
+ >
+ <li
+ class="append-bottom-8"
+ >
+ <a
+ class=""
+ data-original-title="Download asset"
+ href="https://google.com"
+ title=""
+ >
+ <svg
+ aria-hidden="true"
+ class="align-middle append-right-4 align-text-bottom s16 ic-package"
+ >
+ <use
+ xlink:href="#package"
+ />
+ </svg>
+
+ my link
+
+ <span>
+ (external source)
+ </span>
+ </a>
+ </li>
+ <li
+ class="append-bottom-8"
+ >
+ <a
+ class=""
+ data-original-title="Download asset"
+ href="https://gitlab.com/gitlab-org/gitlab-foss/-/jobs/artifacts/v11.6.0-rc4/download?job=rspec-mysql+41%2F50"
+ title=""
+ >
+ <svg
+ aria-hidden="true"
+ class="align-middle append-right-4 align-text-bottom s16 ic-package"
+ >
+ <use
+ xlink:href="#package"
+ />
+ </svg>
+
+ my second link
+
+ <!---->
+ </a>
+ </li>
+ </ul>
+
+ <div
+ class="dropdown"
+ >
+ <button
+ aria-expanded="false"
+ aria-haspopup="true"
+ class="btn btn-link"
+ data-toggle="dropdown"
+ type="button"
+ >
+ <svg
+ aria-hidden="true"
+ class="align-top append-right-4 s16 ic-doc-code"
+ >
+ <use
+ xlink:href="#doc-code"
+ />
+ </svg>
+
+ Source code
+
+ <svg
+ aria-hidden="true"
+ class="s16 ic-arrow-down"
+ >
+ <use
+ xlink:href="#arrow-down"
+ />
+ </svg>
+ </button>
+
+ <div
+ class="js-sources-dropdown dropdown-menu"
+ >
+ <li>
+ <a
+ class=""
+ href="http://0.0.0.0:3001/root/release-test/-/archive/v0.3/release-test-v0.3.zip"
+ >
+ Download zip
+ </a>
+ </li>
+ <li>
+ <a
+ class=""
+ href="http://0.0.0.0:3001/root/release-test/-/archive/v0.3/release-test-v0.3.tar.gz"
+ >
+ Download tar.gz
+ </a>
+ </li>
+ <li>
+ <a
+ class=""
+ href="http://0.0.0.0:3001/root/release-test/-/archive/v0.3/release-test-v0.3.tar.bz2"
+ >
+ Download tar.bz2
+ </a>
+ </li>
+ <li>
+ <a
+ class=""
+ href="http://0.0.0.0:3001/root/release-test/-/archive/v0.3/release-test-v0.3.tar"
+ >
+ Download tar
+ </a>
+ </li>
+ </div>
+ </div>
+ </div>
+
+ <div
+ class="card-text prepend-top-default"
+ >
+ <div>
+ <p
+ data-sourcepos="1:1-1:21"
+ dir="auto"
+ >
+ A super nice release!
+ </p>
+ </div>
+ </div>
+ </div>
+</div>
+`;
diff --git a/spec/frontend/releases/list/components/release_block_spec.js b/spec/frontend/releases/list/components/release_block_spec.js
new file mode 100644
index 00000000000..0b908d7d6bc
--- /dev/null
+++ b/spec/frontend/releases/list/components/release_block_spec.js
@@ -0,0 +1,266 @@
+import { mount } from '@vue/test-utils';
+import ReleaseBlock from '~/releases/list/components/release_block.vue';
+import timeagoMixin from '~/vue_shared/mixins/timeago';
+import { first } from 'underscore';
+import { release } from '../../mock_data';
+import Icon from '~/vue_shared/components/icon.vue';
+import { scrollToElement } from '~/lib/utils/common_utils';
+
+let mockLocationHash;
+jest.mock('~/lib/utils/url_utility', () => ({
+ __esModule: true,
+ getLocationHash: jest.fn().mockImplementation(() => mockLocationHash),
+}));
+
+jest.mock('~/lib/utils/common_utils', () => ({
+ __esModule: true,
+ scrollToElement: jest.fn(),
+}));
+
+describe('Release block', () => {
+ let wrapper;
+ let releaseClone;
+
+ const factory = (releaseProp, releaseEditPageFeatureFlag = true) => {
+ wrapper = mount(ReleaseBlock, {
+ propsData: {
+ release: releaseProp,
+ },
+ provide: {
+ glFeatures: {
+ releaseEditPage: releaseEditPageFeatureFlag,
+ },
+ },
+ sync: false,
+ });
+
+ return wrapper.vm.$nextTick();
+ };
+
+ const milestoneListLabel = () => wrapper.find('.js-milestone-list-label');
+ const editButton = () => wrapper.find('.js-edit-button');
+
+ beforeEach(() => {
+ releaseClone = JSON.parse(JSON.stringify(release));
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('with default props', () => {
+ beforeEach(() => factory(release));
+
+ it('matches the snapshot', () => {
+ expect(wrapper.element).toMatchSnapshot();
+ });
+
+ it("renders the block with an id equal to the release's tag name", () => {
+ expect(wrapper.attributes().id).toBe('v0.3');
+ });
+
+ it('renders an edit button that links to the "Edit release" page', () => {
+ expect(editButton().exists()).toBe(true);
+ expect(editButton().attributes('href')).toBe(release._links.edit);
+ });
+
+ it('renders release name', () => {
+ expect(wrapper.text()).toContain(release.name);
+ });
+
+ it('renders release date', () => {
+ expect(wrapper.text()).toContain(timeagoMixin.methods.timeFormated(release.released_at));
+ });
+
+ it('renders number of assets provided', () => {
+ expect(wrapper.find('.js-assets-count').text()).toContain(release.assets.count);
+ });
+
+ it('renders dropdown with the sources', () => {
+ expect(wrapper.findAll('.js-sources-dropdown li').length).toEqual(
+ release.assets.sources.length,
+ );
+
+ expect(wrapper.find('.js-sources-dropdown li a').attributes().href).toEqual(
+ first(release.assets.sources).url,
+ );
+
+ expect(wrapper.find('.js-sources-dropdown li a').text()).toContain(
+ first(release.assets.sources).format,
+ );
+ });
+
+ it('renders list with the links provided', () => {
+ expect(wrapper.findAll('.js-assets-list li').length).toEqual(release.assets.links.length);
+
+ expect(wrapper.find('.js-assets-list li a').attributes().href).toEqual(
+ first(release.assets.links).url,
+ );
+
+ expect(wrapper.find('.js-assets-list li a').text()).toContain(
+ first(release.assets.links).name,
+ );
+ });
+
+ it('renders author avatar', () => {
+ expect(wrapper.find('.user-avatar-link').exists()).toBe(true);
+ });
+
+ describe('external label', () => {
+ it('renders external label when link is external', () => {
+ expect(wrapper.find('.js-assets-list li a').text()).toContain('external source');
+ });
+
+ it('does not render external label when link is not external', () => {
+ expect(wrapper.find('.js-assets-list li:nth-child(2) a').text()).not.toContain(
+ 'external source',
+ );
+ });
+ });
+
+ it('renders the milestone icon', () => {
+ expect(
+ milestoneListLabel()
+ .find(Icon)
+ .exists(),
+ ).toBe(true);
+ });
+
+ it('renders the label as "Milestones" if more than one milestone is passed in', () => {
+ expect(
+ milestoneListLabel()
+ .find('.js-label-text')
+ .text(),
+ ).toEqual('Milestones');
+ });
+
+ it('renders a link to the milestone with a tooltip', () => {
+ const milestone = first(release.milestones);
+ const milestoneLink = wrapper.find('.js-milestone-link');
+
+ expect(milestoneLink.exists()).toBe(true);
+
+ expect(milestoneLink.text()).toBe(milestone.title);
+
+ expect(milestoneLink.attributes('href')).toBe(milestone.web_url);
+
+ expect(milestoneLink.attributes('data-original-title')).toBe(milestone.description);
+ });
+ });
+
+ it('renders commit sha', () => {
+ releaseClone.commit_path = '/commit/example';
+
+ return factory(releaseClone).then(() => {
+ expect(wrapper.text()).toContain(release.commit.short_id);
+
+ expect(wrapper.find('a[href="/commit/example"]').exists()).toBe(true);
+ });
+ });
+
+ it('renders tag name', () => {
+ releaseClone.tag_path = '/tag/example';
+
+ return factory(releaseClone).then(() => {
+ expect(wrapper.text()).toContain(release.tag_name);
+
+ expect(wrapper.find('a[href="/tag/example"]').exists()).toBe(true);
+ });
+ });
+
+ it("does not render an edit button if release._links.edit isn't a string", () => {
+ delete releaseClone._links;
+
+ return factory(releaseClone).then(() => {
+ expect(editButton().exists()).toBe(false);
+ });
+ });
+
+ it('does not render an edit button if the releaseEditPage feature flag is disabled', () =>
+ factory(releaseClone, false).then(() => {
+ expect(editButton().exists()).toBe(false);
+ }));
+
+ it('does not render the milestone list if no milestones are associated to the release', () => {
+ delete releaseClone.milestones;
+
+ return factory(releaseClone).then(() => {
+ expect(milestoneListLabel().exists()).toBe(false);
+ });
+ });
+
+ it('renders the label as "Milestone" if only a single milestone is passed in', () => {
+ releaseClone.milestones = releaseClone.milestones.slice(0, 1);
+
+ return factory(releaseClone).then(() => {
+ expect(
+ milestoneListLabel()
+ .find('.js-label-text')
+ .text(),
+ ).toEqual('Milestone');
+ });
+ });
+
+ it('renders upcoming release badge', () => {
+ releaseClone.upcoming_release = true;
+
+ return factory(releaseClone).then(() => {
+ expect(wrapper.text()).toContain('Upcoming Release');
+ });
+ });
+
+ it('slugifies the tag_name before setting it as the elements ID', () => {
+ releaseClone.tag_name = 'a dangerous tag name <script>alert("hello")</script>';
+
+ return factory(releaseClone).then(() => {
+ expect(wrapper.attributes().id).toBe('a-dangerous-tag-name-script-alert-hello-script-');
+ });
+ });
+
+ describe('anchor scrolling', () => {
+ beforeEach(() => {
+ scrollToElement.mockClear();
+ });
+
+ const hasTargetBlueBackground = () => wrapper.classes('bg-line-target-blue');
+
+ it('does not attempt to scroll the page if no anchor tag is included in the URL', () => {
+ mockLocationHash = '';
+ return factory(release).then(() => {
+ expect(scrollToElement).not.toHaveBeenCalled();
+ });
+ });
+
+ it("does not attempt to scroll the page if the anchor tag doesn't match the release's tag name", () => {
+ mockLocationHash = 'v0.4';
+ return factory(release).then(() => {
+ expect(scrollToElement).not.toHaveBeenCalled();
+ });
+ });
+
+ it("attempts to scroll itself into view if the anchor tag matches the release's tag name", () => {
+ mockLocationHash = release.tag_name;
+ return factory(release).then(() => {
+ expect(scrollToElement).toHaveBeenCalledTimes(1);
+
+ expect(scrollToElement).toHaveBeenCalledWith(wrapper.element);
+ });
+ });
+
+ it('renders with a light blue background if it is the target of the anchor', () => {
+ mockLocationHash = release.tag_name;
+
+ return factory(release).then(() => {
+ expect(hasTargetBlueBackground()).toBe(true);
+ });
+ });
+
+ it('does not render with a light blue background if it is not the target of the anchor', () => {
+ mockLocationHash = '';
+
+ return factory(release).then(() => {
+ expect(hasTargetBlueBackground()).toBe(false);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/releases/mock_data.js b/spec/frontend/releases/mock_data.js
index a0885813c7e..b2ebf1174d4 100644
--- a/spec/frontend/releases/mock_data.js
+++ b/spec/frontend/releases/mock_data.js
@@ -57,7 +57,7 @@ export const release = {
committed_date: '2019-08-26T17:47:07.000Z',
},
upcoming_release: false,
- milestone: milestones[0],
+ milestones,
assets: {
count: 5,
sources: [
@@ -89,9 +89,12 @@ export const release = {
id: 2,
name: 'my second link',
url:
- 'https://gitlab.com/gitlab-org/gitlab-ce/-/jobs/artifacts/v11.6.0-rc4/download?job=rspec-mysql+41%2F50',
+ 'https://gitlab.com/gitlab-org/gitlab-foss/-/jobs/artifacts/v11.6.0-rc4/download?job=rspec-mysql+41%2F50',
external: false,
},
],
},
+ _links: {
+ edit: 'http://0.0.0.0:3001/root/release-test/-/releases/v0.3/edit',
+ },
};
diff --git a/spec/frontend/reports/store/utils_spec.js b/spec/frontend/reports/store/utils_spec.js
index 1679d120db2..f0141b9e162 100644
--- a/spec/frontend/reports/store/utils_spec.js
+++ b/spec/frontend/reports/store/utils_spec.js
@@ -30,7 +30,9 @@ describe('Reports store utils', () => {
const data = { failed: 3, total: 10 };
const result = utils.summaryTextBuilder(name, data);
- expect(result).toBe('Test summary contained 3 failed test results out of 10 total tests');
+ expect(result).toBe(
+ 'Test summary contained 3 failed/error test results out of 10 total tests',
+ );
});
it('should render text for multiple fixed results', () => {
@@ -47,7 +49,7 @@ describe('Reports store utils', () => {
const result = utils.summaryTextBuilder(name, data);
expect(result).toBe(
- 'Test summary contained 3 failed test results and 4 fixed test results out of 10 total tests',
+ 'Test summary contained 3 failed/error test results and 4 fixed test results out of 10 total tests',
);
});
@@ -57,7 +59,7 @@ describe('Reports store utils', () => {
const result = utils.summaryTextBuilder(name, data);
expect(result).toBe(
- 'Test summary contained 1 failed test result and 1 fixed test result out of 10 total tests',
+ 'Test summary contained 1 failed/error test result and 1 fixed test result out of 10 total tests',
);
});
});
@@ -84,7 +86,7 @@ describe('Reports store utils', () => {
const data = { failed: 3, total: 10 };
const result = utils.reportTextBuilder(name, data);
- expect(result).toBe('Rspec found 3 failed test results out of 10 total tests');
+ expect(result).toBe('Rspec found 3 failed/error test results out of 10 total tests');
});
it('should render text for multiple fixed results', () => {
@@ -101,7 +103,7 @@ describe('Reports store utils', () => {
const result = utils.reportTextBuilder(name, data);
expect(result).toBe(
- 'Rspec found 3 failed test results and 4 fixed test results out of 10 total tests',
+ 'Rspec found 3 failed/error test results and 4 fixed test results out of 10 total tests',
);
});
@@ -111,7 +113,7 @@ describe('Reports store utils', () => {
const result = utils.reportTextBuilder(name, data);
expect(result).toBe(
- 'Rspec found 1 failed test result and 1 fixed test result out of 10 total tests',
+ 'Rspec found 1 failed/error test result and 1 fixed test result out of 10 total tests',
);
});
});
diff --git a/spec/frontend/repository/components/__snapshots__/last_commit_spec.js.snap b/spec/frontend/repository/components/__snapshots__/last_commit_spec.js.snap
index cd8372a8800..08173f4f0c4 100644
--- a/spec/frontend/repository/components/__snapshots__/last_commit_spec.js.snap
+++ b/spec/frontend/repository/components/__snapshots__/last_commit_spec.js.snap
@@ -60,6 +60,111 @@ exports[`Repository last commit component renders commit widget 1`] = `
<div
class="commit-actions flex-row"
>
+ <!---->
+
+ <gllink-stub
+ class="js-commit-pipeline"
+ data-original-title="Commit: failed"
+ href="https://test.com/pipeline"
+ title=""
+ >
+ <ciicon-stub
+ aria-label="Commit: failed"
+ cssclasses=""
+ size="24"
+ status="[object Object]"
+ />
+ </gllink-stub>
+
+ <div
+ class="commit-sha-group d-flex"
+ >
+ <div
+ class="label label-monospace monospace"
+ >
+
+ 12345678
+
+ </div>
+
+ <clipboardbutton-stub
+ cssclass="btn-default"
+ text="123456789"
+ title="Copy commit SHA"
+ tooltipplacement="bottom"
+ />
+ </div>
+ </div>
+ </div>
+</div>
+`;
+
+exports[`Repository last commit component renders the signature HTML as returned by the backend 1`] = `
+<div
+ class="info-well d-none d-sm-flex project-last-commit commit p-3"
+>
+ <useravatarlink-stub
+ class="avatar-cell"
+ imgalt=""
+ imgcssclasses=""
+ imgsize="40"
+ imgsrc="https://test.com"
+ linkhref="https://test.com/test"
+ tooltipplacement="top"
+ tooltiptext=""
+ username=""
+ />
+
+ <div
+ class="commit-detail flex-list"
+ >
+ <div
+ class="commit-content qa-commit-content"
+ >
+ <gllink-stub
+ class="commit-row-message item-title"
+ href="https://test.com/commit/123"
+ >
+
+ Commit title
+
+ </gllink-stub>
+
+ <!---->
+
+ <div
+ class="committer"
+ >
+ <gllink-stub
+ class="commit-author-link js-user-link"
+ href="https://test.com/test"
+ >
+
+ Test
+
+ </gllink-stub>
+
+ authored
+
+ <timeagotooltip-stub
+ cssclass=""
+ time="2019-01-01"
+ tooltipplacement="bottom"
+ />
+ </div>
+
+ <!---->
+ </div>
+
+ <div
+ class="commit-actions flex-row"
+ >
+ <div>
+ <button>
+ Verified
+ </button>
+ </div>
+
<gllink-stub
class="js-commit-pipeline"
data-original-title="Commit: failed"
@@ -88,7 +193,7 @@ exports[`Repository last commit component renders commit widget 1`] = `
<clipboardbutton-stub
cssclass="btn-default"
text="123456789"
- title="Copy commit SHA to clipboard"
+ title="Copy commit SHA"
tooltipplacement="bottom"
/>
</div>
diff --git a/spec/frontend/repository/components/last_commit_spec.js b/spec/frontend/repository/components/last_commit_spec.js
index 14479f3c3a4..01b56d453e6 100644
--- a/spec/frontend/repository/components/last_commit_spec.js
+++ b/spec/frontend/repository/components/last_commit_spec.js
@@ -107,4 +107,10 @@ describe('Repository last commit component', () => {
expect(vm.find('.commit-row-description').isVisible()).toBe(true);
expect(vm.find('.text-expander').classes('open')).toBe(true);
});
+
+ it('renders the signature HTML as returned by the backend', () => {
+ factory(createCommitData({ signatureHtml: '<button>Verified</button>' }));
+
+ expect(vm.element).toMatchSnapshot();
+ });
});
diff --git a/spec/frontend/sidebar/__snapshots__/confidential_issue_sidebar_spec.js.snap b/spec/frontend/sidebar/__snapshots__/confidential_issue_sidebar_spec.js.snap
new file mode 100644
index 00000000000..1f93336e755
--- /dev/null
+++ b/spec/frontend/sidebar/__snapshots__/confidential_issue_sidebar_spec.js.snap
@@ -0,0 +1,229 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Confidential Issue Sidebar Block renders for isConfidential = false and isEditable = false 1`] = `
+<div
+ class="block issuable-sidebar-item confidentiality"
+>
+ <div
+ class="sidebar-collapsed-icon"
+ data-boundary="viewport"
+ data-container="body"
+ data-original-title="Not confidential"
+ data-placement="left"
+ title=""
+ >
+ <icon-stub
+ aria-hidden="true"
+ name="eye"
+ size="16"
+ />
+ </div>
+
+ <div
+ class="title hide-collapsed"
+ >
+
+ Confidentiality
+
+ <!---->
+ </div>
+
+ <div
+ class="value sidebar-item-value hide-collapsed"
+ >
+ <!---->
+
+ <div
+ class="no-value sidebar-item-value"
+ >
+ <icon-stub
+ aria-hidden="true"
+ class="sidebar-item-icon inline"
+ name="eye"
+ size="16"
+ />
+
+ Not confidential
+
+ </div>
+ </div>
+
+ <!---->
+</div>
+`;
+
+exports[`Confidential Issue Sidebar Block renders for isConfidential = false and isEditable = true 1`] = `
+<div
+ class="block issuable-sidebar-item confidentiality"
+>
+ <div
+ class="sidebar-collapsed-icon"
+ data-boundary="viewport"
+ data-container="body"
+ data-original-title="Not confidential"
+ data-placement="left"
+ title=""
+ >
+ <icon-stub
+ aria-hidden="true"
+ name="eye"
+ size="16"
+ />
+ </div>
+
+ <div
+ class="title hide-collapsed"
+ >
+
+ Confidentiality
+
+ <a
+ class="float-right confidential-edit"
+ data-track-event="click_edit_button"
+ data-track-label="right_sidebar"
+ data-track-property="confidentiality"
+ href="#"
+ >
+
+ Edit
+
+ </a>
+ </div>
+
+ <div
+ class="value sidebar-item-value hide-collapsed"
+ >
+ <!---->
+
+ <div
+ class="no-value sidebar-item-value"
+ >
+ <icon-stub
+ aria-hidden="true"
+ class="sidebar-item-icon inline"
+ name="eye"
+ size="16"
+ />
+
+ Not confidential
+
+ </div>
+ </div>
+
+ <!---->
+</div>
+`;
+
+exports[`Confidential Issue Sidebar Block renders for isConfidential = true and isEditable = false 1`] = `
+<div
+ class="block issuable-sidebar-item confidentiality"
+>
+ <div
+ class="sidebar-collapsed-icon"
+ data-boundary="viewport"
+ data-container="body"
+ data-original-title="Confidential"
+ data-placement="left"
+ title=""
+ >
+ <icon-stub
+ aria-hidden="true"
+ name="eye-slash"
+ size="16"
+ />
+ </div>
+
+ <div
+ class="title hide-collapsed"
+ >
+
+ Confidentiality
+
+ <!---->
+ </div>
+
+ <div
+ class="value sidebar-item-value hide-collapsed"
+ >
+ <!---->
+
+ <div
+ class="value sidebar-item-value hide-collapsed"
+ >
+ <icon-stub
+ aria-hidden="true"
+ class="sidebar-item-icon inline is-active"
+ name="eye-slash"
+ size="16"
+ />
+
+ This issue is confidential
+
+ </div>
+ </div>
+
+ <!---->
+</div>
+`;
+
+exports[`Confidential Issue Sidebar Block renders for isConfidential = true and isEditable = true 1`] = `
+<div
+ class="block issuable-sidebar-item confidentiality"
+>
+ <div
+ class="sidebar-collapsed-icon"
+ data-boundary="viewport"
+ data-container="body"
+ data-original-title="Confidential"
+ data-placement="left"
+ title=""
+ >
+ <icon-stub
+ aria-hidden="true"
+ name="eye-slash"
+ size="16"
+ />
+ </div>
+
+ <div
+ class="title hide-collapsed"
+ >
+
+ Confidentiality
+
+ <a
+ class="float-right confidential-edit"
+ data-track-event="click_edit_button"
+ data-track-label="right_sidebar"
+ data-track-property="confidentiality"
+ href="#"
+ >
+
+ Edit
+
+ </a>
+ </div>
+
+ <div
+ class="value sidebar-item-value hide-collapsed"
+ >
+ <!---->
+
+ <div
+ class="value sidebar-item-value hide-collapsed"
+ >
+ <icon-stub
+ aria-hidden="true"
+ class="sidebar-item-icon inline is-active"
+ name="eye-slash"
+ size="16"
+ />
+
+ This issue is confidential
+
+ </div>
+ </div>
+
+ <!---->
+</div>
+`;
diff --git a/spec/frontend/sidebar/__snapshots__/todo_spec.js.snap b/spec/frontend/sidebar/__snapshots__/todo_spec.js.snap
new file mode 100644
index 00000000000..abcdf600a67
--- /dev/null
+++ b/spec/frontend/sidebar/__snapshots__/todo_spec.js.snap
@@ -0,0 +1,37 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`SidebarTodo template renders component container element with proper data attributes 1`] = `
+<button
+ aria-label="Mark as done"
+ class="btn btn-default btn-todo issuable-header-btn float-right"
+ data-boundary="viewport"
+ data-container="body"
+ data-issuable-id="1"
+ data-issuable-type="epic"
+ data-original-title=""
+ data-placement="left"
+ title=""
+ type="button"
+>
+ <icon-stub
+ class="todo-undone"
+ name="todo-done"
+ size="16"
+ style="display: none;"
+ />
+
+ <span
+ class="issuable-todo-inner"
+ >
+ Mark as done
+ </span>
+
+ <glloadingicon-stub
+ color="orange"
+ inline="true"
+ label="Loading"
+ size="sm"
+ style="display: none;"
+ />
+</button>
+`;
diff --git a/spec/frontend/sidebar/confidential_issue_sidebar_spec.js b/spec/frontend/sidebar/confidential_issue_sidebar_spec.js
new file mode 100644
index 00000000000..1ec5a94ba68
--- /dev/null
+++ b/spec/frontend/sidebar/confidential_issue_sidebar_spec.js
@@ -0,0 +1,167 @@
+import { shallowMount } from '@vue/test-utils';
+import ConfidentialIssueSidebar from '~/sidebar/components/confidential/confidential_issue_sidebar.vue';
+import { mockTracking, triggerEvent } from 'helpers/tracking_helper';
+import EditForm from '~/sidebar/components/confidential/edit_form.vue';
+import SidebarService from '~/sidebar/services/sidebar_service';
+import createFlash from '~/flash';
+import RecaptchaModal from '~/vue_shared/components/recaptcha_modal';
+
+jest.mock('~/flash');
+jest.mock('~/sidebar/services/sidebar_service');
+
+describe('Confidential Issue Sidebar Block', () => {
+ let wrapper;
+
+ const findRecaptchaModal = () => wrapper.find(RecaptchaModal);
+
+ const triggerUpdateConfidentialAttribute = () => {
+ wrapper.setData({ edit: true });
+ return (
+ // wait for edit form to become visible
+ wrapper.vm
+ .$nextTick()
+ .then(() => {
+ const editForm = wrapper.find(EditForm);
+ const { updateConfidentialAttribute } = editForm.props();
+ updateConfidentialAttribute();
+ })
+ // wait for reCAPTCHA modal to render
+ .then(() => wrapper.vm.$nextTick())
+ );
+ };
+
+ const createComponent = propsData => {
+ const service = new SidebarService();
+ wrapper = shallowMount(ConfidentialIssueSidebar, {
+ propsData: {
+ service,
+ ...propsData,
+ },
+ sync: false,
+ });
+ };
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ jest.spyOn(window.location, 'reload').mockImplementation();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it.each`
+ isConfidential | isEditable
+ ${false} | ${false}
+ ${false} | ${true}
+ ${true} | ${false}
+ ${true} | ${true}
+ `(
+ 'renders for isConfidential = $isConfidential and isEditable = $isEditable',
+ ({ isConfidential, isEditable }) => {
+ createComponent({
+ isConfidential,
+ isEditable,
+ });
+
+ expect(wrapper.element).toMatchSnapshot();
+ },
+ );
+
+ describe('if editable', () => {
+ beforeEach(() => {
+ createComponent({
+ isConfidential: true,
+ isEditable: true,
+ });
+ });
+
+ it('displays the edit form when editable', () => {
+ wrapper.setData({ edit: false });
+
+ wrapper.find({ ref: 'editLink' }).trigger('click');
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(wrapper.find(EditForm).exists()).toBe(true);
+ });
+ });
+
+ it('displays the edit form when opened from collapsed state', () => {
+ wrapper.setData({ edit: false });
+
+ wrapper.find({ ref: 'collapseIcon' }).trigger('click');
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(wrapper.find(EditForm).exists()).toBe(true);
+ });
+ });
+
+ it('tracks the event when "Edit" is clicked', () => {
+ const spy = mockTracking('_category_', wrapper.element, jest.spyOn);
+
+ const editLink = wrapper.find({ ref: 'editLink' });
+ triggerEvent(editLink.element);
+
+ expect(spy).toHaveBeenCalledWith('_category_', 'click_edit_button', {
+ label: 'right_sidebar',
+ property: 'confidentiality',
+ });
+ });
+
+ describe('for successful update', () => {
+ beforeEach(() => {
+ SidebarService.prototype.update.mockResolvedValue({ data: 'irrelevant' });
+ });
+
+ it('reloads the page', () =>
+ triggerUpdateConfidentialAttribute().then(() => {
+ expect(window.location.reload).toHaveBeenCalled();
+ }));
+
+ it('does not show an error message', () =>
+ triggerUpdateConfidentialAttribute().then(() => {
+ expect(createFlash).not.toHaveBeenCalled();
+ }));
+ });
+
+ describe('for update error', () => {
+ beforeEach(() => {
+ SidebarService.prototype.update.mockRejectedValue(new Error('updating failed!'));
+ });
+
+ it('does not reload the page', () =>
+ triggerUpdateConfidentialAttribute().then(() => {
+ expect(window.location.reload).not.toHaveBeenCalled();
+ }));
+
+ it('shows an error message', () =>
+ triggerUpdateConfidentialAttribute().then(() => {
+ expect(createFlash).toHaveBeenCalled();
+ }));
+ });
+
+ describe('for spam error', () => {
+ beforeEach(() => {
+ SidebarService.prototype.update.mockRejectedValue({ name: 'SpamError' });
+ });
+
+ it('does not reload the page', () =>
+ triggerUpdateConfidentialAttribute().then(() => {
+ expect(window.location.reload).not.toHaveBeenCalled();
+ }));
+
+ it('does not show an error message', () =>
+ triggerUpdateConfidentialAttribute().then(() => {
+ expect(createFlash).not.toHaveBeenCalled();
+ }));
+
+ it('shows a reCAPTCHA modal', () => {
+ expect(findRecaptchaModal().exists()).toBe(false);
+
+ return triggerUpdateConfidentialAttribute().then(() => {
+ expect(findRecaptchaModal().exists()).toBe(true);
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/sidebar/todo_spec.js b/spec/frontend/sidebar/todo_spec.js
new file mode 100644
index 00000000000..c93bbadc264
--- /dev/null
+++ b/spec/frontend/sidebar/todo_spec.js
@@ -0,0 +1,93 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlLoadingIcon } from '@gitlab/ui';
+
+import SidebarTodos from '~/sidebar/components/todo_toggle/todo.vue';
+import Icon from '~/vue_shared/components/icon.vue';
+
+const defaultProps = {
+ issuableId: 1,
+ issuableType: 'epic',
+};
+
+describe('SidebarTodo', () => {
+ let wrapper;
+
+ const createComponent = (props = {}) => {
+ wrapper = shallowMount(SidebarTodos, {
+ sync: false,
+ propsData: {
+ ...defaultProps,
+ ...props,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it.each`
+ state | classes
+ ${false} | ${['btn', 'btn-default', 'btn-todo', 'issuable-header-btn', 'float-right']}
+ ${true} | ${['btn-blank', 'btn-todo', 'sidebar-collapsed-icon', 'dont-change-state']}
+ `('returns todo button classes for when `collapsed` prop is `$state`', ({ state, classes }) => {
+ createComponent({ collapsed: state });
+ expect(wrapper.find('button').classes()).toStrictEqual(classes);
+ });
+
+ it.each`
+ isTodo | iconClass | label | icon
+ ${false} | ${''} | ${'Add a To Do'} | ${'todo-add'}
+ ${true} | ${'todo-undone'} | ${'Mark as done'} | ${'todo-done'}
+ `(
+ 'renders proper button when `isTodo` prop is `$isTodo`',
+ ({ isTodo, iconClass, label, icon }) => {
+ createComponent({ isTodo });
+
+ expect(
+ wrapper
+ .find(Icon)
+ .classes()
+ .join(' '),
+ ).toStrictEqual(iconClass);
+ expect(wrapper.find(Icon).props('name')).toStrictEqual(icon);
+ expect(wrapper.find('button').text()).toBe(label);
+ },
+ );
+
+ describe('template', () => {
+ it('emits `toggleTodo` event when clicked on button', () => {
+ createComponent();
+ wrapper.find('button').trigger('click');
+
+ expect(wrapper.emitted().toggleTodo).toBeTruthy();
+ });
+
+ it('renders component container element with proper data attributes', () => {
+ createComponent({
+ issuableId: 1,
+ issuableType: 'epic',
+ });
+
+ expect(wrapper.element).toMatchSnapshot();
+ });
+
+ it('renders button label element when `collapsed` prop is `false`', () => {
+ createComponent({ collapsed: false });
+
+ expect(wrapper.find('span.issuable-todo-inner').text()).toBe('Mark as done');
+ });
+
+ it('renders button icon when `collapsed` prop is `true`', () => {
+ createComponent({ collapsed: true });
+
+ expect(wrapper.find(Icon).props('name')).toBe('todo-done');
+ });
+
+ it('renders loading icon when `isActionActive` prop is true', () => {
+ createComponent({ isActionActive: true });
+
+ expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
+ });
+ });
+});
diff --git a/spec/frontend/test_setup.js b/spec/frontend/test_setup.js
index 6e1f1038dcd..0b9cfa44409 100644
--- a/spec/frontend/test_setup.js
+++ b/spec/frontend/test_setup.js
@@ -8,6 +8,8 @@ import { getJSONFixture, loadHTMLFixture, setHTMLFixture } from './helpers/fixtu
import { setupManualMocks } from './mocks/mocks_helper';
import customMatchers from './matchers';
+import './helpers/dom_shims';
+
// Expose jQuery so specs using jQuery plugins can be imported nicely.
// Here is an issue to explore better alternatives:
// https://gitlab.com/gitlab-org/gitlab/issues/12448
diff --git a/spec/frontend/tracking_spec.js b/spec/frontend/tracking_spec.js
index dfc068ab6ea..964f8b8787e 100644
--- a/spec/frontend/tracking_spec.js
+++ b/spec/frontend/tracking_spec.js
@@ -1,10 +1,9 @@
-import $ from 'jquery';
import { setHTMLFixture } from './helpers/fixtures';
-
import Tracking, { initUserTracking } from '~/tracking';
describe('Tracking', () => {
let snowplowSpy;
+ let bindDocumentSpy;
beforeEach(() => {
window.snowplow = window.snowplow || (() => {});
@@ -17,6 +16,10 @@ describe('Tracking', () => {
});
describe('initUserTracking', () => {
+ beforeEach(() => {
+ bindDocumentSpy = jest.spyOn(Tracking, 'bindDocument').mockImplementation(() => null);
+ });
+
it('calls through to get a new tracker with the expected options', () => {
initUserTracking();
expect(snowplowSpy).toHaveBeenCalledWith('newTracker', '_namespace_', 'app.gitfoo.com', {
@@ -50,6 +53,11 @@ describe('Tracking', () => {
expect(snowplowSpy).toHaveBeenCalledWith('enableFormTracking');
expect(snowplowSpy).toHaveBeenCalledWith('enableLinkClickTracking');
});
+
+ it('binds the document event handling', () => {
+ initUserTracking();
+ expect(bindDocumentSpy).toHaveBeenCalled();
+ });
});
describe('.event', () => {
@@ -62,11 +70,15 @@ describe('Tracking', () => {
it('tracks to snowplow (our current tracking system)', () => {
Tracking.event('_category_', '_eventName_', { label: '_label_' });
- expect(snowplowSpy).toHaveBeenCalledWith('trackStructEvent', '_category_', '_eventName_', {
- label: '_label_',
- property: '',
- value: '',
- });
+ expect(snowplowSpy).toHaveBeenCalledWith(
+ 'trackStructEvent',
+ '_category_',
+ '_eventName_',
+ '_label_',
+ undefined,
+ undefined,
+ undefined,
+ );
});
it('skips tracking if snowplow is unavailable', () => {
@@ -99,83 +111,70 @@ describe('Tracking', () => {
});
describe('tracking interface events', () => {
- let eventSpy = null;
- let subject = null;
+ let eventSpy;
+
+ const trigger = (selector, eventName = 'click') => {
+ const event = new Event(eventName, { bubbles: true });
+ document.querySelector(selector).dispatchEvent(event);
+ };
beforeEach(() => {
eventSpy = jest.spyOn(Tracking, 'event');
- subject = new Tracking('_category_');
+ Tracking.bindDocument('_category_'); // only happens once
setHTMLFixture(`
<input data-track-event="click_input1" data-track-label="_label_" value="_value_"/>
<input data-track-event="click_input2" data-track-value="_value_override_" value="_value_"/>
<input type="checkbox" data-track-event="toggle_checkbox" value="_value_" checked/>
<input class="dropdown" data-track-event="toggle_dropdown"/>
- <div class="js-projects-list-holder"></div>
+ <div data-track-event="nested_event"><span class="nested"></span></div>
`);
});
it('binds to clicks on elements matching [data-track-event]', () => {
- subject.bind(document);
- $('[data-track-event="click_input1"]').click();
+ trigger('[data-track-event="click_input1"]');
expect(eventSpy).toHaveBeenCalledWith('_category_', 'click_input1', {
label: '_label_',
value: '_value_',
- property: '',
});
});
it('allows value override with the data-track-value attribute', () => {
- subject.bind(document);
- $('[data-track-event="click_input2"]').click();
+ trigger('[data-track-event="click_input2"]');
expect(eventSpy).toHaveBeenCalledWith('_category_', 'click_input2', {
- label: '',
value: '_value_override_',
- property: '',
});
});
it('handles checkbox values correctly', () => {
- subject.bind(document);
- const $checkbox = $('[data-track-event="toggle_checkbox"]');
-
- $checkbox.click(); // unchecking
+ trigger('[data-track-event="toggle_checkbox"]'); // checking
expect(eventSpy).toHaveBeenCalledWith('_category_', 'toggle_checkbox', {
- label: '',
- property: '',
value: false,
});
- $checkbox.click(); // checking
+ trigger('[data-track-event="toggle_checkbox"]'); // unchecking
expect(eventSpy).toHaveBeenCalledWith('_category_', 'toggle_checkbox', {
- label: '',
- property: '',
value: '_value_',
});
});
it('handles bootstrap dropdowns', () => {
- new Tracking('_category_').bind(document);
- const $dropdown = $('[data-track-event="toggle_dropdown"]');
+ trigger('[data-track-event="toggle_dropdown"]', 'show.bs.dropdown'); // showing
- $dropdown.trigger('show.bs.dropdown'); // showing
+ expect(eventSpy).toHaveBeenCalledWith('_category_', 'toggle_dropdown_show', {});
- expect(eventSpy).toHaveBeenCalledWith('_category_', 'toggle_dropdown_show', {
- label: '',
- property: '',
- value: '',
- });
+ trigger('[data-track-event="toggle_dropdown"]', 'hide.bs.dropdown'); // hiding
+
+ expect(eventSpy).toHaveBeenCalledWith('_category_', 'toggle_dropdown_hide', {});
+ });
- $dropdown.trigger('hide.bs.dropdown'); // hiding
+ it('handles nested elements inside an element with tracking', () => {
+ trigger('span.nested', 'click');
- expect(eventSpy).toHaveBeenCalledWith('_category_', 'toggle_dropdown_hide', {
- label: '',
- property: '',
- value: '',
- });
+ expect(eventSpy).toHaveBeenCalledWith('_category_', 'nested_event', {});
});
});
});
diff --git a/spec/frontend/vue_mr_widget/components/artifacts_list_app_spec.js b/spec/frontend/vue_mr_widget/components/artifacts_list_app_spec.js
new file mode 100644
index 00000000000..7d593a77bf3
--- /dev/null
+++ b/spec/frontend/vue_mr_widget/components/artifacts_list_app_spec.js
@@ -0,0 +1,121 @@
+import { mount, createLocalVue } from '@vue/test-utils';
+import Vuex from 'vuex';
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
+import { GlLoadingIcon } from '@gitlab/ui';
+import { TEST_HOST } from 'helpers/test_constants';
+import ArtifactsListApp from '~/vue_merge_request_widget/components/artifacts_list_app.vue';
+import createStore from '~/vue_merge_request_widget/stores/artifacts_list';
+import { artifactsList } from './mock_data';
+
+describe('Merge Requests Artifacts list app', () => {
+ let wrapper;
+ let mock;
+ const store = createStore();
+ const localVue = createLocalVue();
+ localVue.use(Vuex);
+
+ const actionSpies = {
+ fetchArtifacts: jest.fn(),
+ };
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ mock.restore();
+ });
+
+ const createComponent = () => {
+ wrapper = mount(localVue.extend(ArtifactsListApp), {
+ propsData: {
+ endpoint: TEST_HOST,
+ },
+ store,
+ methods: {
+ ...actionSpies,
+ },
+ localVue,
+ sync: false,
+ });
+ };
+
+ const findButtons = () => wrapper.findAll('button');
+ const findTitle = () => wrapper.find('.js-title');
+ const findErrorMessage = () => wrapper.find('.js-error-state');
+ const findTableRows = () => wrapper.findAll('tbody tr');
+
+ describe('while loading', () => {
+ beforeEach(() => {
+ createComponent();
+ store.dispatch('requestArtifacts');
+ return wrapper.vm.$nextTick();
+ });
+
+ it('renders a loading icon', () => {
+ const loadingIcon = wrapper.find(GlLoadingIcon);
+ expect(loadingIcon.exists()).toBe(true);
+ });
+
+ it('renders loading text', () => {
+ expect(findTitle().text()).toBe('Loading artifacts');
+ });
+
+ it('renders disabled buttons', () => {
+ const buttons = findButtons();
+ expect(buttons.at(0).attributes('disabled')).toBe('disabled');
+ expect(buttons.at(1).attributes('disabled')).toBe('disabled');
+ });
+ });
+
+ describe('with results', () => {
+ beforeEach(() => {
+ createComponent();
+ mock.onGet(wrapper.vm.$store.state.endpoint).reply(200, artifactsList, {});
+ store.dispatch('receiveArtifactsSuccess', {
+ data: artifactsList,
+ status: 200,
+ });
+ return wrapper.vm.$nextTick();
+ });
+
+ it('renders a title with the number of artifacts', () => {
+ expect(findTitle().text()).toBe('View 2 exposed artifacts');
+ });
+
+ it('renders both buttons enabled', () => {
+ const buttons = findButtons();
+ expect(buttons.at(0).attributes('disabled')).toBe(undefined);
+ expect(buttons.at(1).attributes('disabled')).toBe(undefined);
+ });
+
+ describe('on click', () => {
+ it('renders the list of artifacts', () => {
+ findTitle().trigger('click');
+ wrapper.vm.$nextTick(() => {
+ expect(findTableRows().length).toEqual(2);
+ });
+ });
+ });
+ });
+
+ describe('with error', () => {
+ beforeEach(() => {
+ createComponent();
+ mock.onGet(wrapper.vm.$store.state.endpoint).reply(500, {}, {});
+ store.dispatch('receiveArtifactsError');
+ return wrapper.vm.$nextTick();
+ });
+
+ it('renders the error state', () => {
+ expect(findErrorMessage().text()).toBe('An error occurred while fetching the artifacts');
+ });
+
+ it('does not render buttons', () => {
+ const buttons = findButtons();
+ expect(buttons.exists()).toBe(false);
+ });
+ });
+});
diff --git a/spec/frontend/vue_mr_widget/components/artifacts_list_spec.js b/spec/frontend/vue_mr_widget/components/artifacts_list_spec.js
new file mode 100644
index 00000000000..8c805faf574
--- /dev/null
+++ b/spec/frontend/vue_mr_widget/components/artifacts_list_spec.js
@@ -0,0 +1,61 @@
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+import { GlLink } from '@gitlab/ui';
+import ArtifactsList from '~/vue_merge_request_widget/components/artifacts_list.vue';
+import { artifactsList } from './mock_data';
+
+describe('Artifacts List', () => {
+ let wrapper;
+ const localVue = createLocalVue();
+
+ const data = {
+ artifacts: artifactsList,
+ };
+
+ const mountComponent = props => {
+ wrapper = shallowMount(localVue.extend(ArtifactsList), {
+ propsData: {
+ ...props,
+ },
+ sync: false,
+ localVue,
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ beforeEach(() => {
+ mountComponent(data);
+ });
+
+ it('renders list of artifacts', () => {
+ expect(wrapper.findAll('tbody tr').length).toEqual(data.artifacts.length);
+ });
+
+ it('renders link for the artifact', () => {
+ expect(wrapper.find(GlLink).attributes('href')).toEqual(data.artifacts[0].url);
+ });
+
+ it('renders artifact name', () => {
+ expect(wrapper.find(GlLink).text()).toEqual(data.artifacts[0].text);
+ });
+
+ it('renders job url', () => {
+ expect(
+ wrapper
+ .findAll(GlLink)
+ .at(1)
+ .attributes('href'),
+ ).toEqual(data.artifacts[0].job_path);
+ });
+
+ it('renders job name', () => {
+ expect(
+ wrapper
+ .findAll(GlLink)
+ .at(1)
+ .text(),
+ ).toEqual(data.artifacts[0].job_name);
+ });
+});
diff --git a/spec/frontend/vue_mr_widget/components/mock_data.js b/spec/frontend/vue_mr_widget/components/mock_data.js
new file mode 100644
index 00000000000..39c7d75cda5
--- /dev/null
+++ b/spec/frontend/vue_mr_widget/components/mock_data.js
@@ -0,0 +1,15 @@
+// eslint-disable-next-line import/prefer-default-export
+export const artifactsList = [
+ {
+ text: 'result.txt',
+ url: 'bar',
+ job_name: 'generate-artifact',
+ job_path: 'bar',
+ },
+ {
+ text: 'foo.txt',
+ url: 'foo',
+ job_name: 'foo-artifact',
+ job_path: 'foo',
+ },
+];
diff --git a/spec/frontend/vue_mr_widget/components/mr_collapsible_extension_spec.js b/spec/frontend/vue_mr_widget/components/mr_collapsible_extension_spec.js
new file mode 100644
index 00000000000..ee107f297ef
--- /dev/null
+++ b/spec/frontend/vue_mr_widget/components/mr_collapsible_extension_spec.js
@@ -0,0 +1,99 @@
+import { mount } from '@vue/test-utils';
+import MrCollapsibleSection from '~/vue_merge_request_widget/components/mr_collapsible_extension.vue';
+import { GlLoadingIcon } from '@gitlab/ui';
+
+describe('Merge Request Collapsible Extension', () => {
+ let wrapper;
+ const data = {
+ title: 'View artifacts',
+ };
+
+ const mountComponent = props => {
+ wrapper = mount(MrCollapsibleSection, {
+ propsData: {
+ ...props,
+ },
+ slots: {
+ default: '<div class="js-slot">Foo</div>',
+ },
+ });
+ };
+
+ const findTitle = () => wrapper.find('.js-title');
+ const findErrorMessage = () => wrapper.find('.js-error-state');
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('while collapsed', () => {
+ beforeEach(() => {
+ mountComponent(data);
+ });
+
+ it('renders provided title', () => {
+ expect(findTitle().text()).toBe(data.title);
+ });
+
+ it('renders angle-right icon', () => {
+ expect(wrapper.vm.arrowIconName).toBe('angle-right');
+ });
+
+ describe('onClick', () => {
+ beforeEach(() => {
+ wrapper.find('button').trigger('click');
+ });
+
+ it('rendes the provided slot', () => {
+ expect(wrapper.find('.js-slot').isVisible()).toBe(true);
+ });
+
+ it('renders `Collapse` as the title', () => {
+ expect(findTitle().text()).toBe('Collapse');
+ });
+
+ it('renders angle-down icon', () => {
+ expect(wrapper.vm.arrowIconName).toBe('angle-down');
+ });
+ });
+ });
+
+ describe('while loading', () => {
+ beforeEach(() => {
+ mountComponent(Object.assign({}, data, { isLoading: true }));
+ });
+
+ it('renders the buttons disabled', () => {
+ expect(
+ wrapper
+ .findAll('button')
+ .at(0)
+ .attributes('disabled'),
+ ).toEqual('disabled');
+ expect(
+ wrapper
+ .findAll('button')
+ .at(1)
+ .attributes('disabled'),
+ ).toEqual('disabled');
+ });
+
+ it('renders loading spinner', () => {
+ expect(wrapper.find(GlLoadingIcon).isVisible()).toBe(true);
+ });
+ });
+
+ describe('with error', () => {
+ beforeEach(() => {
+ mountComponent(Object.assign({}, data, { hasError: true }));
+ });
+
+ it('does not render the buttons', () => {
+ expect(wrapper.findAll('button').exists()).toBe(false);
+ });
+
+ it('renders title message provided', () => {
+ expect(findErrorMessage().text()).toBe(data.title);
+ });
+ });
+});
diff --git a/spec/frontend/vue_mr_widget/stores/artifacts_list/getters_spec.js b/spec/frontend/vue_mr_widget/stores/artifacts_list/getters_spec.js
new file mode 100644
index 00000000000..62ee6f5f189
--- /dev/null
+++ b/spec/frontend/vue_mr_widget/stores/artifacts_list/getters_spec.js
@@ -0,0 +1,32 @@
+import { title } from '~/vue_merge_request_widget/stores/artifacts_list/getters';
+import state from '~/vue_merge_request_widget/stores/artifacts_list/state';
+import { artifactsList } from '../../components/mock_data';
+
+describe('Artifacts Store Getters', () => {
+ let localState;
+
+ beforeEach(() => {
+ localState = state();
+ });
+
+ describe('title', () => {
+ describe('when is loading', () => {
+ it('returns loading message', () => {
+ localState.isLoading = true;
+ expect(title(localState)).toBe('Loading artifacts');
+ });
+ });
+ describe('when has error', () => {
+ it('returns error message', () => {
+ localState.hasError = true;
+ expect(title(localState)).toBe('An error occurred while fetching the artifacts');
+ });
+ });
+ describe('when it has artifacts', () => {
+ it('returns artifacts message', () => {
+ localState.artifacts = artifactsList;
+ expect(title(localState)).toBe('View 2 exposed artifacts');
+ });
+ });
+ });
+});
diff --git a/spec/frontend/vue_mr_widget/stores/artifacts_list/mutations_spec.js b/spec/frontend/vue_mr_widget/stores/artifacts_list/mutations_spec.js
new file mode 100644
index 00000000000..ea89fdb72e9
--- /dev/null
+++ b/spec/frontend/vue_mr_widget/stores/artifacts_list/mutations_spec.js
@@ -0,0 +1,78 @@
+import state from '~/vue_merge_request_widget/stores/artifacts_list/state';
+import mutations from '~/vue_merge_request_widget/stores/artifacts_list/mutations';
+import * as types from '~/vue_merge_request_widget/stores/artifacts_list/mutation_types';
+
+describe('Artifacts Store Mutations', () => {
+ let stateCopy;
+
+ beforeEach(() => {
+ stateCopy = state();
+ });
+
+ describe('SET_ENDPOINT', () => {
+ it('should set endpoint', () => {
+ mutations[types.SET_ENDPOINT](stateCopy, 'endpoint.json');
+
+ expect(stateCopy.endpoint).toEqual('endpoint.json');
+ });
+ });
+
+ describe('REQUEST_ARTIFACTS', () => {
+ it('should set isLoading to true', () => {
+ mutations[types.REQUEST_ARTIFACTS](stateCopy);
+
+ expect(stateCopy.isLoading).toEqual(true);
+ });
+ });
+
+ describe('REECEIVE_ARTIFACTS_SUCCESS', () => {
+ const artifacts = [
+ {
+ text: 'result.txt',
+ url: 'asda',
+ job_name: 'generate-artifact',
+ job_path: 'asda',
+ },
+ {
+ text: 'file.txt',
+ url: 'asda',
+ job_name: 'generate-artifact',
+ job_path: 'asda',
+ },
+ ];
+
+ beforeEach(() => {
+ mutations[types.RECEIVE_ARTIFACTS_SUCCESS](stateCopy, artifacts);
+ });
+
+ it('should set isLoading to false', () => {
+ expect(stateCopy.isLoading).toEqual(false);
+ });
+
+ it('should set hasError to false', () => {
+ expect(stateCopy.hasError).toEqual(false);
+ });
+
+ it('should set list of artifacts', () => {
+ expect(stateCopy.artifacts).toEqual(artifacts);
+ });
+ });
+
+ describe('RECEIVE_ARTIFACTS_ERROR', () => {
+ beforeEach(() => {
+ mutations[types.RECEIVE_ARTIFACTS_ERROR](stateCopy);
+ });
+
+ it('should set isLoading to false', () => {
+ expect(stateCopy.isLoading).toEqual(false);
+ });
+
+ it('should set hasError to true', () => {
+ expect(stateCopy.hasError).toEqual(true);
+ });
+
+ it('should set list of artifacts as empty array', () => {
+ expect(stateCopy.artifacts).toEqual([]);
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/changed_file_icon_spec.js b/spec/frontend/vue_shared/components/changed_file_icon_spec.js
index d0586f9e63f..d5861b18318 100644
--- a/spec/frontend/vue_shared/components/changed_file_icon_spec.js
+++ b/spec/frontend/vue_shared/components/changed_file_icon_spec.js
@@ -28,10 +28,7 @@ describe('Changed file icon', () => {
const findIcon = () => wrapper.find(Icon);
const findIconName = () => findIcon().props('name');
- const findIconClasses = () =>
- findIcon()
- .props('cssClasses')
- .split(' ');
+ const findIconClasses = () => findIcon().classes();
const findTooltipText = () => wrapper.attributes('data-original-title');
it('with isCentered true, adds center class', () => {
diff --git a/spec/frontend/vue_shared/components/file_icon_spec.js b/spec/frontend/vue_shared/components/file_icon_spec.js
index 328eec0a80a..f8f68a6a77a 100644
--- a/spec/frontend/vue_shared/components/file_icon_spec.js
+++ b/spec/frontend/vue_shared/components/file_icon_spec.js
@@ -49,7 +49,7 @@ describe('File Icon component', () => {
});
expect(findIcon().exists()).toBe(false);
- expect(wrapper.find(Icon).props('cssClasses')).toContain('folder-icon');
+ expect(wrapper.find(Icon).classes()).toContain('folder-icon');
});
it('should render a loading icon', () => {
diff --git a/spec/frontend/vue_shared/components/modal_copy_button_spec.js b/spec/frontend/vue_shared/components/modal_copy_button_spec.js
index f1943861523..d8c55bee8e0 100644
--- a/spec/frontend/vue_shared/components/modal_copy_button_spec.js
+++ b/spec/frontend/vue_shared/components/modal_copy_button_spec.js
@@ -14,7 +14,7 @@ describe('modal copy button', () => {
wrapper = shallowMount(Component, {
propsData: {
text: 'copy me',
- title: 'Copy this value into Clipboard!',
+ title: 'Copy this value',
},
});
});
diff --git a/spec/frontend/vue_shared/components/recaptcha_eventhub_spec.js b/spec/frontend/vue_shared/components/recaptcha_eventhub_spec.js
new file mode 100644
index 00000000000..d86d627886f
--- /dev/null
+++ b/spec/frontend/vue_shared/components/recaptcha_eventhub_spec.js
@@ -0,0 +1,21 @@
+import { eventHub, callbackName } from '~/vue_shared/components/recaptcha_eventhub';
+
+describe('reCAPTCHA event hub', () => {
+ // the following test case currently crashes
+ // see https://gitlab.com/gitlab-org/gitlab/issues/29192#note_217840035
+ // eslint-disable-next-line jest/no-disabled-tests
+ it.skip('throws an error for overriding the callback', () => {
+ expect(() => {
+ window[callbackName] = 'something';
+ }).toThrow();
+ });
+
+ it('triggering callback emits a submit event', () => {
+ const eventHandler = jest.fn();
+ eventHub.$once('submit', eventHandler);
+
+ window[callbackName]();
+
+ expect(eventHandler).toHaveBeenCalled();
+ });
+});
diff --git a/spec/frontend/vue_shared/components/recaptcha_modal_spec.js b/spec/frontend/vue_shared/components/recaptcha_modal_spec.js
new file mode 100644
index 00000000000..e509fe09d94
--- /dev/null
+++ b/spec/frontend/vue_shared/components/recaptcha_modal_spec.js
@@ -0,0 +1,36 @@
+import { shallowMount } from '@vue/test-utils';
+
+import { eventHub } from '~/vue_shared/components/recaptcha_eventhub';
+
+import RecaptchaModal from '~/vue_shared/components/recaptcha_modal';
+
+describe('RecaptchaModal', () => {
+ const recaptchaFormId = 'recaptcha-form';
+ const recaptchaHtml = `<form id="${recaptchaFormId}"></form>`;
+
+ let wrapper;
+
+ const findRecaptchaForm = () => wrapper.find(`#${recaptchaFormId}`).element;
+
+ beforeEach(() => {
+ wrapper = shallowMount(RecaptchaModal, {
+ sync: false,
+ propsData: {
+ html: recaptchaHtml,
+ },
+ });
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('submits the form if event hub emits submit event', () => {
+ const form = findRecaptchaForm();
+ jest.spyOn(form, 'submit').mockImplementation();
+
+ eventHub.$emit('submit');
+
+ expect(form.submit).toHaveBeenCalled();
+ });
+});
diff --git a/spec/frontend/vue_shared/directives/track_event_spec.js b/spec/frontend/vue_shared/directives/track_event_spec.js
new file mode 100644
index 00000000000..d63f6ae05b4
--- /dev/null
+++ b/spec/frontend/vue_shared/directives/track_event_spec.js
@@ -0,0 +1,49 @@
+import Vue from 'vue';
+import { createLocalVue, shallowMount } from '@vue/test-utils';
+import Tracking from '~/tracking';
+import TrackEvent from '~/vue_shared/directives/track_event';
+
+jest.mock('~/tracking');
+
+const Component = Vue.component('dummy-element', {
+ directives: {
+ TrackEvent,
+ },
+ data() {
+ return {
+ trackingOptions: null,
+ };
+ },
+ template: '<button id="trackable" v-track-event="trackingOptions"></button>',
+});
+
+const localVue = createLocalVue();
+let wrapper;
+let button;
+
+describe('Error Tracking directive', () => {
+ beforeEach(() => {
+ wrapper = shallowMount(localVue.extend(Component), {
+ localVue,
+ });
+ button = wrapper.find('#trackable');
+ });
+
+ it('should not track the event if required arguments are not provided', () => {
+ button.trigger('click');
+ expect(Tracking.event).not.toHaveBeenCalled();
+ });
+
+ it('should track event on click if tracking info provided', () => {
+ const trackingOptions = {
+ category: 'Tracking',
+ action: 'click_trackable_btn',
+ label: 'Trackable Info',
+ };
+
+ wrapper.setData({ trackingOptions });
+ const { category, action, label, property, value } = trackingOptions;
+ button.trigger('click');
+ expect(Tracking.event).toHaveBeenCalledWith(category, action, { label, property, value });
+ });
+});
diff --git a/spec/frontend/vue_shared/gl_feature_flags_plugin_spec.js b/spec/frontend/vue_shared/gl_feature_flags_plugin_spec.js
new file mode 100644
index 00000000000..6ecc330b5af
--- /dev/null
+++ b/spec/frontend/vue_shared/gl_feature_flags_plugin_spec.js
@@ -0,0 +1,42 @@
+import { createLocalVue, shallowMount } from '@vue/test-utils';
+import GlFeatureFlags from '~/vue_shared/gl_feature_flags_plugin';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+
+const localVue = createLocalVue();
+
+describe('GitLab Feature Flags Plugin', () => {
+ beforeEach(() => {
+ window.gon = {
+ features: {
+ aFeature: true,
+ bFeature: false,
+ },
+ };
+
+ localVue.use(GlFeatureFlags);
+ });
+
+ it('should provide glFeatures to components', () => {
+ const component = {
+ template: `<span></span>`,
+ inject: ['glFeatures'],
+ };
+ const wrapper = shallowMount(component, { localVue });
+ expect(wrapper.vm.glFeatures).toEqual({
+ aFeature: true,
+ bFeature: false,
+ });
+ });
+
+ it('should integrate with the glFeatureMixin', () => {
+ const component = {
+ template: `<span></span>`,
+ mixins: [glFeatureFlagsMixin()],
+ };
+ const wrapper = shallowMount(component, { localVue });
+ expect(wrapper.vm.glFeatures).toEqual({
+ aFeature: true,
+ bFeature: false,
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/mixins/gl_feature_flags_mixin_spec.js b/spec/frontend/vue_shared/mixins/gl_feature_flags_mixin_spec.js
new file mode 100644
index 00000000000..a3e3270a4e8
--- /dev/null
+++ b/spec/frontend/vue_shared/mixins/gl_feature_flags_mixin_spec.js
@@ -0,0 +1,36 @@
+import { createLocalVue, shallowMount } from '@vue/test-utils';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+
+const localVue = createLocalVue();
+
+describe('GitLab Feature Flags Mixin', () => {
+ let wrapper;
+
+ beforeEach(() => {
+ const gon = {
+ features: {
+ aFeature: true,
+ bFeature: false,
+ },
+ };
+
+ const component = {
+ template: `<span></span>`,
+ mixins: [glFeatureFlagsMixin()],
+ };
+
+ wrapper = shallowMount(component, {
+ localVue,
+ provide: {
+ glFeatures: { ...(gon.features || {}) },
+ },
+ });
+ });
+
+ it('should provide glFeatures to components', () => {
+ expect(wrapper.vm.glFeatures).toEqual({
+ aFeature: true,
+ bFeature: false,
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/plugins/global_toast_spec.js b/spec/frontend/vue_shared/plugins/global_toast_spec.js
index 551abe3cb41..89f43a5e556 100644
--- a/spec/frontend/vue_shared/plugins/global_toast_spec.js
+++ b/spec/frontend/vue_shared/plugins/global_toast_spec.js
@@ -1,24 +1,24 @@
-import toast from '~/vue_shared/plugins/global_toast';
import Vue from 'vue';
+import toast from '~/vue_shared/plugins/global_toast';
describe('Global toast', () => {
let spyFunc;
beforeEach(() => {
- spyFunc = jest.spyOn(Vue.toasted, 'show').mockImplementation(() => {});
+ spyFunc = jest.spyOn(Vue.prototype.$toast, 'show').mockImplementation(() => {});
});
afterEach(() => {
spyFunc.mockRestore();
});
- it('should pass all args to Vue toasted', () => {
+ it("should call GitLab UI's toast method", () => {
const arg1 = 'TestMessage';
const arg2 = { className: 'foo' };
toast(arg1, arg2);
- expect(Vue.toasted.show).toHaveBeenCalledTimes(1);
- expect(Vue.toasted.show).toHaveBeenCalledWith(arg1, arg2);
+ expect(Vue.prototype.$toast.show).toHaveBeenCalledTimes(1);
+ expect(Vue.prototype.$toast.show).toHaveBeenCalledWith(arg1, arg2);
});
});
diff --git a/spec/graphql/mutations/concerns/mutations/resolves_group_spec.rb b/spec/graphql/mutations/concerns/mutations/resolves_group_spec.rb
new file mode 100644
index 00000000000..897b8f4e9ef
--- /dev/null
+++ b/spec/graphql/mutations/concerns/mutations/resolves_group_spec.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Mutations::ResolvesGroup do
+ let(:mutation_class) do
+ Class.new(Mutations::BaseMutation) do
+ include Mutations::ResolvesGroup
+ end
+ end
+
+ let(:context) { double }
+ subject(:mutation) { mutation_class.new(object: nil, context: context) }
+
+ it 'uses the GroupsResolver to resolve groups by path' do
+ group = create(:group)
+
+ expect(Resolvers::GroupResolver).to receive(:new).with(object: nil, context: context).and_call_original
+ expect(mutation.resolve_group(full_path: group.full_path).sync).to eq(group)
+ end
+end
diff --git a/spec/graphql/mutations/concerns/mutations/resolves_project_spec.rb b/spec/graphql/mutations/concerns/mutations/resolves_project_spec.rb
index aa0f5c55902..09d1f66a2c7 100644
--- a/spec/graphql/mutations/concerns/mutations/resolves_project_spec.rb
+++ b/spec/graphql/mutations/concerns/mutations/resolves_project_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Mutations::ResolvesProject do
diff --git a/spec/graphql/mutations/merge_requests/set_wip_spec.rb b/spec/graphql/mutations/merge_requests/set_wip_spec.rb
index e600abf3941..c4accab9e46 100644
--- a/spec/graphql/mutations/merge_requests/set_wip_spec.rb
+++ b/spec/graphql/mutations/merge_requests/set_wip_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Mutations::MergeRequests::SetWip do
diff --git a/spec/graphql/resolvers/concerns/resolves_pipelines_spec.rb b/spec/graphql/resolvers/concerns/resolves_pipelines_spec.rb
index 3140af27af5..fa031af4013 100644
--- a/spec/graphql/resolvers/concerns/resolves_pipelines_spec.rb
+++ b/spec/graphql/resolvers/concerns/resolves_pipelines_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe ResolvesPipelines do
diff --git a/spec/graphql/resolvers/issues_resolver_spec.rb b/spec/graphql/resolvers/issues_resolver_spec.rb
index d122c081069..2232c9b7d7b 100644
--- a/spec/graphql/resolvers/issues_resolver_spec.rb
+++ b/spec/graphql/resolvers/issues_resolver_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Resolvers::IssuesResolver do
diff --git a/spec/graphql/resolvers/last_commit_resolver_spec.rb b/spec/graphql/resolvers/last_commit_resolver_spec.rb
new file mode 100644
index 00000000000..15b09b77a10
--- /dev/null
+++ b/spec/graphql/resolvers/last_commit_resolver_spec.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Resolvers::LastCommitResolver do
+ include GraphqlHelpers
+
+ let(:repository) { create(:project, :repository).repository }
+ let(:tree) { repository.tree(ref, path) }
+
+ let(:commit) { resolve(described_class, obj: tree) }
+
+ describe '#resolve' do
+ context 'last commit is a merge commit' do
+ let(:ref) { 'master' }
+ let(:path) { '/' }
+
+ it 'resolves to the merge commit' do
+ expect(commit).to eq(repository.commits(ref, limit: 1).last)
+ end
+ end
+
+ context 'last commit for a different branch and path' do
+ let(:ref) { 'fix' }
+ let(:path) { 'files' }
+
+ it 'resolves commit' do
+ expect(commit).to eq(repository.commits(ref, path: path, limit: 1).last)
+ end
+ end
+
+ context 'last commit does not exist' do
+ let(:ref) { 'master' }
+ let(:path) { 'does-not-exist' }
+
+ it 'returns nil' do
+ expect(commit).to be_nil
+ end
+ end
+ end
+end
diff --git a/spec/graphql/resolvers/merge_request_pipelines_resolver_spec.rb b/spec/graphql/resolvers/merge_request_pipelines_resolver_spec.rb
index 09b17bf6fc9..b8bdfc36ba7 100644
--- a/spec/graphql/resolvers/merge_request_pipelines_resolver_spec.rb
+++ b/spec/graphql/resolvers/merge_request_pipelines_resolver_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Resolvers::MergeRequestPipelinesResolver do
diff --git a/spec/graphql/resolvers/merge_requests_resolver_spec.rb b/spec/graphql/resolvers/merge_requests_resolver_spec.rb
index 97b8e5ed41c..fe167a6ae3e 100644
--- a/spec/graphql/resolvers/merge_requests_resolver_spec.rb
+++ b/spec/graphql/resolvers/merge_requests_resolver_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Resolvers::MergeRequestsResolver do
diff --git a/spec/graphql/resolvers/metadata_resolver_spec.rb b/spec/graphql/resolvers/metadata_resolver_spec.rb
index e662ed127a5..afff9eabb97 100644
--- a/spec/graphql/resolvers/metadata_resolver_spec.rb
+++ b/spec/graphql/resolvers/metadata_resolver_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Resolvers::MetadataResolver do
diff --git a/spec/graphql/resolvers/project_pipelines_resolver_spec.rb b/spec/graphql/resolvers/project_pipelines_resolver_spec.rb
index 6862ae8a5ed..f312a118c96 100644
--- a/spec/graphql/resolvers/project_pipelines_resolver_spec.rb
+++ b/spec/graphql/resolvers/project_pipelines_resolver_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Resolvers::ProjectPipelinesResolver do
diff --git a/spec/graphql/resolvers/project_resolver_spec.rb b/spec/graphql/resolvers/project_resolver_spec.rb
index d0fc2957909..860f8b4abb8 100644
--- a/spec/graphql/resolvers/project_resolver_spec.rb
+++ b/spec/graphql/resolvers/project_resolver_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Resolvers::ProjectResolver do
diff --git a/spec/graphql/resolvers/todo_resolver_spec.rb b/spec/graphql/resolvers/todo_resolver_spec.rb
new file mode 100644
index 00000000000..fef761d7243
--- /dev/null
+++ b/spec/graphql/resolvers/todo_resolver_spec.rb
@@ -0,0 +1,113 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Resolvers::TodoResolver do
+ include GraphqlHelpers
+
+ describe '#resolve' do
+ let_it_be(:current_user) { create(:user) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:author1) { create(:user) }
+ let_it_be(:author2) { create(:user) }
+
+ let_it_be(:todo1) { create(:todo, user: user, target_type: 'MergeRequest', state: :pending, action: Todo::MENTIONED, author: author1) }
+ let_it_be(:todo2) { create(:todo, user: user, state: :done, action: Todo::ASSIGNED, author: author2) }
+ let_it_be(:todo3) { create(:todo, user: user, state: :pending, action: Todo::ASSIGNED, author: author1) }
+
+ it 'calls TodosFinder' do
+ expect_next_instance_of(TodosFinder) do |finder|
+ expect(finder).to receive(:execute)
+ end
+
+ resolve_todos
+ end
+
+ context 'when using no filter' do
+ it 'returns expected todos' do
+ todos = resolve(described_class, obj: user, args: {}, ctx: { current_user: user })
+
+ expect(todos).to contain_exactly(todo1, todo3)
+ end
+ end
+
+ context 'when using filters' do
+ # TODO These can be removed as soon as we support filtering for multiple field contents for todos
+
+ it 'just uses the first state' do
+ todos = resolve(described_class, obj: user, args: { state: [:done, :pending] }, ctx: { current_user: user })
+
+ expect(todos).to contain_exactly(todo2)
+ end
+
+ it 'just uses the first action' do
+ todos = resolve(described_class, obj: user, args: { action: [Todo::MENTIONED, Todo::ASSIGNED] }, ctx: { current_user: user })
+
+ expect(todos).to contain_exactly(todo1)
+ end
+
+ it 'just uses the first author id' do
+ # We need a pending todo for now because of TodosFinder's state query
+ todo4 = create(:todo, user: user, state: :pending, action: Todo::ASSIGNED, author: author2)
+
+ todos = resolve(described_class, obj: user, args: { author_id: [author2.id, author1.id] }, ctx: { current_user: user })
+
+ expect(todos).to contain_exactly(todo4)
+ end
+
+ it 'just uses the first project id' do
+ project1 = create(:project)
+ project2 = create(:project)
+
+ create(:todo, project: project1, user: user, state: :pending, action: Todo::ASSIGNED, author: author1)
+ todo5 = create(:todo, project: project2, user: user, state: :pending, action: Todo::ASSIGNED, author: author1)
+
+ todos = resolve(described_class, obj: user, args: { project_id: [project2.id, project1.id] }, ctx: { current_user: user })
+
+ expect(todos).to contain_exactly(todo5)
+ end
+
+ it 'just uses the first group id' do
+ group1 = create(:group)
+ group2 = create(:group)
+
+ group1.add_developer(user)
+ group2.add_developer(user)
+
+ create(:todo, group: group1, user: user, state: :pending, action: Todo::ASSIGNED, author: author1)
+ todo5 = create(:todo, group: group2, user: user, state: :pending, action: Todo::ASSIGNED, author: author1)
+
+ todos = resolve(described_class, obj: user, args: { group_id: [group2.id, group1.id] }, ctx: { current_user: user })
+
+ expect(todos).to contain_exactly(todo5)
+ end
+
+ it 'just uses the first target' do
+ todos = resolve(described_class, obj: user, args: { type: %w[Issue MergeRequest] }, ctx: { current_user: user })
+
+ # Just todo3 because todo2 is in state "done"
+ expect(todos).to contain_exactly(todo3)
+ end
+ end
+
+ context 'when no user is provided' do
+ it 'returns no todos' do
+ todos = resolve(described_class, obj: nil, args: {}, ctx: { current_user: current_user })
+
+ expect(todos).to be_empty
+ end
+ end
+
+ context 'when provided user is not current user' do
+ it 'returns no todos' do
+ todos = resolve(described_class, obj: user, args: {}, ctx: { current_user: current_user })
+
+ expect(todos).to be_empty
+ end
+ end
+ end
+
+ def resolve_todos(args = {}, context = { current_user: current_user })
+ resolve(described_class, obj: current_user, args: args, ctx: context)
+ end
+end
diff --git a/spec/graphql/resolvers/tree_resolver_spec.rb b/spec/graphql/resolvers/tree_resolver_spec.rb
index 9f95b740ab1..0ea4e6eeaad 100644
--- a/spec/graphql/resolvers/tree_resolver_spec.rb
+++ b/spec/graphql/resolvers/tree_resolver_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Resolvers::TreeResolver do
diff --git a/spec/graphql/types/ci/detailed_status_type_spec.rb b/spec/graphql/types/ci/detailed_status_type_spec.rb
index a21162adb42..169a03c770b 100644
--- a/spec/graphql/types/ci/detailed_status_type_spec.rb
+++ b/spec/graphql/types/ci/detailed_status_type_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Types::Ci::DetailedStatusType do
diff --git a/spec/graphql/types/ci/pipeline_type_spec.rb b/spec/graphql/types/ci/pipeline_type_spec.rb
index ec1c689a4be..2fafc1bc13f 100644
--- a/spec/graphql/types/ci/pipeline_type_spec.rb
+++ b/spec/graphql/types/ci/pipeline_type_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Types::Ci::PipelineType do
diff --git a/spec/graphql/types/commit_type_spec.rb b/spec/graphql/types/commit_type_spec.rb
index 5d8edcf254c..1ff1c97f8db 100644
--- a/spec/graphql/types/commit_type_spec.rb
+++ b/spec/graphql/types/commit_type_spec.rb
@@ -7,5 +7,10 @@ describe GitlabSchema.types['Commit'] do
it { expect(described_class).to require_graphql_authorizations(:download_code) }
- it { expect(described_class).to have_graphql_fields(:id, :sha, :title, :description, :message, :authored_date, :author, :web_url, :latest_pipeline) }
+ it 'contains attributes related to commit' do
+ expect(described_class).to have_graphql_fields(
+ :id, :sha, :title, :description, :message, :authored_date,
+ :author, :web_url, :latest_pipeline, :signature_html
+ )
+ end
end
diff --git a/spec/graphql/types/extended_issue_type_spec.rb b/spec/graphql/types/extended_issue_type_spec.rb
new file mode 100644
index 00000000000..72ce53ae1be
--- /dev/null
+++ b/spec/graphql/types/extended_issue_type_spec.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe GitlabSchema.types['ExtendedIssue'] do
+ it { expect(described_class).to expose_permissions_using(Types::PermissionTypes::Issue) }
+
+ it { expect(described_class.graphql_name).to eq('ExtendedIssue') }
+
+ it { expect(described_class).to require_graphql_authorizations(:read_issue) }
+
+ it { expect(described_class.interfaces).to include(Types::Notes::NoteableType.to_graphql) }
+
+ it 'has specific fields' do
+ fields = Types::IssueType.fields.keys + [:subscribed]
+
+ fields.each do |field_name|
+ expect(described_class).to have_graphql_field(field_name)
+ end
+ end
+end
diff --git a/spec/graphql/types/issue_type_spec.rb b/spec/graphql/types/issue_type_spec.rb
index 734e5af3cd8..8aa2385ddaa 100644
--- a/spec/graphql/types/issue_type_spec.rb
+++ b/spec/graphql/types/issue_type_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe GitlabSchema.types['Issue'] do
@@ -10,8 +12,9 @@ describe GitlabSchema.types['Issue'] do
it { expect(described_class.interfaces).to include(Types::Notes::NoteableType.to_graphql) }
it 'has specific fields' do
- fields = %i[title_html description_html relative_position web_path web_url
- reference]
+ fields = %i[iid title description state reference author assignees participants labels milestone due_date
+ confidential discussion_locked upvotes downvotes user_notes_count web_path web_url relative_position
+ time_estimate total_time_spent closed_at created_at updated_at task_completion_status]
fields.each do |field_name|
expect(described_class).to have_graphql_field(field_name)
diff --git a/spec/graphql/types/merge_request_type_spec.rb b/spec/graphql/types/merge_request_type_spec.rb
index 59bd0123d88..04e9bb6861f 100644
--- a/spec/graphql/types/merge_request_type_spec.rb
+++ b/spec/graphql/types/merge_request_type_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe GitlabSchema.types['MergeRequest'] do
@@ -18,7 +20,9 @@ describe GitlabSchema.types['MergeRequest'] do
merge_error allow_collaboration should_be_rebased rebase_commit_sha
rebase_in_progress merge_commit_message default_merge_commit_message
merge_ongoing source_branch_exists mergeable_discussions_state web_url
- upvotes downvotes subscribed head_pipeline pipelines task_completion_status
+ upvotes downvotes head_pipeline pipelines task_completion_status
+ milestone assignees participants subscribed labels discussion_locked time_estimate
+ total_time_spent reference
]
is_expected.to have_graphql_fields(*expected_fields)
diff --git a/spec/graphql/types/metadata_type_spec.rb b/spec/graphql/types/metadata_type_spec.rb
index 5236380e477..2988f3c6ba5 100644
--- a/spec/graphql/types/metadata_type_spec.rb
+++ b/spec/graphql/types/metadata_type_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe GitlabSchema.types['Metadata'] do
diff --git a/spec/graphql/types/notes/diff_position_type_spec.rb b/spec/graphql/types/notes/diff_position_type_spec.rb
index 345bca8f702..aa08daaacd4 100644
--- a/spec/graphql/types/notes/diff_position_type_spec.rb
+++ b/spec/graphql/types/notes/diff_position_type_spec.rb
@@ -7,6 +7,6 @@ describe GitlabSchema.types['DiffPosition'] do
:new_path, :position_type, :old_line, :new_line, :x, :y,
:width, :height]
- is_expected.to have_graphql_field(*expected_fields)
+ is_expected.to have_graphql_fields(*expected_fields)
end
end
diff --git a/spec/graphql/types/permission_types/base_permission_type_spec.rb b/spec/graphql/types/permission_types/base_permission_type_spec.rb
index 0ee8b883d51..a45102e5b50 100644
--- a/spec/graphql/types/permission_types/base_permission_type_spec.rb
+++ b/spec/graphql/types/permission_types/base_permission_type_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Types::PermissionTypes::BasePermissionType do
diff --git a/spec/graphql/types/permission_types/issue_spec.rb b/spec/graphql/types/permission_types/issue_spec.rb
index f0fbeda202f..a94bc6b780e 100644
--- a/spec/graphql/types/permission_types/issue_spec.rb
+++ b/spec/graphql/types/permission_types/issue_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Types::PermissionTypes::Issue do
diff --git a/spec/graphql/types/permission_types/merge_request_spec.rb b/spec/graphql/types/permission_types/merge_request_spec.rb
index e1026b01a74..e0f8bdd4712 100644
--- a/spec/graphql/types/permission_types/merge_request_spec.rb
+++ b/spec/graphql/types/permission_types/merge_request_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Types::PermissionTypes::MergeRequest do
diff --git a/spec/graphql/types/permission_types/merge_request_type_spec.rb b/spec/graphql/types/permission_types/merge_request_type_spec.rb
index 6e57122867a..572b4ac42d0 100644
--- a/spec/graphql/types/permission_types/merge_request_type_spec.rb
+++ b/spec/graphql/types/permission_types/merge_request_type_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Types::MergeRequestType do
diff --git a/spec/graphql/types/permission_types/note_spec.rb b/spec/graphql/types/permission_types/note_spec.rb
index 32d56eb1f7a..a7811fc20e5 100644
--- a/spec/graphql/types/permission_types/note_spec.rb
+++ b/spec/graphql/types/permission_types/note_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe GitlabSchema.types['NotePermissions'] do
diff --git a/spec/graphql/types/permission_types/project_spec.rb b/spec/graphql/types/permission_types/project_spec.rb
index 4974995b587..6d5a905c128 100644
--- a/spec/graphql/types/permission_types/project_spec.rb
+++ b/spec/graphql/types/permission_types/project_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Types::PermissionTypes::Project do
diff --git a/spec/graphql/types/project_type_spec.rb b/spec/graphql/types/project_type_spec.rb
index 69fbc72bdf5..cfd0f8ec7a7 100644
--- a/spec/graphql/types/project_type_spec.rb
+++ b/spec/graphql/types/project_type_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe GitlabSchema.types['Project'] do
@@ -25,4 +27,40 @@ describe GitlabSchema.types['Project'] do
is_expected.to have_graphql_fields(*expected_fields)
end
+
+ describe 'issue field' do
+ subject { described_class.fields['issue'] }
+
+ it 'returns issue' do
+ is_expected.to have_graphql_type(Types::ExtendedIssueType)
+ is_expected.to have_graphql_resolver(Resolvers::IssuesResolver.single)
+ end
+ end
+
+ describe 'issues field' do
+ subject { described_class.fields['issues'] }
+
+ it 'returns issue' do
+ is_expected.to have_graphql_type(Types::IssueType.connection_type)
+ is_expected.to have_graphql_resolver(Resolvers::IssuesResolver)
+ end
+ end
+
+ describe 'merge_requests field' do
+ subject { described_class.fields['mergeRequest'] }
+
+ it 'returns merge requests' do
+ is_expected.to have_graphql_type(Types::MergeRequestType)
+ is_expected.to have_graphql_resolver(Resolvers::MergeRequestsResolver.single)
+ end
+ end
+
+ describe 'merge_request field' do
+ subject { described_class.fields['mergeRequests'] }
+
+ it 'returns merge request' do
+ is_expected.to have_graphql_type(Types::MergeRequestType.connection_type)
+ is_expected.to have_graphql_resolver(Resolvers::MergeRequestsResolver)
+ end
+ end
end
diff --git a/spec/graphql/types/query_type_spec.rb b/spec/graphql/types/query_type_spec.rb
index bc3b8a42392..1365bc0dc14 100644
--- a/spec/graphql/types/query_type_spec.rb
+++ b/spec/graphql/types/query_type_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe GitlabSchema.types['Query'] do
@@ -5,7 +7,7 @@ describe GitlabSchema.types['Query'] do
expect(described_class.graphql_name).to eq('Query')
end
- it { is_expected.to have_graphql_fields(:project, :namespace, :group, :echo, :metadata) }
+ it { is_expected.to have_graphql_fields(:project, :namespace, :group, :echo, :metadata, :current_user) }
describe 'namespace field' do
subject { described_class.fields['namespace'] }
diff --git a/spec/graphql/types/repository_type_spec.rb b/spec/graphql/types/repository_type_spec.rb
index 8a8238f2a2a..236f9bb9459 100644
--- a/spec/graphql/types/repository_type_spec.rb
+++ b/spec/graphql/types/repository_type_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe GitlabSchema.types['Repository'] do
diff --git a/spec/graphql/types/time_type_spec.rb b/spec/graphql/types/time_type_spec.rb
index 4196d9d27d4..88a535ed3bb 100644
--- a/spec/graphql/types/time_type_spec.rb
+++ b/spec/graphql/types/time_type_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe GitlabSchema.types['Time'] do
diff --git a/spec/graphql/types/todo_type_spec.rb b/spec/graphql/types/todo_type_spec.rb
new file mode 100644
index 00000000000..a5ea5bcffb0
--- /dev/null
+++ b/spec/graphql/types/todo_type_spec.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe GitlabSchema.types['Todo'] do
+ it 'has the correct fields' do
+ expected_fields = [:id, :project, :group, :author, :action, :target_type, :body, :state, :created_at]
+
+ is_expected.to have_graphql_fields(*expected_fields)
+ end
+
+ it { expect(described_class).to require_graphql_authorizations(:read_todo) }
+end
diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb
index 2f481e237a7..e8c438e459b 100644
--- a/spec/helpers/application_helper_spec.rb
+++ b/spec/helpers/application_helper_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe ApplicationHelper do
diff --git a/spec/helpers/auto_devops_helper_spec.rb b/spec/helpers/auto_devops_helper_spec.rb
index e80388f9ea7..5d42a80aae3 100644
--- a/spec/helpers/auto_devops_helper_spec.rb
+++ b/spec/helpers/auto_devops_helper_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe AutoDevopsHelper do
diff --git a/spec/helpers/avatars_helper_spec.rb b/spec/helpers/avatars_helper_spec.rb
index 9330e75af11..77182a59c1c 100644
--- a/spec/helpers/avatars_helper_spec.rb
+++ b/spec/helpers/avatars_helper_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe AvatarsHelper do
diff --git a/spec/helpers/award_emoji_helper_spec.rb b/spec/helpers/award_emoji_helper_spec.rb
index 035960ed96e..2ee27bc5427 100644
--- a/spec/helpers/award_emoji_helper_spec.rb
+++ b/spec/helpers/award_emoji_helper_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe AwardEmojiHelper do
diff --git a/spec/helpers/blame_helper_spec.rb b/spec/helpers/blame_helper_spec.rb
index 722d21c566f..8b5de040508 100644
--- a/spec/helpers/blame_helper_spec.rb
+++ b/spec/helpers/blame_helper_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe BlameHelper do
diff --git a/spec/helpers/blob_helper_spec.rb b/spec/helpers/blob_helper_spec.rb
index 1f236429347..4996e27c2e6 100644
--- a/spec/helpers/blob_helper_spec.rb
+++ b/spec/helpers/blob_helper_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe BlobHelper do
@@ -268,4 +270,32 @@ describe BlobHelper do
end
end
end
+
+ describe '#ide_fork_and_edit_path' do
+ let(:project) { create(:project) }
+ let(:current_user) { create(:user) }
+ let(:can_push_code) { true }
+
+ before do
+ allow(helper).to receive(:current_user).and_return(current_user)
+ allow(helper).to receive(:can?).and_return(can_push_code)
+ end
+
+ it 'returns path to fork the repo with a redirect param to the full IDE path' do
+ uri = URI(helper.ide_fork_and_edit_path(project, "master", ""))
+ params = CGI.unescape(uri.query)
+
+ expect(uri.path).to eq("/#{project.namespace.path}/#{project.path}/-/forks")
+ expect(params).to include("continue[to]=/-/ide/project/#{project.namespace.path}/#{project.path}/edit/master")
+ expect(params).to include("namespace_key=#{current_user.namespace.id}")
+ end
+
+ context 'when user is not logged in' do
+ let(:current_user) { nil }
+
+ it 'returns nil' do
+ expect(helper.ide_fork_and_edit_path(project, "master", "")).to be_nil
+ end
+ end
+ end
end
diff --git a/spec/helpers/boards_helper_spec.rb b/spec/helpers/boards_helper_spec.rb
index ad088398ce9..8a4446b7f59 100644
--- a/spec/helpers/boards_helper_spec.rb
+++ b/spec/helpers/boards_helper_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe BoardsHelper do
diff --git a/spec/helpers/broadcast_messages_helper_spec.rb b/spec/helpers/broadcast_messages_helper_spec.rb
index 9bec0f9f432..d0f0e6f1dd5 100644
--- a/spec/helpers/broadcast_messages_helper_spec.rb
+++ b/spec/helpers/broadcast_messages_helper_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe BroadcastMessagesHelper do
diff --git a/spec/helpers/button_helper_spec.rb b/spec/helpers/button_helper_spec.rb
index eebae1d7290..e918c34ffef 100644
--- a/spec/helpers/button_helper_spec.rb
+++ b/spec/helpers/button_helper_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe ButtonHelper do
@@ -164,7 +166,7 @@ describe ButtonHelper do
it 'shows copy to clipboard button with default configuration and no text set to copy' do
expect(element.attr('class')).to eq('btn btn-clipboard btn-transparent')
expect(element.attr('type')).to eq('button')
- expect(element.attr('aria-label')).to eq('Copy to clipboard')
+ expect(element.attr('aria-label')).to eq('Copy')
expect(element.attr('data-toggle')).to eq('tooltip')
expect(element.attr('data-placement')).to eq('bottom')
expect(element.attr('data-container')).to eq('body')
diff --git a/spec/helpers/calendar_helper_spec.rb b/spec/helpers/calendar_helper_spec.rb
index 828a9d9fea0..8dba6815e8d 100644
--- a/spec/helpers/calendar_helper_spec.rb
+++ b/spec/helpers/calendar_helper_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe CalendarHelper do
diff --git a/spec/helpers/ci_status_helper_spec.rb b/spec/helpers/ci_status_helper_spec.rb
index 4f665dc0514..80be119b069 100644
--- a/spec/helpers/ci_status_helper_spec.rb
+++ b/spec/helpers/ci_status_helper_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe CiStatusHelper do
diff --git a/spec/helpers/commits_helper_spec.rb b/spec/helpers/commits_helper_spec.rb
index 9e53bc05a48..cbc5566979b 100644
--- a/spec/helpers/commits_helper_spec.rb
+++ b/spec/helpers/commits_helper_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe CommitsHelper do
diff --git a/spec/helpers/components_helper_spec.rb b/spec/helpers/components_helper_spec.rb
index 94a59193be8..703bee0ca92 100644
--- a/spec/helpers/components_helper_spec.rb
+++ b/spec/helpers/components_helper_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe ComponentsHelper do
diff --git a/spec/helpers/dashboard_helper_spec.rb b/spec/helpers/dashboard_helper_spec.rb
index 059ae128d93..c899c2d9853 100644
--- a/spec/helpers/dashboard_helper_spec.rb
+++ b/spec/helpers/dashboard_helper_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe DashboardHelper do
diff --git a/spec/helpers/defer_script_tag_helper_spec.rb b/spec/helpers/defer_script_tag_helper_spec.rb
index 9ada3ae75ba..440904188ca 100644
--- a/spec/helpers/defer_script_tag_helper_spec.rb
+++ b/spec/helpers/defer_script_tag_helper_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe DeferScriptTagHelper do
diff --git a/spec/helpers/diff_helper_spec.rb b/spec/helpers/diff_helper_spec.rb
index 5396243f44d..47c076e3322 100644
--- a/spec/helpers/diff_helper_spec.rb
+++ b/spec/helpers/diff_helper_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe DiffHelper do
diff --git a/spec/helpers/emails_helper_spec.rb b/spec/helpers/emails_helper_spec.rb
index a14ae2cde4b..931b7008173 100644
--- a/spec/helpers/emails_helper_spec.rb
+++ b/spec/helpers/emails_helper_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe EmailsHelper do
diff --git a/spec/helpers/environment_helper_spec.rb b/spec/helpers/environment_helper_spec.rb
new file mode 100644
index 00000000000..53953d72b06
--- /dev/null
+++ b/spec/helpers/environment_helper_spec.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe EnvironmentHelper do
+ describe '#render_deployment_status' do
+ context 'when using a manual deployment' do
+ it 'renders a span tag' do
+ deploy = build(:deployment, deployable: nil, status: :success)
+ html = helper.render_deployment_status(deploy)
+
+ expect(html).to have_css('span.ci-status.ci-success')
+ end
+ end
+
+ context 'when using a deployment from a build' do
+ it 'renders a link tag' do
+ deploy = build(:deployment, status: :success)
+ html = helper.render_deployment_status(deploy)
+
+ expect(html).to have_css('a.ci-status.ci-success')
+ end
+ end
+ end
+end
diff --git a/spec/helpers/events_helper_spec.rb b/spec/helpers/events_helper_spec.rb
index e062c841717..7853617c3ed 100644
--- a/spec/helpers/events_helper_spec.rb
+++ b/spec/helpers/events_helper_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe EventsHelper do
diff --git a/spec/helpers/explore_helper_spec.rb b/spec/helpers/explore_helper_spec.rb
index 12651d80e36..5208d3bd656 100644
--- a/spec/helpers/explore_helper_spec.rb
+++ b/spec/helpers/explore_helper_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe ExploreHelper do
diff --git a/spec/helpers/form_helper_spec.rb b/spec/helpers/form_helper_spec.rb
index a70d8333f30..68aa0137cd5 100644
--- a/spec/helpers/form_helper_spec.rb
+++ b/spec/helpers/form_helper_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe FormHelper do
diff --git a/spec/helpers/git_helper_spec.rb b/spec/helpers/git_helper_spec.rb
index 9b1ef1e05a2..505d6ed15ac 100644
--- a/spec/helpers/git_helper_spec.rb
+++ b/spec/helpers/git_helper_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe GitHelper do
diff --git a/spec/helpers/gitlab_routing_helper_spec.rb b/spec/helpers/gitlab_routing_helper_spec.rb
index 027480143bd..bf043f3f013 100644
--- a/spec/helpers/gitlab_routing_helper_spec.rb
+++ b/spec/helpers/gitlab_routing_helper_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe GitlabRoutingHelper do
diff --git a/spec/helpers/graph_helper_spec.rb b/spec/helpers/graph_helper_spec.rb
index 1f8a38dc697..dc389c09e60 100644
--- a/spec/helpers/graph_helper_spec.rb
+++ b/spec/helpers/graph_helper_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe GraphHelper do
diff --git a/spec/helpers/groups_helper_spec.rb b/spec/helpers/groups_helper_spec.rb
index 98719697cea..8b33277ea18 100644
--- a/spec/helpers/groups_helper_spec.rb
+++ b/spec/helpers/groups_helper_spec.rb
@@ -191,6 +191,41 @@ describe GroupsHelper do
end
end
+ describe '#group_container_registry_nav' do
+ let(:group) { create(:group, :public) }
+ let(:user) { create(:user) }
+ before do
+ stub_container_registry_config(enabled: true)
+ allow(helper).to receive(:current_user) { user }
+ allow(helper).to receive(:can?).with(user, :read_container_image, group) { true }
+ helper.instance_variable_set(:@group, group)
+ end
+
+ subject { helper.group_container_registry_nav? }
+
+ context 'when container registry is enabled' do
+ it { is_expected.to be_truthy }
+
+ it 'is disabled for guest' do
+ allow(helper).to receive(:can?).with(user, :read_container_image, group) { false }
+ expect(subject).to be false
+ end
+ end
+
+ context 'when container registry is not enabled' do
+ before do
+ stub_container_registry_config(enabled: false)
+ end
+
+ it { is_expected.to be_falsy }
+
+ it 'is disabled for guests' do
+ allow(helper).to receive(:can?).with(user, :read_container_image, group) { false }
+ expect(subject).to be false
+ end
+ end
+ end
+
describe '#group_sidebar_links' do
let(:group) { create(:group, :public) }
let(:user) { create(:user) }
diff --git a/spec/helpers/hooks_helper_spec.rb b/spec/helpers/hooks_helper_spec.rb
index 2e21f1134b1..4352089c1c0 100644
--- a/spec/helpers/hooks_helper_spec.rb
+++ b/spec/helpers/hooks_helper_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe HooksHelper do
diff --git a/spec/helpers/icons_helper_spec.rb b/spec/helpers/icons_helper_spec.rb
index 950f951e22e..f1b1d411e05 100644
--- a/spec/helpers/icons_helper_spec.rb
+++ b/spec/helpers/icons_helper_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe IconsHelper do
diff --git a/spec/helpers/import_helper_spec.rb b/spec/helpers/import_helper_spec.rb
index 8a1b1e859b1..a6b283e49dc 100644
--- a/spec/helpers/import_helper_spec.rb
+++ b/spec/helpers/import_helper_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe ImportHelper do
diff --git a/spec/helpers/instance_configuration_helper_spec.rb b/spec/helpers/instance_configuration_helper_spec.rb
index 5d716b9191d..31a6c7bc839 100644
--- a/spec/helpers/instance_configuration_helper_spec.rb
+++ b/spec/helpers/instance_configuration_helper_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe InstanceConfigurationHelper do
diff --git a/spec/helpers/issuables_helper_spec.rb b/spec/helpers/issuables_helper_spec.rb
index 3c8179460ac..2f67ea457a0 100644
--- a/spec/helpers/issuables_helper_spec.rb
+++ b/spec/helpers/issuables_helper_spec.rb
@@ -190,7 +190,6 @@ describe IssuablesHelper do
issuableRef: "##{issue.iid}",
markdownPreviewPath: "/#{@project.full_path}/preview_markdown",
markdownDocsPath: '/help/user/markdown',
- issuableTemplates: [],
lockVersion: issue.lock_version,
projectPath: @project.path,
projectNamespace: @project.namespace.path,
@@ -244,4 +243,32 @@ describe IssuablesHelper do
end
end
end
+
+ describe '#assignee_sidebar_data' do
+ let(:user) { create(:user) }
+ let(:merge_request) { nil }
+ subject { helper.assignee_sidebar_data(user, merge_request: merge_request) }
+
+ it 'returns hash of assignee data' do
+ is_expected.to eql({
+ avatar_url: user.avatar_url,
+ name: user.name,
+ username: user.username
+ })
+ end
+
+ context 'with merge_request' do
+ let(:merge_request) { build_stubbed(:merge_request) }
+
+ where(can_merge: [true, false])
+
+ with_them do
+ before do
+ allow(merge_request).to receive(:can_be_merged_by?).and_return(can_merge)
+ end
+
+ it { is_expected.to include({ can_merge: can_merge })}
+ end
+ end
+ end
end
diff --git a/spec/helpers/issues_helper_spec.rb b/spec/helpers/issues_helper_spec.rb
index d15b5a4ab58..a394812f8f0 100644
--- a/spec/helpers/issues_helper_spec.rb
+++ b/spec/helpers/issues_helper_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "spec_helper"
describe IssuesHelper do
diff --git a/spec/helpers/labels_helper_spec.rb b/spec/helpers/labels_helper_spec.rb
index 1d57aaa0da5..a715390ecae 100644
--- a/spec/helpers/labels_helper_spec.rb
+++ b/spec/helpers/labels_helper_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe LabelsHelper do
diff --git a/spec/helpers/markup_helper_spec.rb b/spec/helpers/markup_helper_spec.rb
index f6e1720e113..364f215420a 100644
--- a/spec/helpers/markup_helper_spec.rb
+++ b/spec/helpers/markup_helper_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe MarkupHelper do
@@ -408,12 +410,12 @@ describe MarkupHelper do
it 'preserves a link href when link text is truncated' do
text = 'The quick brown fox jumped over the lazy dog' # 44 chars
- input = "#{text}#{text}#{text} " # 133 chars
link_url = 'http://example.com/foo/bar/baz' # 30 chars
- input << link_url
- object = create_object(input)
+ input = "#{text}#{text}#{text} #{link_url}" # 163 chars
expected_link_text = 'http://example...</a>'
+ object = create_object(input)
+
expect(first_line_in_markdown(object, attribute, 150, project: project)).to match(link_url)
expect(first_line_in_markdown(object, attribute, 150, project: project)).to match(expected_link_text)
end
diff --git a/spec/helpers/members_helper_spec.rb b/spec/helpers/members_helper_spec.rb
index 908e8960f37..169c8707bf4 100644
--- a/spec/helpers/members_helper_spec.rb
+++ b/spec/helpers/members_helper_spec.rb
@@ -1,13 +1,15 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe MembersHelper do
describe '#remove_member_message' do
let(:requester) { create(:user) }
- let(:project) { create(:project, :public, :access_requestable) }
+ let(:project) { create(:project, :public) }
let(:project_member) { build(:project_member, project: project) }
let(:project_member_invite) { build(:project_member, project: project).tap { |m| m.generate_invite_token! } }
let(:project_member_request) { project.request_access(requester) }
- let(:group) { create(:group, :access_requestable) }
+ let(:group) { create(:group) }
let(:group_member) { build(:group_member, group: group) }
let(:group_member_invite) { build(:group_member, group: group).tap { |m| m.generate_invite_token! } }
let(:group_member_request) { group.request_access(requester) }
@@ -24,10 +26,10 @@ describe MembersHelper do
describe '#remove_member_title' do
let(:requester) { create(:user) }
- let(:project) { create(:project, :public, :access_requestable) }
+ let(:project) { create(:project, :public) }
let(:project_member) { build(:project_member, project: project) }
let(:project_member_request) { project.request_access(requester) }
- let(:group) { create(:group, :access_requestable) }
+ let(:group) { create(:group) }
let(:group_member) { build(:group_member, group: group) }
let(:group_member_request) { group.request_access(requester) }
diff --git a/spec/helpers/merge_requests_helper_spec.rb b/spec/helpers/merge_requests_helper_spec.rb
index 193390d2f2c..695d1520897 100644
--- a/spec/helpers/merge_requests_helper_spec.rb
+++ b/spec/helpers/merge_requests_helper_spec.rb
@@ -39,6 +39,7 @@ describe MergeRequestsHelper do
let(:forked_project) { fork_project(project) }
let(:merge_request) { create(:merge_request, source_project: forked_project, target_project: project) }
subject { format_mr_branch_names(merge_request) }
+
let(:source_title) { "#{forked_project.full_path}:#{merge_request.source_branch}" }
let(:target_title) { "#{project.full_path}:#{merge_request.target_branch}" }
diff --git a/spec/helpers/milestones_helper_spec.rb b/spec/helpers/milestones_helper_spec.rb
index f5185cb2857..3574066e03e 100644
--- a/spec/helpers/milestones_helper_spec.rb
+++ b/spec/helpers/milestones_helper_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe MilestonesHelper do
diff --git a/spec/helpers/milestones_routing_helper_spec.rb b/spec/helpers/milestones_routing_helper_spec.rb
index dc13a43c2ab..4da589a5007 100644
--- a/spec/helpers/milestones_routing_helper_spec.rb
+++ b/spec/helpers/milestones_routing_helper_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe MilestonesRoutingHelper do
diff --git a/spec/helpers/namespaces_helper_spec.rb b/spec/helpers/namespaces_helper_spec.rb
index e38513f6d94..36465069311 100644
--- a/spec/helpers/namespaces_helper_spec.rb
+++ b/spec/helpers/namespaces_helper_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe NamespacesHelper do
diff --git a/spec/helpers/nav_helper_spec.rb b/spec/helpers/nav_helper_spec.rb
index 979d89812f5..882a125a0da 100644
--- a/spec/helpers/nav_helper_spec.rb
+++ b/spec/helpers/nav_helper_spec.rb
@@ -1,16 +1,22 @@
+# frozen_string_literal: true
+
require 'spec_helper'
-describe NavHelper do
+describe NavHelper, :do_not_mock_admin_mode do
describe '#header_links' do
+ include_context 'custom session'
+
before do
- allow(helper).to receive(:session) { {} }
+ allow(helper).to receive(:session).and_return(session)
end
context 'when the user is logged in' do
- let(:user) { build(:user) }
+ let(:user) { create(:user) }
+ let(:current_user_mode) { Gitlab::Auth::CurrentUserMode.new(user) }
before do
allow(helper).to receive(:current_user).and_return(user)
+ allow(helper).to receive(:current_user_mode).and_return(current_user_mode)
allow(helper).to receive(:can?) { true }
end
@@ -26,6 +32,46 @@ describe NavHelper do
expect(helper.header_links).to include(:admin_impersonation)
end
+ context 'as admin' do
+ let(:user) { create(:user, :admin) }
+
+ context 'feature flag :user_mode_in_session is enabled' do
+ it 'does not contain the admin mode link by default' do
+ expect(helper.header_links).not_to include(:admin_mode)
+ end
+
+ context 'with admin mode enabled' do
+ before do
+ current_user_mode.enable_admin_mode!(password: user.password)
+ end
+
+ it 'contains the admin mode link' do
+ expect(helper.header_links).to include(:admin_mode)
+ end
+ end
+ end
+
+ context 'feature flag :user_mode_in_session is disabled' do
+ before do
+ stub_feature_flags(user_mode_in_session: false)
+ end
+
+ it 'does not contain the admin mode link' do
+ expect(helper.header_links).not_to include(:admin_mode)
+ end
+
+ context 'with admin mode enabled' do
+ before do
+ current_user_mode.enable_admin_mode!(password: user.password)
+ end
+
+ it 'has no effect on header links' do
+ expect(helper.header_links).not_to include(:admin_mode)
+ end
+ end
+ end
+ end
+
context 'when the user cannot read cross project' do
before do
allow(helper).to receive(:can?).with(user, :read_cross_project) { false }
diff --git a/spec/helpers/notes_helper_spec.rb b/spec/helpers/notes_helper_spec.rb
index 0715f34dafe..2da7717ebfc 100644
--- a/spec/helpers/notes_helper_spec.rb
+++ b/spec/helpers/notes_helper_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "spec_helper"
describe NotesHelper do
diff --git a/spec/helpers/notifications_helper_spec.rb b/spec/helpers/notifications_helper_spec.rb
index 5717b15d656..2384c87b377 100644
--- a/spec/helpers/notifications_helper_spec.rb
+++ b/spec/helpers/notifications_helper_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe NotificationsHelper do
diff --git a/spec/helpers/page_layout_helper_spec.rb b/spec/helpers/page_layout_helper_spec.rb
index 3b08fc511a3..7e851a1af01 100644
--- a/spec/helpers/page_layout_helper_spec.rb
+++ b/spec/helpers/page_layout_helper_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe PageLayoutHelper do
diff --git a/spec/helpers/pagination_helper_spec.rb b/spec/helpers/pagination_helper_spec.rb
index e235475fb47..9fb51249edc 100644
--- a/spec/helpers/pagination_helper_spec.rb
+++ b/spec/helpers/pagination_helper_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe PaginationHelper do
diff --git a/spec/helpers/preferences_helper_spec.rb b/spec/helpers/preferences_helper_spec.rb
index 554c08add2d..c4ed99e56a0 100644
--- a/spec/helpers/preferences_helper_spec.rb
+++ b/spec/helpers/preferences_helper_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe PreferencesHelper do
diff --git a/spec/helpers/profiles_helper_spec.rb b/spec/helpers/profiles_helper_spec.rb
index da2dc229c35..fc282eee26d 100644
--- a/spec/helpers/profiles_helper_spec.rb
+++ b/spec/helpers/profiles_helper_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe ProfilesHelper do
diff --git a/spec/helpers/projects_helper_spec.rb b/spec/helpers/projects_helper_spec.rb
index d2a4ce6540d..1fa3c639603 100644
--- a/spec/helpers/projects_helper_spec.rb
+++ b/spec/helpers/projects_helper_spec.rb
@@ -902,4 +902,40 @@ describe ProjectsHelper do
end
end
end
+
+ describe '#grafana_integration_url' do
+ let(:project) { create(:project) }
+
+ before do
+ helper.instance_variable_set(:@project, project)
+ end
+
+ subject { helper.grafana_integration_url }
+
+ it { is_expected.to eq(nil) }
+
+ context 'grafana integration exists' do
+ let!(:grafana_integration) { create(:grafana_integration, project: project) }
+
+ it { is_expected.to eq(grafana_integration.grafana_url) }
+ end
+ end
+
+ describe '#grafana_integration_token' do
+ let(:project) { create(:project) }
+
+ before do
+ helper.instance_variable_set(:@project, project)
+ end
+
+ subject { helper.grafana_integration_token }
+
+ it { is_expected.to eq(nil) }
+
+ context 'grafana integration exists' do
+ let!(:grafana_integration) { create(:grafana_integration, project: project) }
+
+ it { is_expected.to eq(grafana_integration.token) }
+ end
+ end
end
diff --git a/spec/helpers/releases_helper_spec.rb b/spec/helpers/releases_helper_spec.rb
index ff820b3cc95..3b4973677ef 100644
--- a/spec/helpers/releases_helper_spec.rb
+++ b/spec/helpers/releases_helper_spec.rb
@@ -22,23 +22,9 @@ describe ReleasesHelper do
helper.instance_variable_set(:@project, project)
end
- describe '#url_for_merge_requests' do
- it 'returns the the correct link with the correct parameters' do
- path = "#{project.group.path}/#{project.path}/merge_requests?scope=all&state=opened"
- expect(helper.url_for_merge_requests).to include(path)
- end
- end
-
- describe '#url_for_issues' do
- it 'returns the the correct link with the correct parameters' do
- path = "#{project.group.path}/#{project.path}/issues?scope=all&state=opened"
- expect(helper.url_for_issues).to include(path)
- end
- end
-
describe '#data_for_releases_page' do
it 'has the needed data to display release blocks' do
- keys = %i(project_id illustration_path documentation_path merge_requests_url issues_url)
+ keys = %i(project_id illustration_path documentation_path)
expect(helper.data_for_releases_page.keys).to eq(keys)
end
end
diff --git a/spec/helpers/rss_helper_spec.rb b/spec/helpers/rss_helper_spec.rb
index a7f9bdf07e4..657f5fb42bc 100644
--- a/spec/helpers/rss_helper_spec.rb
+++ b/spec/helpers/rss_helper_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe RssHelper do
diff --git a/spec/helpers/runners_helper_spec.rb b/spec/helpers/runners_helper_spec.rb
index bf00841fcb6..042714d002e 100644
--- a/spec/helpers/runners_helper_spec.rb
+++ b/spec/helpers/runners_helper_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe RunnersHelper do
diff --git a/spec/helpers/search_helper_spec.rb b/spec/helpers/search_helper_spec.rb
index e1dc589236b..9e9f87b3407 100644
--- a/spec/helpers/search_helper_spec.rb
+++ b/spec/helpers/search_helper_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe SearchHelper do
@@ -103,19 +105,17 @@ describe SearchHelper do
using RSpec::Parameterized::TableSyntax
where(:scope, :label) do
+ 'blobs' | 'code result'
'commits' | 'commit'
'issues' | 'issue'
'merge_requests' | 'merge request'
'milestones' | 'milestone'
+ 'notes' | 'comment'
'projects' | 'project'
+ 'snippet_blobs' | 'snippet result'
'snippet_titles' | 'snippet'
'users' | 'user'
-
- 'blobs' | 'result'
- 'snippet_blobs' | 'result'
- 'wiki_blobs' | 'result'
-
- 'notes' | 'comment'
+ 'wiki_blobs' | 'wiki result'
end
with_them do
@@ -140,6 +140,15 @@ describe SearchHelper do
end
end
+ describe 'search_entries_empty_message' do
+ it 'returns the formatted entry message' do
+ message = search_entries_empty_message('projects', '<h1>foo</h1>')
+
+ expect(message).to eq("We couldn't find any projects matching <code>&lt;h1&gt;foo&lt;/h1&gt;</code>")
+ expect(message).to be_html_safe
+ end
+ end
+
describe 'search_filter_input_options' do
context 'project' do
before do
diff --git a/spec/helpers/sidekiq_helper_spec.rb b/spec/helpers/sidekiq_helper_spec.rb
index 117abc9c556..86e52419f9c 100644
--- a/spec/helpers/sidekiq_helper_spec.rb
+++ b/spec/helpers/sidekiq_helper_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe SidekiqHelper do
diff --git a/spec/helpers/snippets_helper_spec.rb b/spec/helpers/snippets_helper_spec.rb
index ce5e037f88d..66c8d576a4c 100644
--- a/spec/helpers/snippets_helper_spec.rb
+++ b/spec/helpers/snippets_helper_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe SnippetsHelper do
diff --git a/spec/helpers/storage_helper_spec.rb b/spec/helpers/storage_helper_spec.rb
index 4bd0fbb76ca..577e6e5caf0 100644
--- a/spec/helpers/storage_helper_spec.rb
+++ b/spec/helpers/storage_helper_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "spec_helper"
describe StorageHelper do
diff --git a/spec/helpers/submodule_helper_spec.rb b/spec/helpers/submodule_helper_spec.rb
index 465c76e5e6b..fcfce0eaf31 100644
--- a/spec/helpers/submodule_helper_spec.rb
+++ b/spec/helpers/submodule_helper_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe SubmoduleHelper do
diff --git a/spec/helpers/tab_helper_spec.rb b/spec/helpers/tab_helper_spec.rb
index 9abf63d4bd4..3a3935a2130 100644
--- a/spec/helpers/tab_helper_spec.rb
+++ b/spec/helpers/tab_helper_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe TabHelper do
diff --git a/spec/helpers/time_helper_spec.rb b/spec/helpers/time_helper_spec.rb
index 8bf378549fe..858d6d341f4 100644
--- a/spec/helpers/time_helper_spec.rb
+++ b/spec/helpers/time_helper_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe TimeHelper do
diff --git a/spec/helpers/todos_helper_spec.rb b/spec/helpers/todos_helper_spec.rb
index 63806ef91f3..7c73b990338 100644
--- a/spec/helpers/todos_helper_spec.rb
+++ b/spec/helpers/todos_helper_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "spec_helper"
describe TodosHelper do
diff --git a/spec/helpers/tree_helper_spec.rb b/spec/helpers/tree_helper_spec.rb
index 4a62e696cd9..c7cdb4ae45c 100644
--- a/spec/helpers/tree_helper_spec.rb
+++ b/spec/helpers/tree_helper_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe TreeHelper do
diff --git a/spec/helpers/user_callouts_helper_spec.rb b/spec/helpers/user_callouts_helper_spec.rb
index 8fa479a4474..547bf693e94 100644
--- a/spec/helpers/user_callouts_helper_spec.rb
+++ b/spec/helpers/user_callouts_helper_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "spec_helper"
describe UserCalloutsHelper do
diff --git a/spec/helpers/users_helper_spec.rb b/spec/helpers/users_helper_spec.rb
index 9165e91ddcd..59abe8c09e1 100644
--- a/spec/helpers/users_helper_spec.rb
+++ b/spec/helpers/users_helper_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe UsersHelper do
diff --git a/spec/helpers/version_check_helper_spec.rb b/spec/helpers/version_check_helper_spec.rb
index edc0d64d031..421ff21bfdb 100644
--- a/spec/helpers/version_check_helper_spec.rb
+++ b/spec/helpers/version_check_helper_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe VersionCheckHelper do
diff --git a/spec/helpers/visibility_level_helper_spec.rb b/spec/helpers/visibility_level_helper_spec.rb
index 2d276696208..1a176cfe965 100644
--- a/spec/helpers/visibility_level_helper_spec.rb
+++ b/spec/helpers/visibility_level_helper_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe VisibilityLevelHelper do
diff --git a/spec/helpers/wiki_helper_spec.rb b/spec/helpers/wiki_helper_spec.rb
index ee977e37ec1..bcc2bd71da1 100644
--- a/spec/helpers/wiki_helper_spec.rb
+++ b/spec/helpers/wiki_helper_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe WikiHelper do
diff --git a/spec/initializers/google_api_client_spec.rb b/spec/initializers/google_api_client_spec.rb
new file mode 100644
index 00000000000..44a1bc0836c
--- /dev/null
+++ b/spec/initializers/google_api_client_spec.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe './config/initializers/google_api_client.rb' do
+ subject { Google::Apis::ContainerV1beta1 }
+
+ it 'is needed' do |example|
+ is_expected.not_to be_const_defined(:CloudRunConfig),
+ <<-MSG.strip_heredoc
+ The google-api-client gem has been upgraded!
+ Remove:
+ #{example.example_group.description}
+ #{example.file_path}
+ MSG
+ end
+end
diff --git a/spec/initializers/lograge_spec.rb b/spec/initializers/lograge_spec.rb
index 24d366731a2..c2c1960eeab 100644
--- a/spec/initializers/lograge_spec.rb
+++ b/spec/initializers/lograge_spec.rb
@@ -34,5 +34,38 @@ describe 'lograge', type: :request do
subject
end
+
+ it 'logs cpu_s on supported platform' do
+ allow(Gitlab::Metrics::System).to receive(:thread_cpu_time)
+ .and_return(
+ 0.111222333,
+ 0.222333833
+ )
+
+ expect(Lograge.formatter).to receive(:call)
+ .with(a_hash_including(cpu_s: 0.1111115))
+ .and_call_original
+
+ expect(Lograge.logger).to receive(:send)
+ .with(anything, include('"cpu_s":0.1111115'))
+ .and_call_original
+
+ subject
+ end
+
+ it 'does not log cpu_s on unsupported platform' do
+ allow(Gitlab::Metrics::System).to receive(:thread_cpu_time)
+ .and_return(nil)
+
+ expect(Lograge.formatter).to receive(:call)
+ .with(hash_not_including(:cpu_s))
+ .and_call_original
+
+ expect(Lograge.logger).not_to receive(:send)
+ .with(anything, include('"cpu_s":'))
+ .and_call_original
+
+ subject
+ end
end
end
diff --git a/spec/javascripts/blob/balsamiq/balsamiq_viewer_spec.js b/spec/javascripts/blob/balsamiq/balsamiq_viewer_spec.js
index fd73fb4bfcc..d175c8ba853 100644
--- a/spec/javascripts/blob/balsamiq/balsamiq_viewer_spec.js
+++ b/spec/javascripts/blob/balsamiq/balsamiq_viewer_spec.js
@@ -1,8 +1,10 @@
import sqljs from 'sql.js';
+import axios from '~/lib/utils/axios_utils';
import BalsamiqViewer from '~/blob/balsamiq/balsamiq_viewer';
import ClassSpecHelper from '../../helpers/class_spec_helper';
describe('BalsamiqViewer', () => {
+ const mockArrayBuffer = new ArrayBuffer(10);
let balsamiqViewer;
let viewer;
@@ -19,44 +21,65 @@ describe('BalsamiqViewer', () => {
});
describe('loadFile', () => {
- let xhr;
- let loadFile;
+ let bv;
const endpoint = 'endpoint';
+ const requestSuccess = Promise.resolve({
+ data: mockArrayBuffer,
+ status: 200,
+ });
beforeEach(() => {
- xhr = jasmine.createSpyObj('xhr', ['open', 'send']);
+ viewer = {};
+ bv = new BalsamiqViewer(viewer);
+ });
- balsamiqViewer = jasmine.createSpyObj('balsamiqViewer', ['renderFile']);
+ it('should call `axios.get` on `endpoint` param with responseType set to `arraybuffer', () => {
+ spyOn(axios, 'get').and.returnValue(requestSuccess);
+ spyOn(bv, 'renderFile').and.stub();
- spyOn(window, 'XMLHttpRequest').and.returnValue(xhr);
+ bv.loadFile(endpoint);
- loadFile = BalsamiqViewer.prototype.loadFile.call(balsamiqViewer, endpoint);
+ expect(axios.get).toHaveBeenCalledWith(
+ endpoint,
+ jasmine.objectContaining({
+ responseType: 'arraybuffer',
+ }),
+ );
});
- it('should call .open', () => {
- expect(xhr.open).toHaveBeenCalledWith('GET', endpoint, true);
- });
+ it('should call `renderFile` on request success', done => {
+ spyOn(axios, 'get').and.returnValue(requestSuccess);
+ spyOn(bv, 'renderFile').and.callFake(() => {});
- it('should set .responseType', () => {
- expect(xhr.responseType).toBe('arraybuffer');
+ bv.loadFile(endpoint)
+ .then(() => {
+ expect(bv.renderFile).toHaveBeenCalledWith(mockArrayBuffer);
+ })
+ .then(done)
+ .catch(done.fail);
});
- it('should call .send', () => {
- expect(xhr.send).toHaveBeenCalled();
- });
+ it('should not call `renderFile` on request failure', done => {
+ spyOn(axios, 'get').and.returnValue(Promise.reject());
+ spyOn(bv, 'renderFile');
- it('should return a promise', () => {
- expect(loadFile).toEqual(jasmine.any(Promise));
+ bv.loadFile(endpoint)
+ .then(() => {
+ done.fail('Expected loadFile to throw error!');
+ })
+ .catch(() => {
+ expect(bv.renderFile).not.toHaveBeenCalled();
+ })
+ .then(done)
+ .catch(done.fail);
});
});
describe('renderFile', () => {
let container;
- let loadEvent;
let previews;
beforeEach(() => {
- loadEvent = { target: { response: {} } };
viewer = jasmine.createSpyObj('viewer', ['appendChild']);
previews = [document.createElement('ul'), document.createElement('ul')];
@@ -73,11 +96,11 @@ describe('BalsamiqViewer', () => {
container = containerElement;
});
- BalsamiqViewer.prototype.renderFile.call(balsamiqViewer, loadEvent);
+ BalsamiqViewer.prototype.renderFile.call(balsamiqViewer, mockArrayBuffer);
});
it('should call .initDatabase', () => {
- expect(balsamiqViewer.initDatabase).toHaveBeenCalledWith(loadEvent.target.response);
+ expect(balsamiqViewer.initDatabase).toHaveBeenCalledWith(mockArrayBuffer);
});
it('should call .getPreviews', () => {
diff --git a/spec/javascripts/blob/viewer/index_spec.js b/spec/javascripts/blob/viewer/index_spec.js
index 4ac15ca5aa2..06c06613887 100644
--- a/spec/javascripts/blob/viewer/index_spec.js
+++ b/spec/javascripts/blob/viewer/index_spec.js
@@ -101,7 +101,7 @@ describe('Blob viewer', () => {
it('has tooltip when disabled', () => {
expect(copyButton.getAttribute('data-original-title')).toBe(
- 'Switch to the source to copy it to the clipboard',
+ 'Switch to the source to copy the file contents',
);
});
@@ -136,7 +136,7 @@ describe('Blob viewer', () => {
document.querySelector('.js-blob-viewer-switch-btn[data-viewer="simple"]').click();
setTimeout(() => {
- expect(copyButton.getAttribute('data-original-title')).toBe('Copy source to clipboard');
+ expect(copyButton.getAttribute('data-original-title')).toBe('Copy file contents');
done();
});
diff --git a/spec/javascripts/boards/board_card_spec.js b/spec/javascripts/boards/board_card_spec.js
index 13b708a03d5..9f441ca319e 100644
--- a/spec/javascripts/boards/board_card_spec.js
+++ b/spec/javascripts/boards/board_card_spec.js
@@ -67,6 +67,16 @@ describe('Board card', () => {
expect(vm.issueDetailVisible).toBe(true);
});
+ it("returns false when multiSelect doesn't contain issue", () => {
+ expect(vm.multiSelectVisible).toBe(false);
+ });
+
+ it('returns true when multiSelect contains issue', () => {
+ boardsStore.multiSelect.list = [vm.issue];
+
+ expect(vm.multiSelectVisible).toBe(true);
+ });
+
it('adds user-can-drag class if not disabled', () => {
expect(vm.$el.classList.contains('user-can-drag')).toBe(true);
});
@@ -180,7 +190,7 @@ describe('Board card', () => {
triggerEvent('mousedown');
triggerEvent('mouseup');
- expect(eventHub.$emit).toHaveBeenCalledWith('newDetailIssue', vm.issue);
+ expect(eventHub.$emit).toHaveBeenCalledWith('newDetailIssue', vm.issue, undefined);
expect(boardsStore.detail.list).toEqual(vm.list);
});
@@ -203,7 +213,7 @@ describe('Board card', () => {
triggerEvent('mousedown');
triggerEvent('mouseup');
- expect(eventHub.$emit).toHaveBeenCalledWith('clearDetailIssue');
+ expect(eventHub.$emit).toHaveBeenCalledWith('clearDetailIssue', undefined);
});
});
});
diff --git a/spec/javascripts/boards/boards_store_spec.js b/spec/javascripts/boards/boards_store_spec.js
index 11352140ba4..678fe5befa8 100644
--- a/spec/javascripts/boards/boards_store_spec.js
+++ b/spec/javascripts/boards/boards_store_spec.js
@@ -12,6 +12,7 @@ import '~/boards/services/board_service';
import boardsStore from '~/boards/stores/boards_store';
import eventHub from '~/boards/eventhub';
import { listObj, listObjDuplicate, boardsMockInterceptor, mockBoardService } from './mock_data';
+import waitForPromises from '../../frontend/helpers/wait_for_promises';
describe('Store', () => {
let mock;
@@ -29,6 +30,13 @@ describe('Store', () => {
}),
);
+ spyOn(gl.boardService, 'moveMultipleIssues').and.callFake(
+ () =>
+ new Promise(resolve => {
+ resolve();
+ }),
+ );
+
Cookies.set('issue_board_welcome_hidden', 'false', {
expires: 365 * 10,
path: '',
@@ -376,4 +384,128 @@ describe('Store', () => {
expect(state.currentBoard).toEqual(dummyBoard);
});
});
+
+ describe('toggleMultiSelect', () => {
+ let basicIssueObj;
+
+ beforeAll(() => {
+ basicIssueObj = { id: 987654 };
+ });
+
+ afterEach(() => {
+ boardsStore.clearMultiSelect();
+ });
+
+ it('adds issue when not present', () => {
+ boardsStore.toggleMultiSelect(basicIssueObj);
+
+ const selectedIds = boardsStore.multiSelect.list.map(x => x.id);
+
+ expect(selectedIds.includes(basicIssueObj.id)).toEqual(true);
+ });
+
+ it('removes issue when issue is present', () => {
+ boardsStore.toggleMultiSelect(basicIssueObj);
+ let selectedIds = boardsStore.multiSelect.list.map(x => x.id);
+
+ expect(selectedIds.includes(basicIssueObj.id)).toEqual(true);
+
+ boardsStore.toggleMultiSelect(basicIssueObj);
+ selectedIds = boardsStore.multiSelect.list.map(x => x.id);
+
+ expect(selectedIds.includes(basicIssueObj.id)).toEqual(false);
+ });
+ });
+
+ describe('clearMultiSelect', () => {
+ it('clears all the multi selected issues', () => {
+ const issue1 = { id: 12345 };
+ const issue2 = { id: 12346 };
+
+ boardsStore.toggleMultiSelect(issue1);
+ boardsStore.toggleMultiSelect(issue2);
+
+ expect(boardsStore.multiSelect.list.length).toEqual(2);
+
+ boardsStore.clearMultiSelect();
+
+ expect(boardsStore.multiSelect.list.length).toEqual(0);
+ });
+ });
+
+ describe('moveMultipleIssuesToList', () => {
+ it('move issues on the new index', done => {
+ const listOne = boardsStore.addList(listObj);
+ const listTwo = boardsStore.addList(listObjDuplicate);
+
+ expect(boardsStore.state.lists.length).toBe(2);
+
+ setTimeout(() => {
+ expect(listOne.issues.length).toBe(1);
+ expect(listTwo.issues.length).toBe(1);
+
+ boardsStore.moveMultipleIssuesToList({
+ listFrom: listOne,
+ listTo: listTwo,
+ issues: listOne.issues,
+ newIndex: 0,
+ });
+
+ expect(listTwo.issues.length).toBe(1);
+
+ done();
+ }, 0);
+ });
+ });
+
+ describe('moveMultipleIssuesInList', () => {
+ it('moves multiple issues in list', done => {
+ const issueObj = {
+ title: 'Issue #1',
+ id: 12345,
+ iid: 2,
+ confidential: false,
+ labels: [],
+ assignees: [],
+ };
+ const issue1 = new ListIssue(issueObj);
+ const issue2 = new ListIssue({
+ ...issueObj,
+ title: 'Issue #2',
+ id: 12346,
+ });
+
+ const list = boardsStore.addList(listObj);
+
+ waitForPromises()
+ .then(() => {
+ list.addIssue(issue1);
+ list.addIssue(issue2);
+
+ expect(list.issues.length).toBe(3);
+ expect(list.issues[0].id).not.toBe(issue2.id);
+
+ boardsStore.moveMultipleIssuesInList({
+ list,
+ issues: [issue1, issue2],
+ oldIndicies: [0],
+ newIndex: 1,
+ idArray: [1, 12345, 12346],
+ });
+
+ expect(list.issues[0].id).toBe(issue1.id);
+
+ expect(gl.boardService.moveMultipleIssues).toHaveBeenCalledWith({
+ ids: [issue1.id, issue2.id],
+ fromListId: null,
+ toListId: null,
+ moveBeforeId: 1,
+ moveAfterId: null,
+ });
+
+ done();
+ })
+ .catch(done.fail);
+ });
+ });
});
diff --git a/spec/javascripts/boards/issue_card_spec.js b/spec/javascripts/boards/issue_card_spec.js
index 8a20911cc66..314e051665e 100644
--- a/spec/javascripts/boards/issue_card_spec.js
+++ b/spec/javascripts/boards/issue_card_spec.js
@@ -32,7 +32,10 @@ describe('Issue card component', () => {
beforeEach(() => {
setFixtures('<div class="test-container"></div>');
- list = listObj;
+ list = {
+ ...listObj,
+ type: 'label',
+ };
issue = new ListIssue({
title: 'Testing',
id: 1,
@@ -42,6 +45,7 @@ describe('Issue card component', () => {
assignees: [],
reference_path: '#1',
real_path: '/test/1',
+ weight: 1,
});
component = new Vue({
@@ -240,8 +244,8 @@ describe('Issue card component', () => {
Vue.nextTick(() => done());
});
- it('renders list label', () => {
- expect(component.$el.querySelectorAll('.badge').length).toBe(2);
+ it('does not render list label but renders all other labels', () => {
+ expect(component.$el.querySelectorAll('.badge').length).toBe(1);
});
it('renders label', () => {
@@ -277,7 +281,7 @@ describe('Issue card component', () => {
Vue.nextTick()
.then(() => {
- expect(component.$el.querySelectorAll('.badge').length).toBe(2);
+ expect(component.$el.querySelectorAll('.badge').length).toBe(1);
expect(component.$el.textContent).not.toContain('closed');
done();
@@ -285,10 +289,4 @@ describe('Issue card component', () => {
.catch(done.fail);
});
});
-
- describe('weights', () => {
- it('not shows weight component', () => {
- expect(component.$el.querySelector('.board-card-weight')).toBeNull();
- });
- });
});
diff --git a/spec/javascripts/boards/mock_data.js b/spec/javascripts/boards/mock_data.js
index 50ad1442873..41b8f567e08 100644
--- a/spec/javascripts/boards/mock_data.js
+++ b/spec/javascripts/boards/mock_data.js
@@ -15,7 +15,7 @@ export const listObj = {
weight: 3,
label: {
id: 5000,
- title: 'Testing',
+ title: 'Test',
color: 'red',
description: 'testing;',
textColor: 'white',
@@ -30,7 +30,7 @@ export const listObjDuplicate = {
weight: 3,
label: {
id: listObj.label.id,
- title: 'Testing',
+ title: 'Test',
color: 'red',
description: 'testing;',
},
diff --git a/spec/javascripts/commit/commit_pipeline_status_component_spec.js b/spec/javascripts/commit/commit_pipeline_status_component_spec.js
deleted file mode 100644
index f6b36e88a5f..00000000000
--- a/spec/javascripts/commit/commit_pipeline_status_component_spec.js
+++ /dev/null
@@ -1,106 +0,0 @@
-import Vue from 'vue';
-import MockAdapter from 'axios-mock-adapter';
-import axios from '~/lib/utils/axios_utils';
-import commitPipelineStatus from '~/projects/tree/components/commit_pipeline_status_component.vue';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
-
-describe('Commit pipeline status component', () => {
- let vm;
- let Component;
- let mock;
- const mockCiStatus = {
- details_path: '/root/hello-world/pipelines/1',
- favicon: 'canceled.ico',
- group: 'canceled',
- has_details: true,
- icon: 'status_canceled',
- label: 'canceled',
- text: 'canceled',
- };
-
- beforeEach(() => {
- Component = Vue.extend(commitPipelineStatus);
- });
-
- describe('While polling pipeline data successfully', () => {
- beforeEach(() => {
- mock = new MockAdapter(axios);
- mock.onGet('/dummy/endpoint').reply(() => {
- const res = Promise.resolve([
- 200,
- {
- pipelines: [
- {
- details: {
- status: mockCiStatus,
- },
- },
- ],
- },
- ]);
- return res;
- });
- vm = mountComponent(Component, {
- endpoint: '/dummy/endpoint',
- });
- });
-
- afterEach(() => {
- vm.poll.stop();
- vm.$destroy();
- mock.restore();
- });
-
- it('shows the loading icon when polling is starting', done => {
- expect(vm.$el.querySelector('.loading-container')).not.toBe(null);
- setTimeout(() => {
- expect(vm.$el.querySelector('.loading-container')).toBe(null);
- done();
- });
- });
-
- it('contains a ciStatus when the polling is successful ', done => {
- setTimeout(() => {
- expect(vm.ciStatus).toEqual(mockCiStatus);
- done();
- });
- });
-
- it('contains a ci-status icon when polling is successful', done => {
- setTimeout(() => {
- expect(vm.$el.querySelector('.ci-status-icon')).not.toBe(null);
- expect(vm.$el.querySelector('.ci-status-icon').classList).toContain(
- `ci-status-icon-${mockCiStatus.group}`,
- );
- done();
- });
- });
- });
-
- describe('When polling data was not successful', () => {
- beforeEach(() => {
- mock = new MockAdapter(axios);
- mock.onGet('/dummy/endpoint').reply(502, {});
- vm = new Component({
- props: {
- endpoint: '/dummy/endpoint',
- },
- });
- });
-
- afterEach(() => {
- vm.poll.stop();
- vm.$destroy();
- mock.restore();
- });
-
- it('calls an errorCallback', done => {
- spyOn(vm, 'errorCallback').and.callThrough();
- vm.$mount();
- setTimeout(() => {
- expect(vm.errorCallback.calls.count()).toEqual(1);
- done();
- });
- });
- });
-});
diff --git a/spec/javascripts/create_cluster/.eslintrc.yml b/spec/javascripts/create_cluster/.eslintrc.yml
new file mode 100644
index 00000000000..14e318a2f3e
--- /dev/null
+++ b/spec/javascripts/create_cluster/.eslintrc.yml
@@ -0,0 +1,3 @@
+rules:
+ # https://gitlab.com/gitlab-org/gitlab/issues/33025
+ promise/no-nesting: off
diff --git a/spec/javascripts/create_cluster/gke_cluster/components/gke_project_id_dropdown_spec.js b/spec/javascripts/create_cluster/gke_cluster/components/gke_project_id_dropdown_spec.js
index 809da3f9088..016ecfb35b8 100644
--- a/spec/javascripts/create_cluster/gke_cluster/components/gke_project_id_dropdown_spec.js
+++ b/spec/javascripts/create_cluster/gke_cluster/components/gke_project_id_dropdown_spec.js
@@ -4,6 +4,7 @@ import { createStore } from '~/create_cluster/gke_cluster/store';
import { SET_PROJECTS } from '~/create_cluster/gke_cluster/store/mutation_types';
import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import { emptyProjectMock, selectedProjectMock } from '../mock_data';
+import { gapi } from '../helpers';
const componentConfig = {
docsUrl: 'https://console.cloud.google.com/home/dashboard',
@@ -32,6 +33,16 @@ describe('GkeProjectIdDropdown', () => {
let vm;
let store;
+ let originalGapi;
+ beforeAll(() => {
+ originalGapi = window.gapi;
+ window.gapi = gapi();
+ });
+
+ afterAll(() => {
+ window.gapi = originalGapi;
+ });
+
beforeEach(() => {
store = createStore();
vm = createComponent(store);
diff --git a/spec/javascripts/create_cluster/gke_cluster/stores/actions_spec.js b/spec/javascripts/create_cluster/gke_cluster/stores/actions_spec.js
index a7591cc38c7..7ceaeace82f 100644
--- a/spec/javascripts/create_cluster/gke_cluster/stores/actions_spec.js
+++ b/spec/javascripts/create_cluster/gke_cluster/stores/actions_spec.js
@@ -64,7 +64,15 @@ describe('GCP Cluster Dropdown Store Actions', () => {
});
describe('async fetch methods', () => {
- window.gapi = gapi();
+ let originalGapi;
+ beforeAll(() => {
+ originalGapi = window.gapi;
+ window.gapi = gapi();
+ });
+
+ afterAll(() => {
+ window.gapi = originalGapi;
+ });
describe('fetchProjects', () => {
it('fetches projects from Google API', done => {
diff --git a/spec/javascripts/flash_spec.js b/spec/javascripts/flash_spec.js
index bd8608b6bac..28fa87ac097 100644
--- a/spec/javascripts/flash_spec.js
+++ b/spec/javascripts/flash_spec.js
@@ -176,7 +176,7 @@ describe('Flash', () => {
it('removes element after clicking', () => {
flash('test', 'alert', document, null, false, true);
- document.querySelector('.flash-alert').click();
+ document.querySelector('.flash-alert .js-close-icon').click();
expect(document.querySelector('.flash-alert')).toBeNull();
@@ -210,7 +210,13 @@ describe('Flash', () => {
describe('removeFlashClickListener', () => {
beforeEach(() => {
- document.body.innerHTML += '<div class="flash-container"><div class="flash"></div></div>';
+ document.body.innerHTML += `
+ <div class="flash-container">
+ <div class="flash">
+ <div class="close-icon js-close-icon"></div>
+ </div>
+ </div>
+ `;
});
it('removes global flash on click', done => {
@@ -218,7 +224,7 @@ describe('Flash', () => {
removeFlashClickListener(flashEl, false);
- flashEl.click();
+ flashEl.querySelector('.js-close-icon').click();
setTimeout(() => {
expect(document.querySelector('.flash')).toBeNull();
diff --git a/spec/javascripts/frequent_items/components/app_spec.js b/spec/javascripts/frequent_items/components/app_spec.js
index 6814f656f5d..36dd8604d08 100644
--- a/spec/javascripts/frequent_items/components/app_spec.js
+++ b/spec/javascripts/frequent_items/components/app_spec.js
@@ -236,8 +236,15 @@ describe('Frequent Items App Component', () => {
.then(() => {
expect(vm.$el.querySelector('.loading-animation')).toBeDefined();
})
+
+ // This test waits for multiple ticks in order to allow the responses to
+ // propagate through each interceptor installed on the Axios instance.
+ // This shouldn't be necessary; this test should be refactored to avoid this.
+ // https://gitlab.com/gitlab-org/gitlab/issues/32479
+ .then(vm.$nextTick)
.then(vm.$nextTick)
.then(vm.$nextTick)
+
.then(() => {
expect(vm.$el.querySelectorAll('.frequent-items-list-container li').length).toBe(
mockSearchedProjects.length,
diff --git a/spec/javascripts/header_spec.js b/spec/javascripts/header_spec.js
index 0ddf589f368..c36d3be1b22 100644
--- a/spec/javascripts/header_spec.js
+++ b/spec/javascripts/header_spec.js
@@ -20,26 +20,26 @@ describe('Header', function() {
});
it('should update todos-count after receiving the todo:toggle event', () => {
- triggerToggle('5');
+ triggerToggle(5);
expect($(todosPendingCount).text()).toEqual('5');
});
it('should hide todos-count when it is 0', () => {
- triggerToggle('0');
+ triggerToggle(0);
expect(isTodosCountHidden()).toEqual(true);
});
it('should show todos-count when it is more than 0', () => {
- triggerToggle('10');
+ triggerToggle(10);
expect(isTodosCountHidden()).toEqual(false);
});
describe('when todos-count is 1000', () => {
beforeEach(() => {
- triggerToggle('1000');
+ triggerToggle(1000);
});
it('should show todos-count', () => {
diff --git a/spec/javascripts/helpers/tracking_helper.js b/spec/javascripts/helpers/tracking_helper.js
new file mode 100644
index 00000000000..68c1bd2dbca
--- /dev/null
+++ b/spec/javascripts/helpers/tracking_helper.js
@@ -0,0 +1,25 @@
+import Tracking from '~/tracking';
+
+export default Tracking;
+
+let document;
+let handlers;
+
+export function mockTracking(category = '_category_', documentOverride, spyMethod) {
+ document = documentOverride || window.document;
+ window.snowplow = () => {};
+ Tracking.bindDocument(category, document);
+ return spyMethod ? spyMethod(Tracking, 'event') : null;
+}
+
+export function unmockTracking() {
+ window.snowplow = undefined;
+ handlers.forEach(event => document.removeEventListener(event.name, event.func));
+}
+
+export function triggerEvent(selectorOrEl, eventName = 'click') {
+ const event = new Event(eventName, { bubbles: true });
+ const el = typeof selectorOrEl === 'string' ? document.querySelector(selectorOrEl) : selectorOrEl;
+
+ el.dispatchEvent(event);
+}
diff --git a/spec/javascripts/helpers/vue_resource_helper.js b/spec/javascripts/helpers/vue_resource_helper.js
deleted file mode 100644
index 0f58af09933..00000000000
--- a/spec/javascripts/helpers/vue_resource_helper.js
+++ /dev/null
@@ -1,11 +0,0 @@
-// eslint-disable-next-line import/prefer-default-export
-export const headersInterceptor = (request, next) => {
- next(response => {
- const headers = {};
- response.headers.forEach((value, key) => {
- headers[key] = value;
- });
- // eslint-disable-next-line no-param-reassign
- response.headers = headers;
- });
-};
diff --git a/spec/javascripts/ide/components/branches/search_list_spec.js b/spec/javascripts/ide/components/branches/search_list_spec.js
deleted file mode 100644
index 72a3c2d5dcd..00000000000
--- a/spec/javascripts/ide/components/branches/search_list_spec.js
+++ /dev/null
@@ -1,80 +0,0 @@
-import Vue from 'vue';
-import store from '~/ide/stores';
-import * as types from '~/ide/stores/modules/branches/mutation_types';
-import List from '~/ide/components/branches/search_list.vue';
-import { createComponentWithStore } from '../../../helpers/vue_mount_component_helper';
-import { branches as testBranches } from '../../mock_data';
-import { resetStore } from '../../helpers';
-
-describe('IDE branches search list', () => {
- const Component = Vue.extend(List);
- let vm;
-
- beforeEach(() => {
- vm = createComponentWithStore(Component, store, {});
-
- spyOn(vm, 'fetchBranches');
-
- vm.$mount();
- });
-
- afterEach(() => {
- vm.$destroy();
-
- resetStore(store);
- });
-
- it('calls fetch on mounted', () => {
- expect(vm.fetchBranches).toHaveBeenCalledWith({
- search: '',
- });
- });
-
- it('renders loading icon', done => {
- vm.$store.state.branches.isLoading = true;
-
- vm.$nextTick()
- .then(() => {
- expect(vm.$el).toContainElement('.loading-container');
- })
- .then(done)
- .catch(done.fail);
- });
-
- it('renders branches not found when search is not empty', done => {
- vm.search = 'testing';
-
- vm.$nextTick(() => {
- expect(vm.$el).toContainText('No branches found');
-
- done();
- });
- });
-
- describe('with branches', () => {
- const currentBranch = testBranches[1];
-
- beforeEach(done => {
- vm.$store.state.currentBranchId = currentBranch.name;
- vm.$store.commit(`branches/${types.RECEIVE_BRANCHES_SUCCESS}`, testBranches);
-
- vm.$nextTick(done);
- });
-
- it('renders list', () => {
- const elementText = Array.from(vm.$el.querySelectorAll('li strong')).map(x =>
- x.textContent.trim(),
- );
-
- expect(elementText).toEqual(testBranches.map(x => x.name));
- });
-
- it('renders check next to active branch', () => {
- const checkedText = Array.from(vm.$el.querySelectorAll('li'))
- .filter(x => x.querySelector('.ide-search-list-current-icon svg'))
- .map(x => x.querySelector('strong').textContent.trim());
-
- expect(checkedText).toEqual([currentBranch.name]);
- });
- });
-});
diff --git a/spec/javascripts/ide/components/commit_sidebar/list_item_spec.js b/spec/javascripts/ide/components/commit_sidebar/list_item_spec.js
index bf48d7bfdad..c1dcd4928a0 100644
--- a/spec/javascripts/ide/components/commit_sidebar/list_item_spec.js
+++ b/spec/javascripts/ide/components/commit_sidebar/list_item_spec.js
@@ -2,12 +2,14 @@ import Vue from 'vue';
import store from '~/ide/stores';
import listItem from '~/ide/components/commit_sidebar/list_item.vue';
import router from '~/ide/ide_router';
+import { trimText } from 'spec/helpers/text_helper';
import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import { file, resetStore } from '../../helpers';
describe('Multi-file editor commit sidebar list item', () => {
let vm;
let f;
+ let findPathEl;
beforeEach(() => {
const Component = Vue.extend(listItem);
@@ -21,6 +23,8 @@ describe('Multi-file editor commit sidebar list item', () => {
actionComponent: 'stage-button',
activeFileKey: `staged-${f.key}`,
}).$mount();
+
+ findPathEl = vm.$el.querySelector('.multi-file-commit-list-path');
});
afterEach(() => {
@@ -29,15 +33,39 @@ describe('Multi-file editor commit sidebar list item', () => {
resetStore(store);
});
+ const findPathText = () => trimText(findPathEl.textContent);
+
it('renders file path', () => {
- expect(vm.$el.querySelector('.multi-file-commit-list-path').textContent).toContain(f.path);
+ expect(findPathText()).toContain(f.path);
+ });
+
+ it('correctly renders renamed entries', done => {
+ Vue.set(vm.file, 'prevName', 'Old name');
+
+ vm.$nextTick()
+ .then(() => {
+ expect(findPathText()).toEqual(`Old name → ${f.name}`);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('correctly renders entry, the name of which did not change after rename (as within a folder)', done => {
+ Vue.set(vm.file, 'prevName', f.name);
+
+ vm.$nextTick()
+ .then(() => {
+ expect(findPathText()).toEqual(f.name);
+ })
+ .then(done)
+ .catch(done.fail);
});
it('opens a closed file in the editor when clicking the file path', done => {
spyOn(vm, 'openPendingTab').and.callThrough();
spyOn(router, 'push');
- vm.$el.querySelector('.multi-file-commit-list-path').click();
+ findPathEl.click();
setTimeout(() => {
expect(vm.openPendingTab).toHaveBeenCalled();
@@ -52,7 +80,7 @@ describe('Multi-file editor commit sidebar list item', () => {
spyOn(vm, 'updateViewer').and.callThrough();
spyOn(router, 'push');
- vm.$el.querySelector('.multi-file-commit-list-path').click();
+ findPathEl.click();
setTimeout(() => {
expect(vm.updateViewer).toHaveBeenCalledWith('diff');
diff --git a/spec/javascripts/ide/components/error_message_spec.js b/spec/javascripts/ide/components/error_message_spec.js
deleted file mode 100644
index 80d6c7fd564..00000000000
--- a/spec/javascripts/ide/components/error_message_spec.js
+++ /dev/null
@@ -1,106 +0,0 @@
-import Vue from 'vue';
-import store from '~/ide/stores';
-import ErrorMessage from '~/ide/components/error_message.vue';
-import { createComponentWithStore } from '../../helpers/vue_mount_component_helper';
-import { resetStore } from '../helpers';
-
-describe('IDE error message component', () => {
- const Component = Vue.extend(ErrorMessage);
- let vm;
-
- beforeEach(() => {
- vm = createComponentWithStore(Component, store, {
- message: {
- text: 'error message',
- action: null,
- actionText: null,
- },
- }).$mount();
- });
-
- afterEach(() => {
- vm.$destroy();
- resetStore(vm.$store);
- });
-
- it('renders error message', () => {
- expect(vm.$el.textContent).toContain('error message');
- });
-
- it('clears error message on click', () => {
- spyOn(vm, 'setErrorMessage');
-
- vm.$el.click();
-
- expect(vm.setErrorMessage).toHaveBeenCalledWith(null);
- });
-
- describe('with action', () => {
- let actionSpy;
-
- beforeEach(done => {
- actionSpy = jasmine.createSpy('action').and.returnValue(Promise.resolve());
-
- vm.message.action = actionSpy;
- vm.message.actionText = 'test action';
- vm.message.actionPayload = 'testActionPayload';
-
- vm.$nextTick(done);
- });
-
- it('renders action button', () => {
- expect(vm.$el.querySelector('.flash-action')).not.toBe(null);
- expect(vm.$el.textContent).toContain('test action');
- });
-
- it('does not clear error message on click', () => {
- spyOn(vm, 'setErrorMessage');
-
- vm.$el.click();
-
- expect(vm.setErrorMessage).not.toHaveBeenCalled();
- });
-
- it('dispatches action', done => {
- vm.$el.querySelector('.flash-action').click();
-
- vm.$nextTick(() => {
- expect(actionSpy).toHaveBeenCalledWith('testActionPayload');
-
- done();
- });
- });
-
- it('does not dispatch action when already loading', () => {
- vm.isLoading = true;
-
- vm.$el.querySelector('.flash-action').click();
-
- expect(actionSpy).not.toHaveBeenCalledWith();
- });
-
- it('resets isLoading after click', done => {
- vm.$el.querySelector('.flash-action').click();
-
- expect(vm.isLoading).toBe(true);
-
- setTimeout(() => {
- expect(vm.isLoading).toBe(false);
-
- done();
- });
- });
-
- it('shows loading icon when isLoading is true', done => {
- expect(vm.$el.querySelector('.loading-container').style.display).not.toBe('');
-
- vm.isLoading = true;
-
- vm.$nextTick(() => {
- expect(vm.$el.querySelector('.loading-container').style.display).toBe('');
-
- done();
- });
- });
- });
-});
diff --git a/spec/javascripts/ide/components/file_row_extra_spec.js b/spec/javascripts/ide/components/file_row_extra_spec.js
index d7fed3f0681..86146fcef69 100644
--- a/spec/javascripts/ide/components/file_row_extra_spec.js
+++ b/spec/javascripts/ide/components/file_row_extra_spec.js
@@ -139,6 +139,27 @@ describe('IDE extra file row component', () => {
done();
});
});
+
+ it('shows when file is renamed', done => {
+ vm.file.prevPath = 'original-file';
+
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelector('.file-changed-icon')).not.toBe(null);
+
+ done();
+ });
+ });
+
+ it('hides when file is renamed', done => {
+ vm.file.prevPath = 'original-file';
+ vm.file.type = 'tree';
+
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelector('.file-changed-icon')).toBe(null);
+
+ done();
+ });
+ });
});
describe('merge request icon', () => {
diff --git a/spec/javascripts/ide/components/file_templates/dropdown_spec.js b/spec/javascripts/ide/components/file_templates/dropdown_spec.js
deleted file mode 100644
index 898796f4fa0..00000000000
--- a/spec/javascripts/ide/components/file_templates/dropdown_spec.js
+++ /dev/null
@@ -1,201 +0,0 @@
-import $ from 'jquery';
-import Vue from 'vue';
-import { createStore } from '~/ide/stores';
-import Dropdown from '~/ide/components/file_templates/dropdown.vue';
-import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
-import { resetStore } from '../../helpers';
-
-describe('IDE file templates dropdown component', () => {
- let Component;
- let vm;
-
- beforeAll(() => {
- Component = Vue.extend(Dropdown);
- });
-
- beforeEach(() => {
- const store = createStore();
-
- vm = createComponentWithStore(Component, store, {
- label: 'Test',
- }).$mount();
- });
-
- afterEach(() => {
- vm.$destroy();
- resetStore(vm.$store);
- });
-
- describe('async', () => {
- beforeEach(() => {
- vm.isAsyncData = true;
- });
-
- it('calls async store method on Bootstrap dropdown event', () => {
- spyOn(vm, 'fetchTemplateTypes').and.stub();
-
- $(vm.$el).trigger('show.bs.dropdown');
-
- expect(vm.fetchTemplateTypes).toHaveBeenCalled();
- });
-
- it('renders templates when async', done => {
- vm.$store.state.fileTemplates.templates = [
- {
- name: 'test',
- },
- ];
-
- vm.$nextTick(() => {
- expect(vm.$el.querySelector('.dropdown-content').textContent).toContain('test');
-
- done();
- });
- });
-
- it('renders loading icon when isLoading is true', done => {
- vm.$store.state.fileTemplates.isLoading = true;
-
- vm.$nextTick(() => {
- expect(vm.$el.querySelector('.loading-container')).not.toBe(null);
-
- done();
- });
- });
-
- it('searches template data', () => {
- vm.$store.state.fileTemplates.templates = [
- {
- name: 'test',
- },
- ];
- vm.searchable = true;
- vm.search = 'hello';
-
- expect(vm.outputData).toEqual([]);
- });
-
- it('does not filter data is searchable is false', () => {
- vm.$store.state.fileTemplates.templates = [
- {
- name: 'test',
- },
- ];
- vm.search = 'hello';
-
- expect(vm.outputData).toEqual([
- {
- name: 'test',
- },
- ]);
- });
-
- it('calls clickItem on click', done => {
- spyOn(vm, 'clickItem').and.stub();
-
- vm.$store.state.fileTemplates.templates = [
- {
- name: 'test',
- },
- ];
-
- vm.$nextTick(() => {
- vm.$el.querySelector('.dropdown-content button').click();
-
- expect(vm.clickItem).toHaveBeenCalledWith({
- name: 'test',
- });
-
- done();
- });
- });
-
- it('renders input when searchable is true', done => {
- vm.searchable = true;
-
- vm.$nextTick(() => {
- expect(vm.$el.querySelector('.dropdown-input')).not.toBe(null);
-
- done();
- });
- });
-
- it('does not render input when searchable is true & showLoading is true', done => {
- vm.searchable = true;
- vm.$store.state.fileTemplates.isLoading = true;
-
- vm.$nextTick(() => {
- expect(vm.$el.querySelector('.dropdown-input')).toBe(null);
-
- done();
- });
- });
- });
-
- describe('sync', () => {
- beforeEach(done => {
- vm.data = [
- {
- name: 'test sync',
- },
- ];
-
- vm.$nextTick(done);
- });
-
- it('renders props data', () => {
- expect(vm.$el.querySelector('.dropdown-content').textContent).toContain('test sync');
- });
-
- it('renders input when searchable is true', done => {
- vm.searchable = true;
-
- vm.$nextTick(() => {
- expect(vm.$el.querySelector('.dropdown-input')).not.toBe(null);
-
- done();
- });
- });
-
- it('calls clickItem on click', done => {
- spyOn(vm, 'clickItem').and.stub();
-
- vm.$nextTick(() => {
- vm.$el.querySelector('.dropdown-content button').click();
-
- expect(vm.clickItem).toHaveBeenCalledWith({
- name: 'test sync',
- });
-
- done();
- });
- });
-
- it('searches template data', () => {
- vm.searchable = true;
- vm.search = 'hello';
-
- expect(vm.outputData).toEqual([]);
- });
-
- it('does not filter data is searchable is false', () => {
- vm.search = 'hello';
-
- expect(vm.outputData).toEqual([
- {
- name: 'test sync',
- },
- ]);
- });
-
- it('renders dropdown title', done => {
- vm.title = 'Test title';
-
- vm.$nextTick(() => {
- expect(vm.$el.querySelector('.dropdown-title').textContent).toContain('Test title');
-
- done();
- });
- });
- });
-});
diff --git a/spec/javascripts/ide/components/ide_tree_list_spec.js b/spec/javascripts/ide/components/ide_tree_list_spec.js
index 554bd1ae3b5..f63007c7dd2 100644
--- a/spec/javascripts/ide/components/ide_tree_list_spec.js
+++ b/spec/javascripts/ide/components/ide_tree_list_spec.js
@@ -58,20 +58,6 @@ describe('IDE tree list', () => {
it('renders list of files', () => {
expect(vm.$el.textContent).toContain('fileName');
});
-
- it('does not render moved entries', done => {
- const tree = [file('moved entry'), file('normal entry')];
- tree[0].moved = true;
- store.state.trees['abcproject/master'].tree = tree;
- const container = vm.$el.querySelector('.ide-tree-body');
-
- vm.$nextTick(() => {
- expect(container.children.length).toBe(1);
- expect(vm.$el.textContent).not.toContain('moved entry');
- expect(vm.$el.textContent).toContain('normal entry');
- done();
- });
- });
});
describe('empty-branch state', () => {
diff --git a/spec/javascripts/ide/components/jobs/list_spec.js b/spec/javascripts/ide/components/jobs/list_spec.js
deleted file mode 100644
index b24853c56fa..00000000000
--- a/spec/javascripts/ide/components/jobs/list_spec.js
+++ /dev/null
@@ -1,67 +0,0 @@
-import Vue from 'vue';
-import StageList from '~/ide/components/jobs/list.vue';
-import { createStore } from '~/ide/stores';
-import { createComponentWithStore } from '../../../helpers/vue_mount_component_helper';
-import { stages, jobs } from '../../mock_data';
-
-describe('IDE stages list', () => {
- const Component = Vue.extend(StageList);
- let vm;
-
- beforeEach(() => {
- const store = createStore();
-
- vm = createComponentWithStore(Component, store, {
- stages: stages.map((mappedState, i) => ({
- ...mappedState,
- id: i,
- dropdownPath: mappedState.dropdown_path,
- jobs: [...jobs],
- isLoading: false,
- isCollapsed: false,
- })),
- loading: false,
- });
-
- spyOn(vm, 'fetchJobs');
- spyOn(vm, 'toggleStageCollapsed');
-
- vm.$mount();
- });
-
- afterEach(() => {
- vm.$destroy();
- });
-
- it('renders list of stages', () => {
- expect(vm.$el.querySelectorAll('.card').length).toBe(2);
- });
-
- it('renders loading icon when no stages & is loading', done => {
- vm.stages = [];
- vm.loading = true;
-
- vm.$nextTick(() => {
- expect(vm.$el.querySelector('.loading-container')).not.toBe(null);
-
- done();
- });
- });
-
- it('calls toggleStageCollapsed when clicking stage header', done => {
- vm.$el.querySelector('.card-header').click();
-
- vm.$nextTick(() => {
- expect(vm.toggleStageCollapsed).toHaveBeenCalledWith(0);
-
- done();
- });
- });
-
- it('calls fetchJobs when stage is mounted', () => {
- expect(vm.fetchJobs.calls.count()).toBe(stages.length);
-
- expect(vm.fetchJobs.calls.argsFor(0)).toEqual([vm.stages[0]]);
- expect(vm.fetchJobs.calls.argsFor(1)).toEqual([vm.stages[1]]);
- });
-});
diff --git a/spec/javascripts/ide/components/merge_requests/list_spec.js b/spec/javascripts/ide/components/merge_requests/list_spec.js
deleted file mode 100644
index 55e4f46d9ca..00000000000
--- a/spec/javascripts/ide/components/merge_requests/list_spec.js
+++ /dev/null
@@ -1,159 +0,0 @@
-import Vue from 'vue';
-import store from '~/ide/stores';
-import List from '~/ide/components/merge_requests/list.vue';
-import { createComponentWithStore } from '../../../helpers/vue_mount_component_helper';
-import { mergeRequests } from '../../mock_data';
-import { resetStore } from '../../helpers';
-
-describe('IDE merge requests list', () => {
- const Component = Vue.extend(List);
- let vm;
-
- beforeEach(() => {
- vm = createComponentWithStore(Component, store, {});
-
- spyOn(vm, 'fetchMergeRequests');
-
- vm.$mount();
- });
-
- afterEach(() => {
- vm.$destroy();
-
- resetStore(vm.$store);
- });
-
- it('calls fetch on mounted', () => {
- expect(vm.fetchMergeRequests).toHaveBeenCalledWith({
- search: '',
- type: '',
- });
- });
-
- it('renders loading icon', done => {
- vm.$store.state.mergeRequests.isLoading = true;
-
- vm.$nextTick(() => {
- expect(vm.$el.querySelector('.loading-container')).not.toBe(null);
-
- done();
- });
- });
-
- it('renders no search results text when search is not empty', done => {
- vm.search = 'testing';
-
- vm.$nextTick(() => {
- expect(vm.$el.textContent).toContain('No merge requests found');
-
- done();
- });
- });
-
- it('clicking on search type, sets currentSearchType and loads merge requests', done => {
- vm.onSearchFocus();
-
- vm.$nextTick()
- .then(() => {
- vm.$el.querySelector('li button').click();
-
- return vm.$nextTick();
- })
- .then(() => {
- expect(vm.currentSearchType).toEqual(vm.$options.searchTypes[0]);
- expect(vm.fetchMergeRequests).toHaveBeenCalledWith({
- type: vm.currentSearchType.type,
- search: '',
- });
- })
- .then(done)
- .catch(done.fail);
- });
-
- describe('with merge requests', () => {
- beforeEach(done => {
- vm.$store.state.mergeRequests.mergeRequests.push({
- ...mergeRequests[0],
- projectPathWithNamespace: 'gitlab-org/gitlab-ce',
- });
-
- vm.$nextTick(done);
- });
-
- it('renders list', () => {
- expect(vm.$el.querySelectorAll('li').length).toBe(1);
- expect(vm.$el.querySelector('li').textContent).toContain(mergeRequests[0].title);
- });
- });
-
- describe('searchMergeRequests', () => {
- beforeEach(() => {
- spyOn(vm, 'loadMergeRequests');
-
- jasmine.clock().install();
- });
-
- afterEach(() => {
- jasmine.clock().uninstall();
- });
-
- it('calls loadMergeRequests on input in search field', () => {
- const event = new Event('input');
-
- vm.$el.querySelector('input').dispatchEvent(event);
-
- jasmine.clock().tick(300);
-
- expect(vm.loadMergeRequests).toHaveBeenCalled();
- });
- });
-
- describe('onSearchFocus', () => {
- it('shows search types', done => {
- vm.$el.querySelector('input').dispatchEvent(new Event('focus'));
-
- expect(vm.hasSearchFocus).toBe(true);
- expect(vm.showSearchTypes).toBe(true);
-
- vm.$nextTick()
- .then(() => {
- const expectedSearchTypes = vm.$options.searchTypes.map(x => x.label);
- const renderedSearchTypes = Array.from(vm.$el.querySelectorAll('li')).map(x =>
- x.textContent.trim(),
- );
-
- expect(renderedSearchTypes).toEqual(expectedSearchTypes);
- })
- .then(done)
- .catch(done.fail);
- });
-
- it('does not show search types, if already has search value', () => {
- vm.search = 'lorem ipsum';
- vm.$el.querySelector('input').dispatchEvent(new Event('focus'));
-
- expect(vm.hasSearchFocus).toBe(true);
- expect(vm.showSearchTypes).toBe(false);
- });
-
- it('does not show search types, if already has a search type', () => {
- vm.currentSearchType = {};
- vm.$el.querySelector('input').dispatchEvent(new Event('focus'));
-
- expect(vm.hasSearchFocus).toBe(true);
- expect(vm.showSearchTypes).toBe(false);
- });
-
- it('resets hasSearchFocus when search changes', done => {
- vm.hasSearchFocus = true;
- vm.search = 'something else';
-
- vm.$nextTick()
- .then(() => {
- expect(vm.hasSearchFocus).toBe(false);
- })
- .then(done)
- .catch(done.fail);
- });
- });
-});
diff --git a/spec/javascripts/ide/components/pipelines/list_spec.js b/spec/javascripts/ide/components/pipelines/list_spec.js
deleted file mode 100644
index 80829f2358a..00000000000
--- a/spec/javascripts/ide/components/pipelines/list_spec.js
+++ /dev/null
@@ -1,137 +0,0 @@
-import Vue from 'vue';
-import MockAdapter from 'axios-mock-adapter';
-import axios from '~/lib/utils/axios_utils';
-import { createStore } from '~/ide/stores';
-import List from '~/ide/components/pipelines/list.vue';
-import { createComponentWithStore } from '../../../helpers/vue_mount_component_helper';
-import { pipelines, projectData, stages, jobs } from '../../mock_data';
-
-describe('IDE pipelines list', () => {
- const Component = Vue.extend(List);
- let vm;
- let mock;
-
- const findLoadingState = () => vm.$el.querySelector('.loading-container');
-
- beforeEach(done => {
- const store = createStore();
-
- mock = new MockAdapter(axios);
-
- store.state.currentProjectId = 'abc/def';
- store.state.currentBranchId = 'master';
- store.state.projects['abc/def'] = {
- ...projectData,
- path_with_namespace: 'abc/def',
- branches: {
- master: { commit: { id: '123' } },
- },
- };
- store.state.links = { ciHelpPagePath: gl.TEST_HOST };
- store.state.pipelinesEmptyStateSvgPath = gl.TEST_HOST;
- store.state.pipelines.stages = stages.map((mappedState, i) => ({
- ...mappedState,
- id: i,
- dropdownPath: mappedState.dropdown_path,
- jobs: [...jobs],
- isLoading: false,
- isCollapsed: false,
- }));
-
- mock
- .onGet('/abc/def/commit/123/pipelines')
- .replyOnce(200, { pipelines: [...pipelines] }, { 'poll-interval': '-1' });
-
- vm = createComponentWithStore(Component, store).$mount();
-
- setTimeout(done);
- });
-
- afterEach(done => {
- vm.$destroy();
- mock.restore();
-
- vm.$store
- .dispatch('pipelines/stopPipelinePolling')
- .then(() => vm.$store.dispatch('pipelines/clearEtagPoll'))
- .then(done)
- .catch(done.fail);
- });
-
- it('renders pipeline data', () => {
- expect(vm.$el.textContent).toContain('#1');
- });
-
- it('renders CI icon', () => {
- expect(vm.$el.querySelector('.ci-status-icon-failed')).not.toBe(null);
- });
-
- it('renders list of jobs', () => {
- expect(vm.$el.querySelectorAll('.tab-pane:first-child .ide-job-item').length).toBe(
- jobs.length * stages.length,
- );
- });
-
- it('renders list of failed jobs on failed jobs tab', done => {
- vm.$el.querySelectorAll('.tab-links a')[1].click();
-
- vm.$nextTick(() => {
- expect(vm.$el.querySelectorAll('.tab-pane.active .ide-job-item').length).toBe(2);
-
- done();
- });
- });
-
- describe('YAML error', () => {
- it('renders YAML error', done => {
- vm.$store.state.pipelines.latestPipeline.yamlError = 'test yaml error';
-
- vm.$nextTick(() => {
- expect(vm.$el.textContent).toContain('Found errors in your .gitlab-ci.yml:');
- expect(vm.$el.textContent).toContain('test yaml error');
-
- done();
- });
- });
- });
-
- describe('empty state', () => {
- it('renders pipelines empty state', done => {
- vm.$store.state.pipelines.latestPipeline = null;
-
- vm.$nextTick(() => {
- expect(vm.$el.querySelector('.empty-state')).not.toBe(null);
-
- done();
- });
- });
- });
-
- describe('loading state', () => {
- beforeEach(() => {
- vm.$store.state.pipelines.isLoadingPipeline = true;
- });
-
- it('does not render when pipeline has loaded before', done => {
- vm.$store.state.pipelines.hasLoadedPipeline = true;
-
- vm.$nextTick()
- .then(() => {
- expect(findLoadingState()).toBe(null);
- })
- .then(done)
- .catch(done.fail);
- });
-
- it('renders loading state when there is no latest pipeline', done => {
- vm.$store.state.pipelines.hasLoadedPipeline = false;
-
- vm.$nextTick()
- .then(() => {
- expect(findLoadingState()).not.toBe(null);
- })
- .then(done)
- .catch(done.fail);
- });
- });
-});
diff --git a/spec/javascripts/ide/components/repo_editor_spec.js b/spec/javascripts/ide/components/repo_editor_spec.js
index 0701b773e17..d1b43df74b9 100644
--- a/spec/javascripts/ide/components/repo_editor_spec.js
+++ b/spec/javascripts/ide/components/repo_editor_spec.js
@@ -410,10 +410,23 @@ describe('RepoEditor', () => {
describe('initEditor', () => {
beforeEach(() => {
+ vm.file.tempFile = false;
spyOn(vm.editor, 'createInstance');
spyOnProperty(vm, 'shouldHideEditor').and.returnValue(true);
});
+ it('does not fetch file information for temp entries', done => {
+ vm.file.tempFile = true;
+
+ vm.initEditor();
+ vm.$nextTick()
+ .then(() => {
+ expect(vm.getFileData).not.toHaveBeenCalled();
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
it('is being initialised for files without content even if shouldHideEditor is `true`', done => {
vm.file.content = '';
vm.file.raw = '';
@@ -429,16 +442,13 @@ describe('RepoEditor', () => {
});
it('does not initialize editor for files already with content', done => {
- expect(vm.getFileData.calls.count()).toEqual(1);
- expect(vm.getRawFileData.calls.count()).toEqual(1);
-
vm.file.content = 'foo';
vm.initEditor();
vm.$nextTick()
.then(() => {
- expect(vm.getFileData.calls.count()).toEqual(1);
- expect(vm.getRawFileData.calls.count()).toEqual(1);
+ expect(vm.getFileData).not.toHaveBeenCalled();
+ expect(vm.getRawFileData).not.toHaveBeenCalled();
expect(vm.editor.createInstance).not.toHaveBeenCalled();
})
.then(done)
@@ -446,23 +456,56 @@ describe('RepoEditor', () => {
});
});
- it('calls removePendingTab when old file is pending', done => {
- spyOnProperty(vm, 'shouldHideEditor').and.returnValue(true);
- spyOn(vm, 'removePendingTab');
+ describe('updates on file changes', () => {
+ beforeEach(() => {
+ spyOn(vm, 'initEditor');
+ });
- vm.file.pending = true;
+ it('calls removePendingTab when old file is pending', done => {
+ spyOnProperty(vm, 'shouldHideEditor').and.returnValue(true);
+ spyOn(vm, 'removePendingTab');
- vm.$nextTick()
- .then(() => {
- vm.file = file('testing');
- vm.file.content = 'foo'; // need to prevent full cycle of initEditor
+ vm.file.pending = true;
+
+ vm.$nextTick()
+ .then(() => {
+ vm.file = file('testing');
+ vm.file.content = 'foo'; // need to prevent full cycle of initEditor
- return vm.$nextTick();
- })
- .then(() => {
- expect(vm.removePendingTab).toHaveBeenCalled();
- })
- .then(done)
- .catch(done.fail);
+ return vm.$nextTick();
+ })
+ .then(() => {
+ expect(vm.removePendingTab).toHaveBeenCalled();
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('does not call initEditor if the file did not change', done => {
+ Vue.set(vm, 'file', vm.file);
+
+ vm.$nextTick()
+ .then(() => {
+ expect(vm.initEditor).not.toHaveBeenCalled();
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('calls initEditor when file key is changed', done => {
+ expect(vm.initEditor).not.toHaveBeenCalled();
+
+ Vue.set(vm, 'file', {
+ ...vm.file,
+ key: 'new',
+ });
+
+ vm.$nextTick()
+ .then(() => {
+ expect(vm.initEditor).toHaveBeenCalled();
+ })
+ .then(done)
+ .catch(done.fail);
+ });
});
});
diff --git a/spec/javascripts/ide/mock_data.js b/spec/javascripts/ide/mock_data.js
index c02c7e5d45e..27f0ad01f54 100644
--- a/spec/javascripts/ide/mock_data.js
+++ b/spec/javascripts/ide/mock_data.js
@@ -1,226 +1 @@
-export const projectData = {
- id: 1,
- name: 'abcproject',
- web_url: '',
- avatar_url: '',
- path: '',
- name_with_namespace: 'namespace/abcproject',
- branches: {
- master: {
- treeId: 'abcproject/master',
- can_push: true,
- commit: {
- id: '123',
- },
- },
- },
- mergeRequests: {},
- merge_requests_enabled: true,
- default_branch: 'master',
-};
-
-export const pipelines = [
- {
- id: 1,
- ref: 'master',
- sha: '123',
- details: {
- status: {
- icon: 'status_failed',
- group: 'failed',
- text: 'Failed',
- },
- },
- commit: { id: '123' },
- },
- {
- id: 2,
- ref: 'master',
- sha: '213',
- details: {
- status: {
- icon: 'status_failed',
- group: 'failed',
- text: 'Failed',
- },
- },
- commit: { id: '213' },
- },
-];
-
-export const stages = [
- {
- dropdown_path: `${gl.TEST_HOST}/testing`,
- name: 'build',
- status: {
- icon: 'status_failed',
- group: 'failed',
- text: 'failed',
- },
- },
- {
- dropdown_path: 'testing',
- name: 'test',
- status: {
- icon: 'status_failed',
- group: 'failed',
- text: 'failed',
- },
- },
-];
-
-export const jobs = [
- {
- id: 1,
- name: 'test',
- path: 'testing',
- status: {
- icon: 'status_success',
- text: 'passed',
- },
- stage: 'test',
- duration: 1,
- started: new Date(),
- },
- {
- id: 2,
- name: 'test 2',
- path: 'testing2',
- status: {
- icon: 'status_success',
- text: 'passed',
- },
- stage: 'test',
- duration: 1,
- started: new Date(),
- },
- {
- id: 3,
- name: 'test 3',
- path: 'testing3',
- status: {
- icon: 'status_success',
- text: 'passed',
- },
- stage: 'test',
- duration: 1,
- started: new Date(),
- },
- {
- id: 4,
- name: 'test 4',
- path: 'testing4',
- status: {
- icon: 'status_failed',
- text: 'failed',
- },
- stage: 'build',
- duration: 1,
- started: new Date(),
- },
-];
-
-export const fullPipelinesResponse = {
- data: {
- count: {
- all: 2,
- },
- pipelines: [
- {
- id: '51',
- path: 'test',
- commit: {
- id: '123',
- },
- details: {
- status: {
- icon: 'status_failed',
- text: 'failed',
- },
- stages: [...stages],
- },
- },
- {
- id: '50',
- commit: {
- id: 'abc123def456ghi789jkl',
- },
- details: {
- status: {
- icon: 'status_success',
- text: 'passed',
- },
- stages: [...stages],
- },
- },
- ],
- },
-};
-
-export const mergeRequests = [
- {
- id: 1,
- iid: 1,
- title: 'Test merge request',
- project_id: 1,
- web_url: `${gl.TEST_HOST}/namespace/project-path/merge_requests/1`,
- },
-];
-
-export const branches = [
- {
- id: 1,
- name: 'master',
- commit: {
- message: 'Update master branch',
- committed_date: '2018-08-01T00:20:05Z',
- },
- can_push: true,
- protected: true,
- default: true,
- },
- {
- id: 2,
- name: 'protected/no-access',
- commit: {
- message: 'Update some stuff',
- committed_date: '2018-08-02T00:00:05Z',
- },
- can_push: false,
- protected: true,
- default: false,
- },
- {
- id: 3,
- name: 'protected/access',
- commit: {
- message: 'Update some stuff',
- committed_date: '2018-08-02T00:00:05Z',
- },
- can_push: true,
- protected: true,
- default: false,
- },
- {
- id: 4,
- name: 'regular',
- commit: {
- message: 'Update some more stuff',
- committed_date: '2018-06-30T00:20:05Z',
- },
- can_push: true,
- protected: false,
- default: false,
- },
- {
- id: 5,
- name: 'regular/no-access',
- commit: {
- message: 'Update some more stuff',
- committed_date: '2018-06-30T00:20:05Z',
- },
- can_push: false,
- protected: false,
- default: false,
- },
-];
+export * from '../../frontend/ide/mock_data';
diff --git a/spec/javascripts/ide/stores/actions/project_spec.js b/spec/javascripts/ide/stores/actions/project_spec.js
index 8ecb6129c63..bcc7b5d5e46 100644
--- a/spec/javascripts/ide/stores/actions/project_spec.js
+++ b/spec/javascripts/ide/stores/actions/project_spec.js
@@ -6,8 +6,10 @@ import {
createNewBranchFromDefault,
showEmptyState,
openBranch,
+ loadFile,
+ loadBranch,
} from '~/ide/stores/actions';
-import store from '~/ide/stores';
+import { createStore } from '~/ide/stores';
import service from '~/ide/services';
import api from '~/api';
import router from '~/ide/ide_router';
@@ -16,8 +18,10 @@ import testAction from '../../../helpers/vuex_action_helper';
describe('IDE store project actions', () => {
let mock;
+ let store;
beforeEach(() => {
+ store = createStore();
mock = new MockAdapter(axios);
store.state.projects['abc/def'] = {
@@ -231,28 +235,139 @@ describe('IDE store project actions', () => {
});
});
+ describe('loadFile', () => {
+ beforeEach(() => {
+ Object.assign(store.state, {
+ entries: {
+ foo: { pending: false },
+ 'foo/bar-pending': { pending: true },
+ 'foo/bar': { pending: false },
+ },
+ });
+ spyOn(store, 'dispatch');
+ });
+
+ it('does nothing, if basePath is not given', () => {
+ loadFile(store, { basePath: undefined });
+
+ expect(store.dispatch).not.toHaveBeenCalled();
+ });
+
+ it('handles tree entry action, if basePath is given and the entry is not pending', () => {
+ loadFile(store, { basePath: 'foo/bar/' });
+
+ expect(store.dispatch).toHaveBeenCalledWith(
+ 'handleTreeEntryAction',
+ store.state.entries['foo/bar'],
+ );
+ });
+
+ it('does not handle tree entry action, if entry is pending', () => {
+ loadFile(store, { basePath: 'foo/bar-pending/' });
+
+ expect(store.dispatch).not.toHaveBeenCalledWith('handleTreeEntryAction', jasmine.anything());
+ });
+
+ it('creates a new temp file supplied via URL if the file does not exist yet', () => {
+ loadFile(store, { basePath: 'not-existent.md' });
+
+ expect(store.dispatch.calls.count()).toBe(1);
+
+ expect(store.dispatch).not.toHaveBeenCalledWith('handleTreeEntryAction', jasmine.anything());
+
+ expect(store.dispatch).toHaveBeenCalledWith('createTempEntry', {
+ name: 'not-existent.md',
+ type: 'blob',
+ });
+ });
+ });
+
+ describe('loadBranch', () => {
+ const projectId = 'abc/def';
+ const branchId = '123-lorem';
+
+ it('fetches branch data', done => {
+ spyOn(store, 'dispatch').and.returnValue(Promise.resolve());
+
+ loadBranch(store, { projectId, branchId })
+ .then(() => {
+ expect(store.dispatch.calls.allArgs()).toEqual([
+ ['getBranchData', { projectId, branchId }],
+ ['getMergeRequestsForBranch', { projectId, branchId }],
+ ['getFiles', { projectId, branchId }],
+ ]);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('shows an error if branch can not be fetched', done => {
+ spyOn(store, 'dispatch').and.returnValue(Promise.reject());
+
+ loadBranch(store, { projectId, branchId })
+ .then(done.fail)
+ .catch(() => {
+ expect(store.dispatch.calls.allArgs()).toEqual([
+ ['getBranchData', { projectId, branchId }],
+ ['showBranchNotFoundError', branchId],
+ ]);
+ done();
+ });
+ });
+ });
+
describe('openBranch', () => {
+ const projectId = 'abc/def';
+ const branchId = '123-lorem';
+
const branch = {
- projectId: 'abc/def',
- branchId: '123-lorem',
+ projectId,
+ branchId,
};
beforeEach(() => {
- store.state.entries = {
- foo: { pending: false },
- 'foo/bar-pending': { pending: true },
- 'foo/bar': { pending: false },
- };
+ Object.assign(store.state, {
+ entries: {
+ foo: { pending: false },
+ 'foo/bar-pending': { pending: true },
+ 'foo/bar': { pending: false },
+ },
+ });
+ });
+
+ it('loads file right away if the branch has already been fetched', done => {
+ spyOn(store, 'dispatch');
+
+ Object.assign(store.state, {
+ projects: {
+ [projectId]: {
+ branches: {
+ [branchId]: { foo: 'bar' },
+ },
+ },
+ },
+ });
+
+ openBranch(store, branch)
+ .then(() => {
+ expect(store.dispatch.calls.allArgs()).toEqual([['loadFile', { basePath: undefined }]]);
+ })
+ .then(done)
+ .catch(done.fail);
});
describe('empty repo', () => {
beforeEach(() => {
spyOn(store, 'dispatch').and.returnValue(Promise.resolve());
- store.state.currentProjectId = 'abc/def';
- store.state.projects['abc/def'] = {
- empty_repo: true,
- };
+ Object.assign(store.state, {
+ currentProjectId: 'abc/def',
+ projects: {
+ 'abc/def': {
+ empty_repo: true,
+ },
+ },
+ });
});
afterEach(() => {
@@ -262,10 +377,7 @@ describe('IDE store project actions', () => {
it('dispatches showEmptyState action right away', done => {
openBranch(store, branch)
.then(() => {
- expect(store.dispatch.calls.allArgs()).toEqual([
- ['setCurrentBranchId', branch.branchId],
- ['showEmptyState', branch],
- ]);
+ expect(store.dispatch.calls.allArgs()).toEqual([['showEmptyState', branch]]);
done();
})
.catch(done.fail);
@@ -281,56 +393,14 @@ describe('IDE store project actions', () => {
openBranch(store, branch)
.then(() => {
expect(store.dispatch.calls.allArgs()).toEqual([
- ['setCurrentBranchId', branch.branchId],
- ['getBranchData', branch],
- ['getMergeRequestsForBranch', branch],
- ['getFiles', branch],
+ ['setCurrentBranchId', branchId],
+ ['loadBranch', { projectId, branchId }],
+ ['loadFile', { basePath: undefined }],
]);
})
.then(done)
.catch(done.fail);
});
-
- it('handles tree entry action, if basePath is given', done => {
- openBranch(store, { ...branch, basePath: 'foo/bar/' })
- .then(() => {
- expect(store.dispatch).toHaveBeenCalledWith(
- 'handleTreeEntryAction',
- store.state.entries['foo/bar'],
- );
- })
- .then(done)
- .catch(done.fail);
- });
-
- it('does not handle tree entry action, if entry is pending', done => {
- openBranch(store, { ...branch, basePath: 'foo/bar-pending' })
- .then(() => {
- expect(store.dispatch).not.toHaveBeenCalledWith(
- 'handleTreeEntryAction',
- jasmine.anything(),
- );
- })
- .then(done)
- .catch(done.fail);
- });
-
- it('creates a new file supplied via URL if the file does not exist yet', done => {
- openBranch(store, { ...branch, basePath: 'not-existent.md' })
- .then(() => {
- expect(store.dispatch).not.toHaveBeenCalledWith(
- 'handleTreeEntryAction',
- jasmine.anything(),
- );
-
- expect(store.dispatch).toHaveBeenCalledWith('createTempEntry', {
- name: 'not-existent.md',
- type: 'blob',
- });
- })
- .then(done)
- .catch(done.fail);
- });
});
describe('non-existent branch', () => {
@@ -342,9 +412,8 @@ describe('IDE store project actions', () => {
openBranch(store, branch)
.then(() => {
expect(store.dispatch.calls.allArgs()).toEqual([
- ['setCurrentBranchId', branch.branchId],
- ['getBranchData', branch],
- ['showBranchNotFoundError', branch.branchId],
+ ['setCurrentBranchId', branchId],
+ ['loadBranch', { projectId, branchId }],
]);
})
.then(done)
diff --git a/spec/javascripts/ide/stores/actions_spec.js b/spec/javascripts/ide/stores/actions_spec.js
index 8504fb3f42b..7e77b859fdd 100644
--- a/spec/javascripts/ide/stores/actions_spec.js
+++ b/spec/javascripts/ide/stores/actions_spec.js
@@ -13,12 +13,15 @@ import actions, {
createTempEntry,
} from '~/ide/stores/actions';
import axios from '~/lib/utils/axios_utils';
-import store from '~/ide/stores';
+import { createStore } from '~/ide/stores';
import * as types from '~/ide/stores/mutation_types';
import router from '~/ide/ide_router';
import { resetStore, file } from '../helpers';
import testAction from '../../helpers/vuex_action_helper';
import MockAdapter from 'axios-mock-adapter';
+import eventHub from '~/ide/eventhub';
+
+const store = createStore();
describe('Multi-file store actions', () => {
beforeEach(() => {
@@ -451,6 +454,24 @@ describe('Multi-file store actions', () => {
done,
);
});
+
+ it('does not dispatch for parent, if parent does not exist', done => {
+ const f = {
+ ...file(),
+ path: 'test',
+ parentPath: 'testing',
+ };
+ store.state.entries[f.path] = f;
+
+ testAction(
+ updateTempFlagForEntry,
+ { file: f, tempFile: false },
+ store.state,
+ [{ type: 'UPDATE_TEMP_FLAG', payload: { path: f.path, tempFile: false } }],
+ [],
+ done,
+ );
+ });
});
describe('setCurrentBranchId', () => {
@@ -540,82 +561,298 @@ describe('Multi-file store actions', () => {
done,
);
});
- });
- describe('renameEntry', () => {
- it('renames entry', done => {
- store.state.entries.test = {
- tree: [],
+ it('if renamed, reverts the rename before deleting', () => {
+ const testEntry = {
+ path: 'test',
+ name: 'test',
+ prevPath: 'lorem/ipsum',
+ prevName: 'ipsum',
+ prevParentPath: 'lorem',
};
+ store.state.entries = { test: testEntry };
testAction(
- renameEntry,
- { path: 'test', name: 'new-name', entryPath: null, parentPath: 'parent-path' },
+ deleteEntry,
+ testEntry.path,
store.state,
+ [],
[
{
- type: types.RENAME_ENTRY,
- payload: { path: 'test', name: 'new-name', entryPath: null, parentPath: 'parent-path' },
- },
- {
- type: types.TOGGLE_FILE_CHANGED,
+ type: 'renameEntry',
payload: {
- file: store.state.entries['parent-path/new-name'],
- changed: true,
+ path: testEntry.path,
+ name: testEntry.prevName,
+ parentPath: testEntry.prevParentPath,
},
},
+ {
+ type: 'deleteEntry',
+ payload: testEntry.prevPath,
+ },
],
- [{ type: 'triggerFilesChange' }],
- done,
);
});
+ });
- it('renames all entries in tree', done => {
- store.state.entries.test = {
- type: 'tree',
- tree: [
- {
- path: 'tree-1',
- },
- {
- path: 'tree-2',
+ describe('renameEntry', () => {
+ describe('purging of file model cache', () => {
+ beforeEach(() => {
+ spyOn(eventHub, '$emit');
+ });
+
+ it('does not purge model cache for temporary entries that got renamed', done => {
+ Object.assign(store.state.entries, {
+ test: {
+ ...file('test'),
+ key: 'foo-key',
+ type: 'blob',
+ tempFile: true,
},
- ],
- };
+ });
- testAction(
- renameEntry,
- { path: 'test', name: 'new-name', parentPath: 'parent-path' },
- store.state,
- [
- {
- type: types.RENAME_ENTRY,
- payload: { path: 'test', name: 'new-name', entryPath: null, parentPath: 'parent-path' },
+ store
+ .dispatch('renameEntry', {
+ path: 'test',
+ name: 'new',
+ })
+ .then(() => {
+ expect(eventHub.$emit.calls.allArgs()).not.toContain(
+ 'editor.update.model.dispose.foo-bar',
+ );
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('purges model cache for renamed entry', done => {
+ Object.assign(store.state.entries, {
+ test: {
+ ...file('test'),
+ key: 'foo-key',
+ type: 'blob',
+ tempFile: false,
},
- ],
- [
- {
- type: 'renameEntry',
- payload: {
- path: 'test',
- name: 'new-name',
- entryPath: 'tree-1',
- parentPath: 'parent-path/new-name',
+ });
+
+ store
+ .dispatch('renameEntry', {
+ path: 'test',
+ name: 'new',
+ })
+ .then(() => {
+ expect(eventHub.$emit).toHaveBeenCalled();
+ expect(eventHub.$emit).toHaveBeenCalledWith(`editor.update.model.dispose.foo-key`);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+
+ describe('single entry', () => {
+ let origEntry;
+ let renamedEntry;
+
+ beforeEach(() => {
+ // Need to insert both because `testAction` doesn't actually call the mutation
+ origEntry = file('orig', 'orig', 'blob');
+ renamedEntry = {
+ ...file('renamed', 'renamed', 'blob'),
+ prevKey: origEntry.key,
+ prevName: origEntry.name,
+ prevPath: origEntry.path,
+ };
+
+ Object.assign(store.state.entries, {
+ orig: origEntry,
+ renamed: renamedEntry,
+ });
+ });
+
+ afterEach(() => {
+ resetStore(store);
+ });
+
+ it('by default renames an entry and adds to changed', done => {
+ testAction(
+ renameEntry,
+ { path: 'orig', name: 'renamed' },
+ store.state,
+ [
+ {
+ type: types.RENAME_ENTRY,
+ payload: {
+ path: 'orig',
+ name: 'renamed',
+ parentPath: undefined,
+ },
},
- },
- {
- type: 'renameEntry',
- payload: {
- path: 'test',
- name: 'new-name',
- entryPath: 'tree-2',
- parentPath: 'parent-path/new-name',
+ {
+ type: types.ADD_FILE_TO_CHANGED,
+ payload: 'renamed',
+ },
+ ],
+ [{ type: 'triggerFilesChange' }],
+ done,
+ );
+ });
+
+ it('if not changed, completely unstages entry if renamed to original', done => {
+ testAction(
+ renameEntry,
+ { path: 'renamed', name: 'orig' },
+ store.state,
+ [
+ {
+ type: types.RENAME_ENTRY,
+ payload: {
+ path: 'renamed',
+ name: 'orig',
+ parentPath: undefined,
+ },
+ },
+ {
+ type: types.REMOVE_FILE_FROM_STAGED_AND_CHANGED,
+ payload: origEntry,
+ },
+ ],
+ [{ type: 'triggerFilesChange' }],
+ done,
+ );
+ });
+
+ it('if already in changed, does not add to change', done => {
+ store.state.changedFiles.push(renamedEntry);
+
+ testAction(
+ renameEntry,
+ { path: 'orig', name: 'renamed' },
+ store.state,
+ [jasmine.objectContaining({ type: types.RENAME_ENTRY })],
+ [{ type: 'triggerFilesChange' }],
+ done,
+ );
+ });
+
+ it('routes to the renamed file if the original file has been opened', done => {
+ Object.assign(store.state.entries.orig, {
+ opened: true,
+ url: '/foo-bar.md',
+ });
+
+ store
+ .dispatch('renameEntry', {
+ path: 'orig',
+ name: 'renamed',
+ })
+ .then(() => {
+ expect(router.push.calls.count()).toBe(1);
+ expect(router.push).toHaveBeenCalledWith(`/project/foo-bar.md`);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+
+ describe('folder', () => {
+ let folder;
+ let file1;
+ let file2;
+
+ beforeEach(() => {
+ folder = file('folder', 'folder', 'tree');
+ file1 = file('file-1', 'file-1', 'blob', folder);
+ file2 = file('file-2', 'file-2', 'blob', folder);
+
+ folder.tree = [file1, file2];
+
+ Object.assign(store.state.entries, {
+ [folder.path]: folder,
+ [file1.path]: file1,
+ [file2.path]: file2,
+ });
+ });
+
+ it('updates entries in a folder correctly, when folder is renamed', done => {
+ store
+ .dispatch('renameEntry', {
+ path: 'folder',
+ name: 'new-folder',
+ })
+ .then(() => {
+ const keys = Object.keys(store.state.entries);
+
+ expect(keys.length).toBe(3);
+ expect(keys.indexOf('new-folder')).toBe(0);
+ expect(keys.indexOf('new-folder/file-1')).toBe(1);
+ expect(keys.indexOf('new-folder/file-2')).toBe(2);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('discards renaming of an entry if the root folder is renamed back to a previous name', done => {
+ const rootFolder = file('old-folder', 'old-folder', 'tree');
+ const testEntry = file('test', 'test', 'blob', rootFolder);
+
+ Object.assign(store.state, {
+ entries: {
+ 'old-folder': {
+ ...rootFolder,
+ tree: [testEntry],
},
+ 'old-folder/test': testEntry,
},
- { type: 'triggerFilesChange' },
- ],
- done,
- );
+ });
+
+ store
+ .dispatch('renameEntry', {
+ path: 'old-folder',
+ name: 'new-folder',
+ })
+ .then(() => {
+ const { entries } = store.state;
+
+ expect(Object.keys(entries).length).toBe(2);
+ expect(entries['old-folder']).toBeUndefined();
+ expect(entries['old-folder/test']).toBeUndefined();
+
+ expect(entries['new-folder']).toBeDefined();
+ expect(entries['new-folder/test']).toEqual(
+ jasmine.objectContaining({
+ path: 'new-folder/test',
+ name: 'test',
+ prevPath: 'old-folder/test',
+ prevName: 'test',
+ }),
+ );
+ })
+ .then(() =>
+ store.dispatch('renameEntry', {
+ path: 'new-folder',
+ name: 'old-folder',
+ }),
+ )
+ .then(() => {
+ const { entries } = store.state;
+
+ expect(Object.keys(entries).length).toBe(2);
+ expect(entries['new-folder']).toBeUndefined();
+ expect(entries['new-folder/test']).toBeUndefined();
+
+ expect(entries['old-folder']).toBeDefined();
+ expect(entries['old-folder/test']).toEqual(
+ jasmine.objectContaining({
+ path: 'old-folder/test',
+ name: 'test',
+ prevPath: undefined,
+ prevName: undefined,
+ }),
+ );
+ })
+ .then(done)
+ .catch(done.fail);
+ });
});
});
diff --git a/spec/javascripts/ide/stores/modules/commit/actions_spec.js b/spec/javascripts/ide/stores/modules/commit/actions_spec.js
index 091b454c0d2..95d927065f0 100644
--- a/spec/javascripts/ide/stores/modules/commit/actions_spec.js
+++ b/spec/javascripts/ide/stores/modules/commit/actions_spec.js
@@ -1,5 +1,5 @@
import rootActions from '~/ide/stores/actions';
-import store from '~/ide/stores';
+import { createStore } from '~/ide/stores';
import service from '~/ide/services';
import router from '~/ide/ide_router';
import eventHub from '~/ide/eventhub';
@@ -11,6 +11,7 @@ import { resetStore, file } from 'spec/ide/helpers';
import testAction from '../../../../helpers/vuex_action_helper';
const TEST_COMMIT_SHA = '123456789';
+const store = createStore();
describe('IDE commit module actions', () => {
beforeEach(() => {
@@ -59,7 +60,9 @@ describe('IDE commit module actions', () => {
});
it('sets shouldCreateMR to true if "Create new MR" option is visible', done => {
- store.state.shouldHideNewMrOption = false;
+ Object.assign(store.state, {
+ shouldHideNewMrOption: false,
+ });
testAction(
actions.updateCommitAction,
@@ -78,7 +81,9 @@ describe('IDE commit module actions', () => {
});
it('sets shouldCreateMR to false if "Create new MR" option is hidden', done => {
- store.state.shouldHideNewMrOption = true;
+ Object.assign(store.state, {
+ shouldHideNewMrOption: true,
+ });
testAction(
actions.updateCommitAction,
@@ -172,24 +177,31 @@ describe('IDE commit module actions', () => {
content: 'file content',
});
- store.state.currentProjectId = 'abcproject';
- store.state.currentBranchId = 'master';
- store.state.projects.abcproject = {
- web_url: 'web_url',
- branches: {
- master: {
- workingReference: '',
- commit: {
- short_id: TEST_COMMIT_SHA,
+ Object.assign(store.state, {
+ currentProjectId: 'abcproject',
+ currentBranchId: 'master',
+ projects: {
+ abcproject: {
+ web_url: 'web_url',
+ branches: {
+ master: {
+ workingReference: '',
+ commit: {
+ short_id: TEST_COMMIT_SHA,
+ },
+ },
},
},
},
- };
- store.state.stagedFiles.push(f, {
- ...file('changedFile2'),
- changed: true,
+ stagedFiles: [
+ f,
+ {
+ ...file('changedFile2'),
+ changed: true,
+ },
+ ],
+ openFiles: store.state.stagedFiles,
});
- store.state.openFiles = store.state.stagedFiles;
store.state.stagedFiles.forEach(stagedFile => {
store.state.entries[stagedFile.path] = stagedFile;
@@ -275,40 +287,40 @@ describe('IDE commit module actions', () => {
document.body.innerHTML += '<div class="flash-container"></div>';
- store.state.currentProjectId = 'abcproject';
- store.state.currentBranchId = 'master';
- store.state.projects.abcproject = {
- web_url: 'webUrl',
- branches: {
- master: {
- workingReference: '1',
- commit: {
- id: TEST_COMMIT_SHA,
- },
- },
- },
- };
-
const f = {
...file('changed'),
type: 'blob',
active: true,
lastCommitSha: TEST_COMMIT_SHA,
};
- store.state.stagedFiles.push(f);
- store.state.changedFiles = [
- {
- ...f,
- },
- ];
- store.state.openFiles = store.state.changedFiles;
- store.state.openFiles.forEach(localF => {
- store.state.entries[localF.path] = localF;
+ Object.assign(store.state, {
+ stagedFiles: [f],
+ changedFiles: [f],
+ openFiles: [f],
+ currentProjectId: 'abcproject',
+ currentBranchId: 'master',
+ projects: {
+ abcproject: {
+ web_url: 'webUrl',
+ branches: {
+ master: {
+ workingReference: '1',
+ commit: {
+ id: TEST_COMMIT_SHA,
+ },
+ },
+ },
+ },
+ },
});
store.state.commit.commitAction = '2';
store.state.commit.commitMessage = 'testing 123';
+
+ store.state.openFiles.forEach(localF => {
+ store.state.entries[localF.path] = localF;
+ });
});
afterEach(() => {
@@ -473,18 +485,16 @@ describe('IDE commit module actions', () => {
});
it('resets changed files before redirecting', done => {
+ visitUrl = visitUrl.and.callFake(() => {
+ expect(store.state.stagedFiles.length).toBe(0);
+ done();
+ });
+
spyOn(eventHub, '$on');
store.state.commit.commitAction = '3';
- store
- .dispatch('commit/commitChanges')
- .then(() => {
- expect(store.state.stagedFiles.length).toBe(0);
-
- done();
- })
- .catch(done.fail);
+ store.dispatch('commit/commitChanges').catch(done.fail);
});
});
});
diff --git a/spec/javascripts/ide/stores/mutations/file_spec.js b/spec/javascripts/ide/stores/mutations/file_spec.js
index 064e66cef64..7c46bf55318 100644
--- a/spec/javascripts/ide/stores/mutations/file_spec.js
+++ b/spec/javascripts/ide/stores/mutations/file_spec.js
@@ -356,16 +356,16 @@ describe('IDE store file mutations', () => {
});
describe('STAGE_CHANGE', () => {
- it('adds file into stagedFiles array', () => {
+ beforeEach(() => {
mutations.STAGE_CHANGE(localState, localFile.path);
+ });
+ it('adds file into stagedFiles array', () => {
expect(localState.stagedFiles.length).toBe(1);
expect(localState.stagedFiles[0]).toEqual(localFile);
});
it('updates stagedFile if it is already staged', () => {
- mutations.STAGE_CHANGE(localState, localFile.path);
-
localFile.raw = 'testing 123';
mutations.STAGE_CHANGE(localState, localFile.path);
@@ -373,19 +373,6 @@ describe('IDE store file mutations', () => {
expect(localState.stagedFiles.length).toBe(1);
expect(localState.stagedFiles[0].raw).toEqual('testing 123');
});
-
- it('adds already-staged file to `replacedFiles`', () => {
- localFile.raw = 'already-staged';
-
- mutations.STAGE_CHANGE(localState, localFile.path);
-
- localFile.raw = 'testing 123';
-
- mutations.STAGE_CHANGE(localState, localFile.path);
-
- expect(localState.replacedFiles.length).toBe(1);
- expect(localState.replacedFiles[0].raw).toEqual('already-staged');
- });
});
describe('UNSTAGE_CHANGE', () => {
diff --git a/spec/javascripts/ide/stores/mutations_spec.js b/spec/javascripts/ide/stores/mutations_spec.js
index 2470c99e300..7dd5d323f69 100644
--- a/spec/javascripts/ide/stores/mutations_spec.js
+++ b/spec/javascripts/ide/stores/mutations_spec.js
@@ -79,16 +79,6 @@ describe('Multi-file store mutations', () => {
});
});
- describe('CLEAR_REPLACED_FILES', () => {
- it('clears replacedFiles array', () => {
- localState.replacedFiles.push('a');
-
- mutations.CLEAR_REPLACED_FILES(localState);
-
- expect(localState.replacedFiles.length).toBe(0);
- });
- });
-
describe('UPDATE_VIEWER', () => {
it('sets viewer state', () => {
mutations.UPDATE_VIEWER(localState, 'diff');
@@ -311,8 +301,7 @@ describe('Multi-file store mutations', () => {
describe('UPDATE_FILE_AFTER_COMMIT', () => {
it('updates URLs if prevPath is set', () => {
const f = {
- ...file(),
- path: 'test',
+ ...file('test'),
prevPath: 'testing-123',
rawPath: `${gl.TEST_HOST}/testing-123`,
permalink: `${gl.TEST_HOST}/testing-123`,
@@ -325,19 +314,26 @@ describe('Multi-file store mutations', () => {
mutations.UPDATE_FILE_AFTER_COMMIT(localState, { file: f, lastCommit: { commit: {} } });
- expect(f.rawPath).toBe(`${gl.TEST_HOST}/test`);
- expect(f.permalink).toBe(`${gl.TEST_HOST}/test`);
- expect(f.commitsPath).toBe(`${gl.TEST_HOST}/test`);
- expect(f.blamePath).toBe(`${gl.TEST_HOST}/test`);
- expect(f.replaces).toBe(false);
+ expect(f).toEqual(
+ jasmine.objectContaining({
+ rawPath: `${gl.TEST_HOST}/test`,
+ permalink: `${gl.TEST_HOST}/test`,
+ commitsPath: `${gl.TEST_HOST}/test`,
+ blamePath: `${gl.TEST_HOST}/test`,
+ replaces: false,
+ prevId: undefined,
+ prevPath: undefined,
+ prevName: undefined,
+ prevUrl: undefined,
+ prevKey: undefined,
+ }),
+ );
});
});
describe('OPEN_NEW_ENTRY_MODAL', () => {
it('sets entryModal', () => {
- localState.entries.testPath = {
- ...file(),
- };
+ localState.entries.testPath = file();
mutations.OPEN_NEW_ENTRY_MODAL(localState, { type: 'test', path: 'testPath' });
@@ -356,58 +352,178 @@ describe('Multi-file store mutations', () => {
};
localState.currentProjectId = 'gitlab-ce';
localState.currentBranchId = 'master';
- localState.entries.oldPath = {
- ...file(),
- type: 'blob',
- name: 'oldPath',
- path: 'oldPath',
- url: `${gl.TEST_HOST}/oldPath`,
+ localState.entries = {
+ oldPath: file('oldPath', 'oldPath', 'blob'),
};
});
- it('creates new renamed entry', () => {
+ it('updates existing entry without creating a new one', () => {
+ mutations.RENAME_ENTRY(localState, {
+ path: 'oldPath',
+ name: 'newPath',
+ parentPath: '',
+ });
+
+ expect(localState.entries).toEqual({
+ newPath: jasmine.objectContaining({
+ path: 'newPath',
+ prevPath: 'oldPath',
+ }),
+ });
+ });
+
+ it('correctly handles consecutive renames for the same entry', () => {
mutations.RENAME_ENTRY(localState, {
path: 'oldPath',
name: 'newPath',
+ parentPath: '',
+ });
+
+ mutations.RENAME_ENTRY(localState, {
+ path: 'newPath',
+ name: 'newestPath',
+ parentPath: '',
+ });
+
+ expect(localState.entries).toEqual({
+ newestPath: jasmine.objectContaining({
+ path: 'newestPath',
+ prevPath: 'oldPath',
+ }),
+ });
+ });
+
+ it('correctly handles the same entry within a consecutively renamed folder', () => {
+ const oldPath = file('root-folder/oldPath', 'root-folder/oldPath', 'blob');
+ localState.entries = {
+ 'root-folder': {
+ ...file('root-folder', 'root-folder', 'tree'),
+ tree: [oldPath],
+ },
+ 'root-folder/oldPath': oldPath,
+ };
+ Object.assign(localState.entries['root-folder/oldPath'], {
+ parentPath: 'root-folder',
+ url: 'root-folder/oldPath-blob-root-folder/oldPath',
+ });
+
+ mutations.RENAME_ENTRY(localState, {
+ path: 'root-folder/oldPath',
+ name: 'renamed-folder/oldPath',
entryPath: null,
parentPath: '',
});
+ mutations.RENAME_ENTRY(localState, {
+ path: 'renamed-folder/oldPath',
+ name: 'simply-renamed/oldPath',
+ entryPath: null,
+ parentPath: '',
+ });
+
+ expect(localState.entries).toEqual({
+ 'root-folder': jasmine.objectContaining({
+ path: 'root-folder',
+ }),
+ 'simply-renamed/oldPath': jasmine.objectContaining({
+ path: 'simply-renamed/oldPath',
+ prevPath: 'root-folder/oldPath',
+ }),
+ });
+ });
+
+ it('renames entry, preserving old parameters', () => {
+ Object.assign(localState.entries.oldPath, {
+ url: `project/-/oldPath`,
+ });
+ const oldPathData = localState.entries.oldPath;
+
+ mutations.RENAME_ENTRY(localState, {
+ path: 'oldPath',
+ name: 'newPath',
+ parentPath: '',
+ });
+
expect(localState.entries.newPath).toEqual({
- ...localState.entries.oldPath,
+ ...oldPathData,
id: 'newPath',
- name: 'newPath',
- key: 'newPath-blob-oldPath',
path: 'newPath',
- tempFile: true,
+ name: 'newPath',
+ url: `project/-/newPath`,
+ key: jasmine.stringMatching('newPath'),
+
+ prevId: 'oldPath',
+ prevName: 'oldPath',
prevPath: 'oldPath',
- tree: [],
- parentPath: '',
- url: `${gl.TEST_HOST}/newPath`,
- moved: jasmine.anything(),
- movedPath: jasmine.anything(),
- opened: false,
+ prevUrl: `project/-/oldPath`,
+ prevKey: oldPathData.key,
+ prevParentPath: oldPathData.parentPath,
});
});
- it('adds new entry to changedFiles', () => {
- mutations.RENAME_ENTRY(localState, { path: 'oldPath', name: 'newPath' });
+ it('does not store previous attributes on temp files', () => {
+ Object.assign(localState.entries.oldPath, {
+ tempFile: true,
+ });
+ mutations.RENAME_ENTRY(localState, {
+ path: 'oldPath',
+ name: 'newPath',
+ entryPath: null,
+ parentPath: '',
+ });
- expect(localState.changedFiles.length).toBe(1);
- expect(localState.changedFiles[0].path).toBe('newPath');
- });
+ expect(localState.entries.newPath).not.toEqual(
+ jasmine.objectContaining({
+ prevId: jasmine.anything(),
+ prevName: jasmine.anything(),
+ prevPath: jasmine.anything(),
+ prevUrl: jasmine.anything(),
+ prevKey: jasmine.anything(),
+ prevParentPath: jasmine.anything(),
+ }),
+ );
+ });
+
+ it('properly handles files with spaces in name', () => {
+ const path = 'my fancy path';
+ const newPath = 'new path';
+ const oldEntry = {
+ ...file(path, path, 'blob'),
+ url: `project/-/${encodeURI(path)}`,
+ };
- it('sets oldEntry as moved', () => {
- mutations.RENAME_ENTRY(localState, { path: 'oldPath', name: 'newPath' });
+ localState.entries[path] = oldEntry;
- expect(localState.entries.oldPath.moved).toBe(true);
+ mutations.RENAME_ENTRY(localState, {
+ path,
+ name: newPath,
+ entryPath: null,
+ parentPath: '',
+ });
+
+ expect(localState.entries[newPath]).toEqual({
+ ...oldEntry,
+ id: newPath,
+ path: newPath,
+ name: newPath,
+ url: `project/-/new%20path`,
+ key: jasmine.stringMatching(newPath),
+
+ prevId: path,
+ prevName: path,
+ prevPath: path,
+ prevUrl: `project/-/my%20fancy%20path`,
+ prevKey: oldEntry.key,
+ prevParentPath: oldEntry.parentPath,
+ });
});
- it('adds to parents tree', () => {
- localState.entries.oldPath.parentPath = 'parentPath';
- localState.entries.parentPath = {
- ...file(),
+ it('adds to parent tree', () => {
+ const parentEntry = {
+ ...file('parentPath', 'parentPath', 'tree'),
+ tree: [localState.entries.oldPath],
};
+ localState.entries.parentPath = parentEntry;
mutations.RENAME_ENTRY(localState, {
path: 'oldPath',
@@ -416,7 +532,180 @@ describe('Multi-file store mutations', () => {
parentPath: 'parentPath',
});
- expect(localState.entries.parentPath.tree.length).toBe(1);
+ expect(parentEntry.tree.length).toBe(1);
+ expect(parentEntry.tree[0].name).toBe('newPath');
+ });
+
+ it('sorts tree after renaming an entry', () => {
+ const alpha = file('alpha', 'alpha', 'blob');
+ const beta = file('beta', 'beta', 'blob');
+ const gamma = file('gamma', 'gamma', 'blob');
+ localState.entries = { alpha, beta, gamma };
+
+ localState.trees['gitlab-ce/master'].tree = [alpha, beta, gamma];
+
+ mutations.RENAME_ENTRY(localState, {
+ path: 'alpha',
+ name: 'theta',
+ entryPath: null,
+ parentPath: '',
+ });
+
+ expect(localState.trees['gitlab-ce/master'].tree).toEqual([
+ jasmine.objectContaining({ name: 'beta' }),
+ jasmine.objectContaining({ name: 'gamma' }),
+ jasmine.objectContaining({
+ path: 'theta',
+ name: 'theta',
+ }),
+ ]);
+ });
+
+ it('updates openFiles with the renamed one if the original one is open', () => {
+ Object.assign(localState.entries.oldPath, {
+ opened: true,
+ type: 'blob',
+ });
+ Object.assign(localState, {
+ openFiles: [localState.entries.oldPath],
+ });
+
+ mutations.RENAME_ENTRY(localState, { path: 'oldPath', name: 'newPath' });
+
+ expect(localState.openFiles.length).toBe(1);
+ expect(localState.openFiles[0].path).toBe('newPath');
+ });
+
+ it('does not add renamed entry to changedFiles', () => {
+ mutations.RENAME_ENTRY(localState, { path: 'oldPath', name: 'newPath' });
+
+ expect(localState.changedFiles.length).toBe(0);
+ });
+
+ it('updates existing changedFiles entry with the renamed one', () => {
+ const origFile = {
+ ...file('oldPath', 'oldPath', 'blob'),
+ content: 'Foo',
+ };
+
+ Object.assign(localState, {
+ changedFiles: [origFile],
+ });
+ Object.assign(localState.entries, {
+ oldPath: origFile,
+ });
+
+ mutations.RENAME_ENTRY(localState, { path: 'oldPath', name: 'newPath' });
+
+ expect(localState.changedFiles).toEqual([
+ jasmine.objectContaining({
+ path: 'newPath',
+ content: 'Foo',
+ }),
+ ]);
+ });
+
+ it('correctly saves original values if an entry is renamed multiple times', () => {
+ const original = { ...localState.entries.oldPath };
+ const paramsToCheck = ['prevId', 'prevPath', 'prevName', 'prevUrl'];
+ const expectedObj = paramsToCheck.reduce(
+ (o, param) => ({ ...o, [param]: original[param.replace('prev', '').toLowerCase()] }),
+ {},
+ );
+
+ mutations.RENAME_ENTRY(localState, { path: 'oldPath', name: 'newPath' });
+
+ expect(localState.entries.newPath).toEqual(jasmine.objectContaining(expectedObj));
+
+ mutations.RENAME_ENTRY(localState, { path: 'newPath', name: 'newer' });
+
+ expect(localState.entries.newer).toEqual(jasmine.objectContaining(expectedObj));
+ });
+
+ describe('renaming back to original', () => {
+ beforeEach(() => {
+ const renamedEntry = {
+ ...file('renamed', 'renamed', 'blob'),
+ prevId: 'lorem/orig',
+ prevPath: 'lorem/orig',
+ prevName: 'orig',
+ prevUrl: 'project/-/loren/orig',
+ prevKey: 'lorem/orig',
+ prevParentPath: 'lorem',
+ };
+
+ localState.entries = {
+ renamed: renamedEntry,
+ };
+
+ mutations.RENAME_ENTRY(localState, { path: 'renamed', name: 'orig', parentPath: 'lorem' });
+ });
+
+ it('renames entry and clears prev properties', () => {
+ expect(localState.entries).toEqual({
+ 'lorem/orig': jasmine.objectContaining({
+ id: 'lorem/orig',
+ path: 'lorem/orig',
+ name: 'orig',
+ prevId: undefined,
+ prevPath: undefined,
+ prevName: undefined,
+ prevUrl: undefined,
+ prevKey: undefined,
+ prevParentPath: undefined,
+ }),
+ });
+ });
+ });
+
+ describe('key updates', () => {
+ beforeEach(() => {
+ const rootFolder = file('rootFolder', 'rootFolder', 'tree');
+ localState.entries = {
+ rootFolder,
+ oldPath: file('oldPath', 'oldPath', 'blob'),
+ 'oldPath.txt': file('oldPath.txt', 'oldPath.txt', 'blob'),
+ 'rootFolder/oldPath.md': file('oldPath.md', 'oldPath.md', 'blob', rootFolder),
+ };
+ });
+
+ it('sets properly constucted key while preserving the original one', () => {
+ const key = 'oldPath.txt-blob-oldPath.txt';
+ localState.entries['oldPath.txt'].key = key;
+ mutations.RENAME_ENTRY(localState, { path: 'oldPath.txt', name: 'newPath.md' });
+
+ expect(localState.entries['newPath.md'].key).toBe('newPath.md-blob-newPath.md');
+ expect(localState.entries['newPath.md'].prevKey).toBe(key);
+ });
+
+ it('correctly updates key for an entry without an extension', () => {
+ localState.entries.oldPath.key = 'oldPath-blob-oldPath';
+ mutations.RENAME_ENTRY(localState, { path: 'oldPath', name: 'newPath.md' });
+
+ expect(localState.entries['newPath.md'].key).toBe('newPath.md-blob-newPath.md');
+ });
+
+ it('correctly updates key when new name does not have an extension', () => {
+ localState.entries['oldPath.txt'].key = 'oldPath.txt-blob-oldPath.txt';
+ mutations.RENAME_ENTRY(localState, { path: 'oldPath.txt', name: 'newPath' });
+
+ expect(localState.entries.newPath.key).toBe('newPath-blob-newPath');
+ });
+
+ it('correctly updates key when renaming an entry in a folder', () => {
+ localState.entries['rootFolder/oldPath.md'].key =
+ 'rootFolder/oldPath.md-blob-rootFolder/oldPath.md';
+ mutations.RENAME_ENTRY(localState, {
+ path: 'rootFolder/oldPath.md',
+ name: 'newPath.md',
+ entryPath: null,
+ parentPath: 'rootFolder',
+ });
+
+ expect(localState.entries['rootFolder/newPath.md'].key).toBe(
+ 'rootFolder/newPath.md-blob-rootFolder/newPath.md',
+ );
+ });
});
});
});
diff --git a/spec/javascripts/ide/stores/utils_spec.js b/spec/javascripts/ide/stores/utils_spec.js
index 0fc9519a6bf..a477d4fc200 100644
--- a/spec/javascripts/ide/stores/utils_spec.js
+++ b/spec/javascripts/ide/stores/utils_spec.js
@@ -237,31 +237,6 @@ describe('Multi-file store utils', () => {
});
describe('getCommitFiles', () => {
- it('returns list of files excluding moved files', () => {
- const files = [
- {
- path: 'a',
- type: 'blob',
- deleted: true,
- },
- {
- path: 'c',
- type: 'blob',
- moved: true,
- },
- ];
-
- const flattendFiles = utils.getCommitFiles(files);
-
- expect(flattendFiles).toEqual([
- {
- path: 'a',
- type: 'blob',
- deleted: true,
- },
- ]);
- });
-
it('filters out folders from the list', () => {
const files = [
{
@@ -422,4 +397,204 @@ describe('Multi-file store utils', () => {
expect(res[1].tree[0].opened).toEqual(true);
});
});
+
+ describe('escapeFileUrl', () => {
+ it('encodes URL excluding the slashes', () => {
+ expect(utils.escapeFileUrl('/foo-bar/file.md')).toBe('/foo-bar/file.md');
+ expect(utils.escapeFileUrl('foo bar/file.md')).toBe('foo%20bar/file.md');
+ expect(utils.escapeFileUrl('foo/bar/file.md')).toBe('foo/bar/file.md');
+ });
+ });
+
+ describe('swapInStateArray', () => {
+ let localState;
+
+ beforeEach(() => {
+ localState = [];
+ });
+
+ it('swaps existing entry with a new one', () => {
+ const file1 = {
+ ...file('old'),
+ key: 'foo',
+ };
+ const file2 = file('new');
+ const arr = [file1];
+
+ Object.assign(localState, {
+ dummyArray: arr,
+ entries: {
+ new: file2,
+ },
+ });
+
+ utils.swapInStateArray(localState, 'dummyArray', 'foo', 'new');
+
+ expect(localState.dummyArray.length).toBe(1);
+ expect(localState.dummyArray[0]).toBe(file2);
+ });
+
+ it('does not add an item if it does not exist yet in array', () => {
+ const file1 = file('file');
+ Object.assign(localState, {
+ dummyArray: [],
+ entries: {
+ file: file1,
+ },
+ });
+
+ utils.swapInStateArray(localState, 'dummyArray', 'foo', 'file');
+
+ expect(localState.dummyArray.length).toBe(0);
+ });
+ });
+
+ describe('swapInParentTreeWithSorting', () => {
+ let localState;
+ let branchInfo;
+ const currentProjectId = '123-foo';
+ const currentBranchId = 'master';
+
+ beforeEach(() => {
+ localState = {
+ currentBranchId,
+ currentProjectId,
+ trees: {
+ [`${currentProjectId}/${currentBranchId}`]: {
+ tree: [],
+ },
+ },
+ entries: {
+ oldPath: file('oldPath', 'oldPath', 'blob'),
+ newPath: file('newPath', 'newPath', 'blob'),
+ parentPath: file('parentPath', 'parentPath', 'tree'),
+ },
+ };
+ branchInfo = localState.trees[`${currentProjectId}/${currentBranchId}`];
+ });
+
+ it('does not change tree if newPath is not supplied', () => {
+ branchInfo.tree = [localState.entries.oldPath];
+
+ utils.swapInParentTreeWithSorting(localState, 'oldPath', undefined, undefined);
+
+ expect(branchInfo.tree).toEqual([localState.entries.oldPath]);
+ });
+
+ describe('oldPath to replace is not defined: simple addition to tree', () => {
+ it('adds to tree on the state if there is no parent for the entry', () => {
+ expect(branchInfo.tree.length).toBe(0);
+
+ utils.swapInParentTreeWithSorting(localState, undefined, 'oldPath', undefined);
+
+ expect(branchInfo.tree.length).toBe(1);
+ expect(branchInfo.tree[0].name).toBe('oldPath');
+
+ utils.swapInParentTreeWithSorting(localState, undefined, 'newPath', undefined);
+
+ expect(branchInfo.tree.length).toBe(2);
+ expect(branchInfo.tree).toEqual([
+ jasmine.objectContaining({ name: 'newPath' }),
+ jasmine.objectContaining({ name: 'oldPath' }),
+ ]);
+ });
+
+ it('adds to parent tree if it is supplied', () => {
+ utils.swapInParentTreeWithSorting(localState, undefined, 'newPath', 'parentPath');
+
+ expect(localState.entries.parentPath.tree.length).toBe(1);
+ expect(localState.entries.parentPath.tree).toEqual([
+ jasmine.objectContaining({ name: 'newPath' }),
+ ]);
+
+ localState.entries.parentPath.tree = [localState.entries.oldPath];
+
+ utils.swapInParentTreeWithSorting(localState, undefined, 'newPath', 'parentPath');
+
+ expect(localState.entries.parentPath.tree.length).toBe(2);
+ expect(localState.entries.parentPath.tree).toEqual([
+ jasmine.objectContaining({ name: 'newPath' }),
+ jasmine.objectContaining({ name: 'oldPath' }),
+ ]);
+ });
+ });
+
+ describe('swapping of the items', () => {
+ it('swaps entries if both paths are supplied', () => {
+ branchInfo.tree = [localState.entries.oldPath];
+
+ utils.swapInParentTreeWithSorting(localState, localState.entries.oldPath.key, 'newPath');
+
+ expect(branchInfo.tree).toEqual([jasmine.objectContaining({ name: 'newPath' })]);
+
+ utils.swapInParentTreeWithSorting(localState, localState.entries.newPath.key, 'oldPath');
+
+ expect(branchInfo.tree).toEqual([jasmine.objectContaining({ name: 'oldPath' })]);
+ });
+
+ it('sorts tree after swapping the entries', () => {
+ const alpha = file('alpha', 'alpha', 'blob');
+ const beta = file('beta', 'beta', 'blob');
+ const gamma = file('gamma', 'gamma', 'blob');
+ const theta = file('theta', 'theta', 'blob');
+ localState.entries = { alpha, beta, gamma, theta };
+
+ branchInfo.tree = [alpha, beta, gamma];
+
+ utils.swapInParentTreeWithSorting(localState, alpha.key, 'theta');
+
+ expect(branchInfo.tree).toEqual([
+ jasmine.objectContaining({ name: 'beta' }),
+ jasmine.objectContaining({ name: 'gamma' }),
+ jasmine.objectContaining({ name: 'theta' }),
+ ]);
+
+ utils.swapInParentTreeWithSorting(localState, gamma.key, 'alpha');
+
+ expect(branchInfo.tree).toEqual([
+ jasmine.objectContaining({ name: 'alpha' }),
+ jasmine.objectContaining({ name: 'beta' }),
+ jasmine.objectContaining({ name: 'theta' }),
+ ]);
+
+ utils.swapInParentTreeWithSorting(localState, beta.key, 'gamma');
+
+ expect(branchInfo.tree).toEqual([
+ jasmine.objectContaining({ name: 'alpha' }),
+ jasmine.objectContaining({ name: 'gamma' }),
+ jasmine.objectContaining({ name: 'theta' }),
+ ]);
+ });
+ });
+ });
+
+ describe('cleanTrailingSlash', () => {
+ [
+ { input: '', output: '' },
+ { input: 'abc', output: 'abc' },
+ { input: 'abc/', output: 'abc' },
+ { input: 'abc/def', output: 'abc/def' },
+ { input: 'abc/def/', output: 'abc/def' },
+ ].forEach(({ input, output }) => {
+ it(`cleans trailing slash from string "${input}"`, () => {
+ expect(utils.cleanTrailingSlash(input)).toEqual(output);
+ });
+ });
+ });
+
+ describe('pathsAreEqual', () => {
+ [
+ { args: ['abc', 'abc'], output: true },
+ { args: ['abc', 'def'], output: false },
+ { args: ['abc/', 'abc'], output: true },
+ { args: ['abc/abc', 'abc'], output: false },
+ { args: ['/', ''], output: true },
+ { args: ['', '/'], output: true },
+ { args: [false, '/'], output: true },
+ ].forEach(({ args, output }) => {
+ it(`cleans and tests equality (${JSON.stringify(args)})`, () => {
+ expect(utils.pathsAreEqual(...args)).toEqual(output);
+ });
+ });
+ });
});
diff --git a/spec/javascripts/integrations/integration_settings_form_spec.js b/spec/javascripts/integrations/integration_settings_form_spec.js
index 069e2cb07b5..82d1f815ca8 100644
--- a/spec/javascripts/integrations/integration_settings_form_spec.js
+++ b/spec/javascripts/integrations/integration_settings_form_spec.js
@@ -126,6 +126,7 @@ describe('IntegrationSettingsForm', () => {
spyOn(axios, 'put').and.callThrough();
integrationSettingsForm = new IntegrationSettingsForm('.js-integration-settings-form');
+ // eslint-disable-next-line no-jquery/no-serialize
formData = integrationSettingsForm.$form.serialize();
});
diff --git a/spec/javascripts/issue_show/components/app_spec.js b/spec/javascripts/issue_show/components/app_spec.js
index 2770743937e..9fce040fd8c 100644
--- a/spec/javascripts/issue_show/components/app_spec.js
+++ b/spec/javascripts/issue_show/components/app_spec.js
@@ -1,3 +1,5 @@
+/* eslint-disable no-unused-vars */
+import GLDropdown from '~/gl_dropdown';
import Vue from 'vue';
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
@@ -21,6 +23,14 @@ describe('Issuable output', () => {
beforeEach(done => {
setFixtures(`
<div>
+ <div class="detail-page-description content-block">
+ <details open>
+ <summary>One</summary>
+ </details>
+ <details>
+ <summary>Two</summary>
+ </details>
+ </div>
<div class="flash-container"></div>
<span id="task_status"></span>
</div>
@@ -52,6 +62,7 @@ describe('Issuable output', () => {
markdownDocsPath: '/',
projectNamespace: '/',
projectPath: '/',
+ issuableTemplateNamesPath: '/issuable-templates-path',
},
}).$mount();
@@ -129,11 +140,11 @@ describe('Issuable output', () => {
});
it('does not update formState if form is already open', done => {
- vm.openForm();
+ vm.updateAndShowForm();
vm.state.titleText = 'testing 123';
- vm.openForm();
+ vm.updateAndShowForm();
Vue.nextTick(() => {
expect(vm.store.formState.title).not.toBe('testing 123');
@@ -284,7 +295,7 @@ describe('Issuable output', () => {
});
});
- it('shows error mesage from backend if exists', done => {
+ it('shows error message from backend if exists', done => {
const msg = 'Custom error message from backend';
spyOn(vm.service, 'updateIssuable').and.callFake(
// eslint-disable-next-line prefer-promise-reject-errors
@@ -405,20 +416,20 @@ describe('Issuable output', () => {
});
});
- describe('open form', () => {
+ describe('updateAndShowForm', () => {
it('shows locked warning if form is open & data is different', done => {
vm.$nextTick()
.then(() => {
- vm.openForm();
+ vm.updateAndShowForm();
vm.poll.makeRequest();
+
+ return new Promise(resolve => {
+ vm.$watch('formState.lockedWarningVisible', value => {
+ if (value) resolve();
+ });
+ });
})
- // Wait for the request
- .then(vm.$nextTick)
- // Wait for the successCallback to update the store state
- .then(vm.$nextTick)
- // Wait for the new state to flow to the Vue components
- .then(vm.$nextTick)
.then(() => {
expect(vm.formState.lockedWarningVisible).toEqual(true);
expect(vm.formState.lock_version).toEqual(1);
@@ -429,6 +440,41 @@ describe('Issuable output', () => {
});
});
+ describe('requestTemplatesAndShowForm', () => {
+ beforeEach(() => {
+ spyOn(vm, 'updateAndShowForm');
+ });
+
+ it('shows the form if template names request is successful', done => {
+ const mockData = [{ name: 'Bug' }];
+ mock.onGet('/issuable-templates-path').reply(() => Promise.resolve([200, mockData]));
+
+ vm.requestTemplatesAndShowForm()
+ .then(() => {
+ expect(vm.updateAndShowForm).toHaveBeenCalledWith(mockData);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('shows the form if template names request failed', done => {
+ mock
+ .onGet('/issuable-templates-path')
+ .reply(() => Promise.reject(new Error('something went wrong')));
+
+ vm.requestTemplatesAndShowForm()
+ .then(() => {
+ expect(document.querySelector('.flash-container .flash-text').textContent).toContain(
+ 'Error updating issue',
+ );
+
+ expect(vm.updateAndShowForm).toHaveBeenCalledWith();
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+
describe('show inline edit button', () => {
it('should not render by default', () => {
expect(vm.$el.querySelector('.title-container .note-action-button')).toBeDefined();
diff --git a/spec/javascripts/jobs/components/environments_block_spec.js b/spec/javascripts/jobs/components/environments_block_spec.js
index 4bbc5f5a348..64a59d659a7 100644
--- a/spec/javascripts/jobs/components/environments_block_spec.js
+++ b/spec/javascripts/jobs/components/environments_block_spec.js
@@ -2,6 +2,9 @@ import Vue from 'vue';
import component from '~/jobs/components/environments_block.vue';
import mountComponent from '../../helpers/vue_mount_component_helper';
+const TEST_CLUSTER_NAME = 'test_cluster';
+const TEST_CLUSTER_PATH = 'path/to/test_cluster';
+
describe('Environments block', () => {
const Component = Vue.extend(component);
let vm;
@@ -20,22 +23,53 @@ describe('Environments block', () => {
const lastDeployment = { iid: 'deployment', deployable: { build_path: 'bar' } };
+ const createEnvironmentWithLastDeployment = () => ({
+ ...environment,
+ last_deployment: { ...lastDeployment },
+ });
+
+ const createEnvironmentWithCluster = () => ({
+ ...environment,
+ last_deployment: {
+ ...lastDeployment,
+ cluster: { name: TEST_CLUSTER_NAME, path: TEST_CLUSTER_PATH },
+ },
+ });
+
+ const createComponent = (deploymentStatus = {}) => {
+ vm = mountComponent(Component, {
+ deploymentStatus,
+ iconStatus: status,
+ });
+ };
+
+ const findText = () => vm.$el.textContent.trim();
+ const findJobDeploymentLink = () => vm.$el.querySelector('.js-job-deployment-link');
+ const findEnvironmentLink = () => vm.$el.querySelector('.js-environment-link');
+ const findClusterLink = () => vm.$el.querySelector('.js-job-cluster-link');
+
afterEach(() => {
vm.$destroy();
});
describe('with last deployment', () => {
it('renders info for most recent deployment', () => {
- vm = mountComponent(Component, {
- deploymentStatus: {
- status: 'last',
- environment,
- },
- iconStatus: status,
+ createComponent({
+ status: 'last',
+ environment,
});
- expect(vm.$el.textContent.trim()).toEqual(
- 'This job is the most recent deployment to environment.',
+ expect(findText()).toEqual('This job is deployed to environment.');
+ });
+
+ it('renders info with cluster', () => {
+ createComponent({
+ status: 'last',
+ environment: createEnvironmentWithCluster(),
+ });
+
+ expect(findText()).toEqual(
+ `This job is deployed to environment using cluster ${TEST_CLUSTER_NAME}.`,
);
});
});
@@ -43,133 +77,106 @@ describe('Environments block', () => {
describe('with out of date deployment', () => {
describe('with last deployment', () => {
it('renders info for out date and most recent', () => {
- vm = mountComponent(Component, {
- deploymentStatus: {
- status: 'out_of_date',
- environment: Object.assign({}, environment, {
- last_deployment: lastDeployment,
- }),
- },
- iconStatus: status,
+ createComponent({
+ status: 'out_of_date',
+ environment: createEnvironmentWithLastDeployment(),
});
- expect(vm.$el.textContent.trim()).toEqual(
- 'This job is an out-of-date deployment to environment. View the most recent deployment #deployment.',
+ expect(findText()).toEqual(
+ 'This job is an out-of-date deployment to environment. View the most recent deployment.',
);
- expect(vm.$el.querySelector('.js-job-deployment-link').getAttribute('href')).toEqual('bar');
+ expect(findJobDeploymentLink().getAttribute('href')).toEqual('bar');
+ });
+
+ it('renders info with cluster', () => {
+ createComponent({
+ status: 'out_of_date',
+ environment: createEnvironmentWithCluster(),
+ });
+
+ expect(findText()).toEqual(
+ `This job is an out-of-date deployment to environment using cluster ${TEST_CLUSTER_NAME}. View the most recent deployment.`,
+ );
});
});
describe('without last deployment', () => {
it('renders info about out of date deployment', () => {
- vm = mountComponent(Component, {
- deploymentStatus: {
- status: 'out_of_date',
- environment,
- },
- iconStatus: status,
+ createComponent({
+ status: 'out_of_date',
+ environment,
});
- expect(vm.$el.textContent.trim()).toEqual(
- 'This job is an out-of-date deployment to environment.',
- );
+ expect(findText()).toEqual('This job is an out-of-date deployment to environment.');
});
});
});
describe('with failed deployment', () => {
it('renders info about failed deployment', () => {
- vm = mountComponent(Component, {
- deploymentStatus: {
- status: 'failed',
- environment,
- },
- iconStatus: status,
+ createComponent({
+ status: 'failed',
+ environment,
});
- expect(vm.$el.textContent.trim()).toEqual(
- 'The deployment of this job to environment did not succeed.',
- );
+ expect(findText()).toEqual('The deployment of this job to environment did not succeed.');
});
});
describe('creating deployment', () => {
describe('with last deployment', () => {
it('renders info about creating deployment and overriding latest deployment', () => {
- vm = mountComponent(Component, {
- deploymentStatus: {
- status: 'creating',
- environment: Object.assign({}, environment, {
- last_deployment: lastDeployment,
- }),
- },
- iconStatus: status,
+ createComponent({
+ status: 'creating',
+ environment: createEnvironmentWithLastDeployment(),
});
- expect(vm.$el.textContent.trim()).toEqual(
- 'This job is creating a deployment to environment and will overwrite the latest deployment.',
+ expect(findText()).toEqual(
+ 'This job is creating a deployment to environment. This will overwrite the latest deployment.',
);
- expect(vm.$el.querySelector('.js-job-deployment-link').getAttribute('href')).toEqual('bar');
+ expect(findJobDeploymentLink().getAttribute('href')).toEqual('bar');
+ expect(findEnvironmentLink().getAttribute('href')).toEqual(environment.environment_path);
+ expect(findClusterLink()).toBeNull();
});
});
describe('without last deployment', () => {
it('renders info about failed deployment', () => {
- vm = mountComponent(Component, {
- deploymentStatus: {
- status: 'creating',
- environment,
- },
- iconStatus: status,
+ createComponent({
+ status: 'creating',
+ environment,
});
- expect(vm.$el.textContent.trim()).toEqual(
- 'This job is creating a deployment to environment.',
- );
+ expect(findText()).toEqual('This job is creating a deployment to environment.');
});
});
describe('without environment', () => {
it('does not render environment link', () => {
- vm = mountComponent(Component, {
- deploymentStatus: {
- status: 'creating',
- environment: null,
- },
- iconStatus: status,
+ createComponent({
+ status: 'creating',
+ environment: null,
});
- expect(vm.$el.querySelector('.js-environment-link')).toBeNull();
+ expect(findEnvironmentLink()).toBeNull();
});
});
});
describe('with a cluster', () => {
it('renders the cluster link', () => {
- const cluster = {
- name: 'the-cluster',
- path: '/the-cluster-path',
- };
- vm = mountComponent(Component, {
- deploymentStatus: {
- status: 'last',
- environment: Object.assign({}, environment, {
- last_deployment: {
- ...lastDeployment,
- cluster,
- },
- }),
- },
- iconStatus: status,
+ createComponent({
+ status: 'last',
+ environment: createEnvironmentWithCluster(),
});
- expect(vm.$el.textContent.trim()).toContain('Cluster the-cluster was used.');
-
- expect(vm.$el.querySelector('.js-job-cluster-link').getAttribute('href')).toEqual(
- '/the-cluster-path',
+ expect(findText()).toEqual(
+ `This job is deployed to environment using cluster ${TEST_CLUSTER_NAME}.`,
);
+
+ expect(findClusterLink().getAttribute('href')).toEqual(TEST_CLUSTER_PATH);
});
describe('when the cluster is missing the path', () => {
@@ -177,39 +184,20 @@ describe('Environments block', () => {
const cluster = {
name: 'the-cluster',
};
- vm = mountComponent(Component, {
- deploymentStatus: {
- status: 'last',
- environment: Object.assign({}, environment, {
- last_deployment: {
- ...lastDeployment,
- cluster,
- },
- }),
- },
- iconStatus: status,
- });
-
- expect(vm.$el.textContent.trim()).toContain('Cluster the-cluster was used.');
-
- expect(vm.$el.querySelector('.js-job-cluster-link')).toBeNull();
- });
- });
- });
-
- describe('without a cluster', () => {
- it('does not render a cluster link', () => {
- vm = mountComponent(Component, {
- deploymentStatus: {
+ createComponent({
status: 'last',
environment: Object.assign({}, environment, {
- last_deployment: lastDeployment,
+ last_deployment: {
+ ...lastDeployment,
+ cluster,
+ },
}),
- },
- iconStatus: status,
- });
+ });
+
+ expect(findText()).toContain('using cluster the-cluster.');
- expect(vm.$el.querySelector('.js-job-cluster-link')).toBeNull();
+ expect(findClusterLink()).toBeNull();
+ });
});
});
});
diff --git a/spec/javascripts/jobs/components/job_log_spec.js b/spec/javascripts/jobs/components/job_log_spec.js
index 24bb6b9a48b..dd58f234394 100644
--- a/spec/javascripts/jobs/components/job_log_spec.js
+++ b/spec/javascripts/jobs/components/job_log_spec.js
@@ -3,7 +3,6 @@ import component from '~/jobs/components/job_log.vue';
import createStore from '~/jobs/store';
import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import { resetStore } from '../store/helpers';
-import { logWithCollapsibleSections } from '../mock_data';
describe('Job Log', () => {
const Component = Vue.extend(component);
@@ -63,60 +62,4 @@ describe('Job Log', () => {
expect(vm.$el.querySelector('.js-log-animation')).toBeNull();
});
});
-
- describe('Collapsible sections', () => {
- beforeEach(() => {
- vm = mountComponentWithStore(Component, {
- props: {
- trace: logWithCollapsibleSections.html,
- isComplete: true,
- },
- store,
- });
- });
-
- it('renders open arrow', () => {
- expect(vm.$el.querySelector('.fa-caret-down')).not.toBeNull();
- });
-
- it('toggles hidden class to the sibilings rows when arrow is clicked', done => {
- vm.$nextTick()
- .then(() => {
- const { section } = vm.$el.querySelector('.js-section-start').dataset;
- vm.$el.querySelector('.js-section-start').click();
-
- vm.$el.querySelectorAll(`.js-s-${section}:not(.js-section-header)`).forEach(el => {
- expect(el.classList.contains('hidden')).toEqual(true);
- });
-
- vm.$el.querySelector('.js-section-start').click();
-
- vm.$el.querySelectorAll(`.js-s-${section}:not(.js-section-header)`).forEach(el => {
- expect(el.classList.contains('hidden')).toEqual(false);
- });
- })
- .then(done)
- .catch(done.fail);
- });
-
- it('toggles hidden class to the sibilings rows when header section is clicked', done => {
- vm.$nextTick()
- .then(() => {
- const { section } = vm.$el.querySelector('.js-section-header').dataset;
- vm.$el.querySelector('.js-section-header').click();
-
- vm.$el.querySelectorAll(`.js-s-${section}:not(.js-section-header)`).forEach(el => {
- expect(el.classList.contains('hidden')).toEqual(true);
- });
-
- vm.$el.querySelector('.js-section-header').click();
-
- vm.$el.querySelectorAll(`.js-s-${section}:not(.js-section-header)`).forEach(el => {
- expect(el.classList.contains('hidden')).toEqual(false);
- });
- })
- .then(done)
- .catch(done.fail);
- });
- });
});
diff --git a/spec/javascripts/labels_issue_sidebar_spec.js b/spec/javascripts/labels_issue_sidebar_spec.js
index ccf439aac74..5ae5643aefc 100644
--- a/spec/javascripts/labels_issue_sidebar_spec.js
+++ b/spec/javascripts/labels_issue_sidebar_spec.js
@@ -5,6 +5,7 @@ import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import IssuableContext from '~/issuable_context';
import LabelsSelect from '~/labels_select';
+import _ from 'underscore';
import '~/gl_dropdown';
import 'select2';
@@ -15,6 +16,35 @@ import '~/users_select';
let saveLabelCount = 0;
let mock;
+function testLabelClicks(labelOrder, done) {
+ $('.edit-link')
+ .get(0)
+ .click();
+
+ setTimeout(() => {
+ const labelsInDropdown = $('.dropdown-content a');
+
+ expect(labelsInDropdown.length).toBe(10);
+
+ const arrayOfLabels = labelsInDropdown.get();
+ const randomArrayOfLabels = _.shuffle(arrayOfLabels);
+ randomArrayOfLabels.forEach((label, i) => {
+ if (i < saveLabelCount) {
+ $(label).click();
+ }
+ });
+
+ $('.edit-link')
+ .get(0)
+ .click();
+
+ setTimeout(() => {
+ expect($('.sidebar-collapsed-icon').attr('data-original-title')).toBe(labelOrder);
+ done();
+ }, 0);
+ }, 0);
+}
+
describe('Issue dropdown sidebar', () => {
preloadFixtures('static/issue_sidebar_label.html');
@@ -29,7 +59,7 @@ describe('Issue dropdown sidebar', () => {
mock.onGet('/root/test/labels.json').reply(() => {
const labels = Array(10)
.fill()
- .map((_, i) => ({
+ .map((_val, i) => ({
id: i,
title: `test ${i}`,
color: '#5CB85C',
@@ -41,7 +71,7 @@ describe('Issue dropdown sidebar', () => {
mock.onPut('/root/test/issues/2.json').reply(() => {
const labels = Array(saveLabelCount)
.fill()
- .map((_, i) => ({
+ .map((_val, i) => ({
id: i,
title: `test ${i}`,
color: '#5CB85C',
@@ -57,61 +87,11 @@ describe('Issue dropdown sidebar', () => {
it('changes collapsed tooltip when changing labels when less than 5', done => {
saveLabelCount = 5;
- $('.edit-link')
- .get(0)
- .click();
-
- setTimeout(() => {
- expect($('.dropdown-content a').length).toBe(10);
-
- $('.dropdown-content a').each(function(i) {
- if (i < saveLabelCount) {
- $(this)
- .get(0)
- .click();
- }
- });
-
- $('.edit-link')
- .get(0)
- .click();
-
- setTimeout(() => {
- expect($('.sidebar-collapsed-icon').attr('data-original-title')).toBe(
- 'test 0, test 1, test 2, test 3, test 4',
- );
- done();
- }, 0);
- }, 0);
+ testLabelClicks('test 0, test 1, test 2, test 3, test 4', done);
});
it('changes collapsed tooltip when changing labels when more than 5', done => {
saveLabelCount = 6;
- $('.edit-link')
- .get(0)
- .click();
-
- setTimeout(() => {
- expect($('.dropdown-content a').length).toBe(10);
-
- $('.dropdown-content a').each(function(i) {
- if (i < saveLabelCount) {
- $(this)
- .get(0)
- .click();
- }
- });
-
- $('.edit-link')
- .get(0)
- .click();
-
- setTimeout(() => {
- expect($('.sidebar-collapsed-icon').attr('data-original-title')).toBe(
- 'test 0, test 1, test 2, test 3, test 4, and 1 more',
- );
- done();
- }, 0);
- }, 0);
+ testLabelClicks('test 0, test 1, test 2, test 3, test 4, and 1 more', done);
});
});
diff --git a/spec/javascripts/lazy_loader_spec.js b/spec/javascripts/lazy_loader_spec.js
index f3fb792c62d..82ab73c2170 100644
--- a/spec/javascripts/lazy_loader_spec.js
+++ b/spec/javascripts/lazy_loader_spec.js
@@ -62,7 +62,7 @@ describe('LazyLoader', function() {
waitForAttributeChange(newImg, ['data-src', 'src']),
])
.then(() => {
- expect(LazyLoader.loadImage).toHaveBeenCalled();
+ expect(LazyLoader.loadImage).toHaveBeenCalledWith(newImg);
expect(newImg.getAttribute('src')).toBe(testPath);
expect(newImg).toHaveClass('js-lazy-loaded');
done();
@@ -79,7 +79,7 @@ describe('LazyLoader', function() {
scrollIntoViewPromise(newImg)
.then(waitForPromises)
.then(() => {
- expect(LazyLoader.loadImage).not.toHaveBeenCalled();
+ expect(LazyLoader.loadImage).not.toHaveBeenCalledWith(newImg);
expect(newImg).not.toHaveClass('js-lazy-loaded');
done();
})
@@ -98,7 +98,7 @@ describe('LazyLoader', function() {
scrollIntoViewPromise(newImg)
.then(waitForPromises)
.then(() => {
- expect(LazyLoader.loadImage).not.toHaveBeenCalled();
+ expect(LazyLoader.loadImage).not.toHaveBeenCalledWith(newImg);
expect(newImg).not.toHaveClass('js-lazy-loaded');
done();
})
@@ -121,7 +121,7 @@ describe('LazyLoader', function() {
])
.then(waitForPromises)
.then(() => {
- expect(LazyLoader.loadImage).toHaveBeenCalled();
+ expect(LazyLoader.loadImage).toHaveBeenCalledWith(newImg);
expect(newImg).toHaveClass('js-lazy-loaded');
done();
})
@@ -156,7 +156,7 @@ describe('LazyLoader', function() {
Promise.all([scrollIntoViewPromise(img), waitForAttributeChange(img, ['data-src', 'src'])])
.then(() => {
- expect(LazyLoader.loadImage).toHaveBeenCalled();
+ expect(LazyLoader.loadImage).toHaveBeenCalledWith(img);
expect(img.getAttribute('src')).toBe(originalDataSrc);
expect(img).toHaveClass('js-lazy-loaded');
done();
@@ -176,7 +176,7 @@ describe('LazyLoader', function() {
waitForAttributeChange(newImg, ['data-src', 'src']),
])
.then(() => {
- expect(LazyLoader.loadImage).toHaveBeenCalled();
+ expect(LazyLoader.loadImage).toHaveBeenCalledWith(newImg);
expect(newImg.getAttribute('src')).toBe(testPath);
expect(newImg).toHaveClass('js-lazy-loaded');
done();
@@ -193,7 +193,7 @@ describe('LazyLoader', function() {
scrollIntoViewPromise(newImg)
.then(waitForPromises)
.then(() => {
- expect(LazyLoader.loadImage).not.toHaveBeenCalled();
+ expect(LazyLoader.loadImage).not.toHaveBeenCalledWith(newImg);
expect(newImg).not.toHaveClass('js-lazy-loaded');
done();
})
@@ -212,7 +212,7 @@ describe('LazyLoader', function() {
scrollIntoViewPromise(newImg)
.then(waitForPromises)
.then(() => {
- expect(LazyLoader.loadImage).not.toHaveBeenCalled();
+ expect(LazyLoader.loadImage).not.toHaveBeenCalledWith(newImg);
expect(newImg).not.toHaveClass('js-lazy-loaded');
done();
})
@@ -234,7 +234,7 @@ describe('LazyLoader', function() {
waitForAttributeChange(newImg, ['data-src', 'src']),
])
.then(() => {
- expect(LazyLoader.loadImage).toHaveBeenCalled();
+ expect(LazyLoader.loadImage).toHaveBeenCalledWith(newImg);
expect(newImg).toHaveClass('js-lazy-loaded');
done();
})
diff --git a/spec/javascripts/lib/utils/common_utils_spec.js b/spec/javascripts/lib/utils/common_utils_spec.js
index 85949f2ae86..8956bc92e6b 100644
--- a/spec/javascripts/lib/utils/common_utils_spec.js
+++ b/spec/javascripts/lib/utils/common_utils_spec.js
@@ -943,4 +943,14 @@ describe('common_utils', () => {
expect(commonUtils.isScopedLabel({ title: 'foobar' })).toBe(false);
});
});
+
+ describe('getDashPath', () => {
+ it('returns the path following /-/', () => {
+ expect(commonUtils.getDashPath('/some/-/url-with-dashes-/')).toEqual('url-with-dashes-/');
+ });
+
+ it('returns null when no path follows /-/', () => {
+ expect(commonUtils.getDashPath('/some/url')).toEqual(null);
+ });
+ });
});
diff --git a/spec/javascripts/line_highlighter_spec.js b/spec/javascripts/line_highlighter_spec.js
index a75470b4db8..f8f835ffdef 100644
--- a/spec/javascripts/line_highlighter_spec.js
+++ b/spec/javascripts/line_highlighter_spec.js
@@ -1,4 +1,4 @@
-/* eslint-disable no-var, prefer-template, no-else-return, dot-notation, no-return-assign, no-new, no-underscore-dangle */
+/* eslint-disable no-var, no-else-return, dot-notation, no-return-assign, no-new, no-underscore-dangle */
import $ from 'jquery';
import LineHighlighter from '~/line_highlighter';
@@ -8,10 +8,10 @@ describe('LineHighlighter', function() {
preloadFixtures('static/line_highlighter.html');
clickLine = function(number, eventData = {}) {
if ($.isEmptyObject(eventData)) {
- return $('#L' + number).click();
+ return $(`#L${number}`).click();
} else {
const e = $.Event('click', eventData);
- return $('#L' + number).trigger(e);
+ return $(`#L${number}`).trigger(e);
}
};
beforeEach(function() {
@@ -42,9 +42,9 @@ describe('LineHighlighter', function() {
var line;
new LineHighlighter({ hash: '#L5-25' });
- expect($('.' + this.css).length).toBe(21);
+ expect($(`.${this.css}`).length).toBe(21);
for (line = 5; line <= 25; line += 1) {
- expect($('#LC' + line)).toHaveClass(this.css);
+ expect($(`#LC${line}`)).toHaveClass(this.css);
}
});
@@ -130,7 +130,7 @@ describe('LineHighlighter', function() {
});
expect($('#LC13')).toHaveClass(this.css);
- expect($('.' + this.css).length).toBe(1);
+ expect($(`.${this.css}`).length).toBe(1);
});
it('sets the hash', function() {
@@ -152,9 +152,9 @@ describe('LineHighlighter', function() {
shiftKey: true,
});
- expect($('.' + this.css).length).toBe(6);
+ expect($(`.${this.css}`).length).toBe(6);
for (line = 15; line <= 20; line += 1) {
- expect($('#LC' + line)).toHaveClass(this.css);
+ expect($(`#LC${line}`)).toHaveClass(this.css);
}
});
@@ -165,9 +165,9 @@ describe('LineHighlighter', function() {
shiftKey: true,
});
- expect($('.' + this.css).length).toBe(6);
+ expect($(`.${this.css}`).length).toBe(6);
for (line = 5; line <= 10; line += 1) {
- expect($('#LC' + line)).toHaveClass(this.css);
+ expect($(`#LC${line}`)).toHaveClass(this.css);
}
});
});
@@ -188,9 +188,9 @@ describe('LineHighlighter', function() {
shiftKey: true,
});
- expect($('.' + this.css).length).toBe(6);
+ expect($(`.${this.css}`).length).toBe(6);
for (line = 5; line <= 10; line += 1) {
- expect($('#LC' + line)).toHaveClass(this.css);
+ expect($(`#LC${line}`)).toHaveClass(this.css);
}
});
@@ -200,9 +200,9 @@ describe('LineHighlighter', function() {
shiftKey: true,
});
- expect($('.' + this.css).length).toBe(6);
+ expect($(`.${this.css}`).length).toBe(6);
for (line = 10; line <= 15; line += 1) {
- expect($('#LC' + line)).toHaveClass(this.css);
+ expect($(`#LC${line}`)).toHaveClass(this.css);
}
});
});
diff --git a/spec/javascripts/monitoring/charts/time_series_spec.js b/spec/javascripts/monitoring/charts/time_series_spec.js
index f6a5ed03c0d..5c718135b90 100644
--- a/spec/javascripts/monitoring/charts/time_series_spec.js
+++ b/spec/javascripts/monitoring/charts/time_series_spec.js
@@ -60,6 +60,18 @@ describe('Time series component', () => {
expect(timeSeriesChart.find('.js-graph-widgets').text()).toBe(mockWidgets);
});
+ it('allows user to override max value label text using prop', () => {
+ timeSeriesChart.setProps({ legendMaxText: 'legendMaxText' });
+
+ expect(timeSeriesChart.props().legendMaxText).toBe('legendMaxText');
+ });
+
+ it('allows user to override average value label text using prop', () => {
+ timeSeriesChart.setProps({ legendAverageText: 'averageText' });
+
+ expect(timeSeriesChart.props().legendAverageText).toBe('averageText');
+ });
+
describe('methods', () => {
describe('formatTooltipText', () => {
const mockDate = deploymentData[0].created_at;
@@ -140,6 +152,16 @@ describe('Time series component', () => {
expect(timeSeriesChart.vm.svgs[mockSvgName]).toBe(`path://${mockSvgPathContent}`);
});
});
+
+ it('contains an svg object within an array to properly render icon', () => {
+ timeSeriesChart.vm.$nextTick(() => {
+ expect(timeSeriesChart.vm.chartOptions.dataZoom).toEqual([
+ {
+ handleIcon: `path://${mockSvgPathContent}`,
+ },
+ ]);
+ });
+ });
});
describe('onResize', () => {
diff --git a/spec/javascripts/monitoring/components/dashboard_spec.js b/spec/javascripts/monitoring/components/dashboard_spec.js
index 6ce32d21f45..75df2ce3103 100644
--- a/spec/javascripts/monitoring/components/dashboard_spec.js
+++ b/spec/javascripts/monitoring/components/dashboard_spec.js
@@ -1,9 +1,9 @@
import Vue from 'vue';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import { GlToast } from '@gitlab/ui';
+import VueDraggable from 'vuedraggable';
import MockAdapter from 'axios-mock-adapter';
import Dashboard from '~/monitoring/components/dashboard.vue';
-import { timeWindows, timeWindowsKeyNames } from '~/monitoring/constants';
import * as types from '~/monitoring/stores/mutation_types';
import { createStore } from '~/monitoring/stores';
import axios from '~/lib/utils/axios_utils';
@@ -36,6 +36,12 @@ const propsData = {
validateQueryPath: '',
};
+const resetSpy = spy => {
+ if (spy) {
+ spy.calls.reset();
+ }
+};
+
export default propsData;
describe('Dashboard', () => {
@@ -51,11 +57,6 @@ describe('Dashboard', () => {
<div class="layout-page"></div>
`);
- window.gon = {
- ...window.gon,
- ee: false,
- };
-
store = createStore();
mock = new MockAdapter(axios);
DashboardComponent = Vue.extend(Dashboard);
@@ -100,10 +101,15 @@ describe('Dashboard', () => {
});
describe('requests information to the server', () => {
+ let spy;
beforeEach(() => {
mock.onGet(mockApiEndpoint).reply(200, metricsGroupsAPIResponse);
});
+ afterEach(() => {
+ resetSpy(spy);
+ });
+
it('shows up a loading state', done => {
component = new DashboardComponent({
el: document.querySelector('.prometheus-graphs'),
@@ -276,7 +282,7 @@ describe('Dashboard', () => {
});
});
- it('renders the time window dropdown with a set of options', done => {
+ it('renders the datetimepicker dropdown', done => {
component = new DashboardComponent({
el: document.querySelector('.prometheus-graphs'),
propsData: {
@@ -286,17 +292,9 @@ describe('Dashboard', () => {
},
store,
});
- const numberOfTimeWindows = Object.keys(timeWindows).length;
setTimeout(() => {
- const timeWindowDropdown = component.$el.querySelector('.js-time-window-dropdown');
- const timeWindowDropdownEls = component.$el.querySelectorAll(
- '.js-time-window-dropdown .dropdown-item',
- );
-
- expect(timeWindowDropdown).not.toBeNull();
- expect(timeWindowDropdownEls.length).toEqual(numberOfTimeWindows);
-
+ expect(component.$el.querySelector('.js-time-window-dropdown')).not.toBeNull();
done();
});
});
@@ -333,8 +331,8 @@ describe('Dashboard', () => {
});
it('shows a specific time window selected from the url params', done => {
- const start = 1564439536;
- const end = 1564441336;
+ const start = '2019-10-01T18:27:47.000Z';
+ const end = '2019-10-01T18:57:47.000Z';
spyOnDependency(Dashboard, 'getTimeDiff').and.returnValue({
start,
end,
@@ -359,7 +357,7 @@ describe('Dashboard', () => {
});
});
- it('defaults to the eight hours time window for non valid url parameters', done => {
+ it('shows an error message if invalid url parameters are passed', done => {
spyOnDependency(Dashboard, 'getParameterValues').and.returnValue([
'<script>alert("XSS")</script>',
]);
@@ -370,15 +368,111 @@ describe('Dashboard', () => {
store,
});
- Vue.nextTick(() => {
- expect(component.selectedTimeWindowKey).toEqual(timeWindowsKeyNames.eightHours);
+ spy = spyOn(component, 'showInvalidDateError');
+ component.$mount();
+ component.$nextTick(() => {
+ expect(component.showInvalidDateError).toHaveBeenCalled();
done();
});
});
});
- // https://gitlab.com/gitlab-org/gitlab-foss/issues/66922
+ describe('drag and drop function', () => {
+ let wrapper;
+ let expectedPanelCount; // also called metrics, naming to be improved: https://gitlab.com/gitlab-org/gitlab/issues/31565
+ const findDraggables = () => wrapper.findAll(VueDraggable);
+ const findEnabledDraggables = () => findDraggables().filter(f => !f.attributes('disabled'));
+ const findDraggablePanels = () => wrapper.findAll('.js-draggable-panel');
+ const findRearrangeButton = () => wrapper.find('.js-rearrange-button');
+
+ beforeEach(done => {
+ mock.onGet(mockApiEndpoint).reply(200, metricsGroupsAPIResponse);
+ expectedPanelCount = metricsGroupsAPIResponse.data.reduce(
+ (acc, d) => d.metrics.length + acc,
+ 0,
+ );
+ store.dispatch('monitoringDashboard/setFeatureFlags', { additionalPanelTypesEnabled: true });
+
+ wrapper = shallowMount(DashboardComponent, {
+ localVue,
+ sync: false,
+ propsData: { ...propsData, hasMetrics: true },
+ store,
+ });
+
+ // not using $nextTicket becuase we must wait for the dashboard
+ // to be populated with the mock data results.
+ setTimeout(done);
+ });
+
+ it('wraps vuedraggable', () => {
+ expect(findDraggablePanels().exists()).toBe(true);
+ expect(findDraggablePanels().length).toEqual(expectedPanelCount);
+ });
+
+ it('is disabled by default', () => {
+ expect(findRearrangeButton().exists()).toBe(false);
+ expect(findEnabledDraggables().length).toBe(0);
+ });
+
+ describe('when rearrange is enabled', () => {
+ beforeEach(done => {
+ wrapper.setProps({ rearrangePanelsAvailable: true });
+ wrapper.vm.$nextTick(done);
+ });
+
+ it('displays rearrange button', () => {
+ expect(findRearrangeButton().exists()).toBe(true);
+ });
+
+ describe('when rearrange button is clicked', () => {
+ const findFirstDraggableRemoveButton = () =>
+ findDraggablePanels()
+ .at(0)
+ .find('.js-draggable-remove');
+
+ beforeEach(done => {
+ findRearrangeButton().vm.$emit('click');
+ wrapper.vm.$nextTick(done);
+ });
+
+ it('it enables draggables', () => {
+ expect(findRearrangeButton().attributes('pressed')).toBeTruthy();
+ expect(findEnabledDraggables()).toEqual(findDraggables());
+ });
+
+ it('shows a remove button, which removes a panel', done => {
+ expect(findFirstDraggableRemoveButton().isEmpty()).toBe(false);
+
+ expect(findDraggablePanels().length).toEqual(expectedPanelCount);
+ findFirstDraggableRemoveButton().trigger('click');
+
+ wrapper.vm.$nextTick(() => {
+ // At present graphs will not be removed in backend
+ // See https://gitlab.com/gitlab-org/gitlab/issues/27835
+ expect(findDraggablePanels().length).toEqual(expectedPanelCount - 1);
+ done();
+ });
+ });
+
+ it('it disables draggables when clicked again', done => {
+ findRearrangeButton().vm.$emit('click');
+ wrapper.vm.$nextTick(() => {
+ expect(findRearrangeButton().attributes('pressed')).toBeFalsy();
+ expect(findEnabledDraggables().length).toBe(0);
+ done();
+ });
+ });
+ });
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+ });
+
+ // https://gitlab.com/gitlab-org/gitlab-ce/issues/66922
// eslint-disable-next-line jasmine/no-disabled-tests
xdescribe('link to chart', () => {
let wrapper;
@@ -527,7 +621,6 @@ describe('Dashboard', () => {
component.$store.dispatch('monitoringDashboard/setFeatureFlags', {
prometheusEndpoint: false,
- multipleDashboardsEnabled: true,
});
component.$store.commit(
diff --git a/spec/javascripts/monitoring/store/actions_spec.js b/spec/javascripts/monitoring/store/actions_spec.js
index 955a39e03a5..1bd74f59282 100644
--- a/spec/javascripts/monitoring/store/actions_spec.js
+++ b/spec/javascripts/monitoring/store/actions_spec.js
@@ -240,8 +240,6 @@ describe('Monitoring store actions', () => {
const response = metricsDashboardResponse;
response.all_dashboards = dashboardGitResponse;
- state.multipleDashboardsEnabled = true;
-
receiveMetricsDashboardSuccess({ state, commit, dispatch }, { response, params });
expect(commit).toHaveBeenCalledWith(types.SET_ALL_DASHBOARDS, dashboardGitResponse);
diff --git a/spec/javascripts/monitoring/store/mutations_spec.js b/spec/javascripts/monitoring/store/mutations_spec.js
index bdb68a80a8a..bdddd83358c 100644
--- a/spec/javascripts/monitoring/store/mutations_spec.js
+++ b/spec/javascripts/monitoring/store/mutations_spec.js
@@ -7,6 +7,7 @@ import {
metricsDashboardResponse,
dashboardGitResponse,
} from '../mock_data';
+import { uniqMetricsId } from '~/monitoring/stores/utils';
describe('Monitoring mutations', () => {
let stateCopy;
@@ -128,6 +129,7 @@ describe('Monitoring mutations', () => {
describe('SET_QUERY_RESULT', () => {
const metricId = 12;
+ const id = 'system_metrics_kubernetes_container_memory_total';
const result = [{ values: [[0, 1], [1, 1], [1, 3]] }];
beforeEach(() => {
@@ -146,12 +148,13 @@ describe('Monitoring mutations', () => {
});
it('sets metricsWithData value', () => {
+ const uniqId = uniqMetricsId({ metric_id: metricId, id });
mutations[types.SET_QUERY_RESULT](stateCopy, {
- metricId,
+ metricId: uniqId,
result,
});
- expect(stateCopy.metricsWithData).toEqual([12]);
+ expect(stateCopy.metricsWithData).toEqual([uniqId]);
});
it('does not store empty results', () => {
diff --git a/spec/javascripts/monitoring/store/utils_spec.js b/spec/javascripts/monitoring/store/utils_spec.js
index 73dd370ffb3..98388ac19f8 100644
--- a/spec/javascripts/monitoring/store/utils_spec.js
+++ b/spec/javascripts/monitoring/store/utils_spec.js
@@ -1,4 +1,4 @@
-import { groupQueriesByChartInfo } from '~/monitoring/stores/utils';
+import { groupQueriesByChartInfo, normalizeMetric, uniqMetricsId } from '~/monitoring/stores/utils';
describe('groupQueriesByChartInfo', () => {
let input;
@@ -12,7 +12,11 @@ describe('groupQueriesByChartInfo', () => {
];
output = [
- { title: 'title', y_label: 'MB', queries: [{ metricId: null }, { metricId: null }] },
+ {
+ title: 'title',
+ y_label: 'MB',
+ queries: [{ metricId: null }, { metricId: null }],
+ },
{ title: 'new title', y_label: 'MB', queries: [{ metricId: null }] },
];
@@ -35,3 +39,36 @@ describe('groupQueriesByChartInfo', () => {
expect(groupQueriesByChartInfo(input)).toEqual(output);
});
});
+
+describe('normalizeMetric', () => {
+ [
+ { args: [], expected: 'undefined_undefined' },
+ { args: [undefined], expected: 'undefined_undefined' },
+ { args: [{ id: 'something' }], expected: 'undefined_something' },
+ { args: [{ id: 45 }], expected: 'undefined_45' },
+ { args: [{ metric_id: 5 }], expected: '5_undefined' },
+ { args: [{ metric_id: 'something' }], expected: 'something_undefined' },
+ {
+ args: [{ metric_id: 5, id: 'system_metrics_kubernetes_container_memory_total' }],
+ expected: '5_system_metrics_kubernetes_container_memory_total',
+ },
+ ].forEach(({ args, expected }) => {
+ it(`normalizes metric to "${expected}" with args=${JSON.stringify(args)}`, () => {
+ expect(normalizeMetric(...args)).toEqual({ metric_id: expected });
+ });
+ });
+});
+
+describe('uniqMetricsId', () => {
+ [
+ { input: { id: 1 }, expected: 'undefined_1' },
+ { input: { metric_id: 2 }, expected: '2_undefined' },
+ { input: { metric_id: 2, id: 21 }, expected: '2_21' },
+ { input: { metric_id: 22, id: 1 }, expected: '22_1' },
+ { input: { metric_id: 'aaa', id: '_a' }, expected: 'aaa__a' },
+ ].forEach(({ input, expected }) => {
+ it(`creates unique metric ID with ${JSON.stringify(input)}`, () => {
+ expect(uniqMetricsId(input)).toEqual(expected);
+ });
+ });
+});
diff --git a/spec/javascripts/monitoring/utils_spec.js b/spec/javascripts/monitoring/utils_spec.js
index e22e8cdc03d..512dd2a0eb3 100644
--- a/spec/javascripts/monitoring/utils_spec.js
+++ b/spec/javascripts/monitoring/utils_spec.js
@@ -1,5 +1,14 @@
-import { getTimeDiff, graphDataValidatorForValues } from '~/monitoring/utils';
-import { timeWindows } from '~/monitoring/constants';
+import {
+ getTimeDiff,
+ getTimeWindow,
+ graphDataValidatorForValues,
+ isDateTimePickerInputValid,
+ truncateZerosInDateTime,
+ stringToISODate,
+ ISODateToString,
+ isValidDate,
+} from '~/monitoring/utils';
+import { timeWindows, timeWindowsKeyNames } from '~/monitoring/constants';
import { graphDataPrometheusQuery, graphDataPrometheusQueryRange } from './mock_data';
describe('getTimeDiff', () => {
@@ -39,6 +48,55 @@ describe('getTimeDiff', () => {
});
});
+describe('getTimeWindow', () => {
+ [
+ {
+ args: [
+ {
+ start: '2019-10-01T18:27:47.000Z',
+ end: '2019-10-01T21:27:47.000Z',
+ },
+ ],
+ expected: timeWindowsKeyNames.threeHours,
+ },
+ {
+ args: [
+ {
+ start: '2019-10-01T28:27:47.000Z',
+ end: '2019-10-01T21:27:47.000Z',
+ },
+ ],
+ expected: null,
+ },
+ {
+ args: [
+ {
+ start: '',
+ end: '',
+ },
+ ],
+ expected: null,
+ },
+ {
+ args: [
+ {
+ start: null,
+ end: null,
+ },
+ ],
+ expected: null,
+ },
+ {
+ args: [{}],
+ expected: null,
+ },
+ ].forEach(({ args, expected }) => {
+ it(`returns "${expected}" with args=${JSON.stringify(args)}`, () => {
+ expect(getTimeWindow(...args)).toEqual(expected);
+ });
+ });
+});
+
describe('graphDataValidatorForValues', () => {
/*
* When dealing with a metric using the query format, e.g.
@@ -62,3 +120,190 @@ describe('graphDataValidatorForValues', () => {
expect(validGraphData).toBe(true);
});
});
+
+describe('stringToISODate', () => {
+ ['', 'null', undefined, 'abc'].forEach(input => {
+ it(`throws error for invalid input like ${input}`, done => {
+ try {
+ stringToISODate(input);
+ } catch (e) {
+ expect(e).toBeDefined();
+ done();
+ }
+ });
+ });
+ [
+ {
+ input: '2019-09-09 01:01:01',
+ output: '2019-09-09T01:01:01Z',
+ },
+ {
+ input: '2019-09-09 00:00:00',
+ output: '2019-09-09T00:00:00Z',
+ },
+ {
+ input: '2019-09-09 23:59:59',
+ output: '2019-09-09T23:59:59Z',
+ },
+ {
+ input: '2019-09-09',
+ output: '2019-09-09T00:00:00Z',
+ },
+ ].forEach(({ input, output }) => {
+ it(`returns ${output} from ${input}`, () => {
+ expect(stringToISODate(input)).toBe(output);
+ });
+ });
+});
+
+describe('ISODateToString', () => {
+ [
+ {
+ input: new Date('2019-09-09T00:00:00.000Z'),
+ output: '2019-09-09 00:00:00',
+ },
+ {
+ input: new Date('2019-09-09T07:00:00.000Z'),
+ output: '2019-09-09 07:00:00',
+ },
+ ].forEach(({ input, output }) => {
+ it(`ISODateToString return ${output} for ${input}`, () => {
+ expect(ISODateToString(input)).toBe(output);
+ });
+ });
+});
+
+describe('truncateZerosInDateTime', () => {
+ [
+ {
+ input: '',
+ output: '',
+ },
+ {
+ input: '2019-10-10',
+ output: '2019-10-10',
+ },
+ {
+ input: '2019-10-10 00:00:01',
+ output: '2019-10-10 00:00:01',
+ },
+ {
+ input: '2019-10-10 00:00:00',
+ output: '2019-10-10',
+ },
+ ].forEach(({ input, output }) => {
+ it(`truncateZerosInDateTime return ${output} for ${input}`, () => {
+ expect(truncateZerosInDateTime(input)).toBe(output);
+ });
+ });
+});
+
+describe('isValidDate', () => {
+ [
+ {
+ input: '2019-09-09T00:00:00.000Z',
+ output: true,
+ },
+ {
+ input: '2019-09-09T000:00.000Z',
+ output: false,
+ },
+ {
+ input: 'a2019-09-09T000:00.000Z',
+ output: false,
+ },
+ {
+ input: '2019-09-09T',
+ output: false,
+ },
+ {
+ input: '2019-09-09',
+ output: true,
+ },
+ {
+ input: '2019-9-9',
+ output: true,
+ },
+ {
+ input: '2019-9-',
+ output: true,
+ },
+ {
+ input: '2019--',
+ output: false,
+ },
+ {
+ input: '2019',
+ output: true,
+ },
+ {
+ input: '',
+ output: false,
+ },
+ {
+ input: null,
+ output: false,
+ },
+ ].forEach(({ input, output }) => {
+ it(`isValidDate return ${output} for ${input}`, () => {
+ expect(isValidDate(input)).toBe(output);
+ });
+ });
+});
+
+describe('isDateTimePickerInputValid', () => {
+ [
+ {
+ input: null,
+ output: false,
+ },
+ {
+ input: '',
+ output: false,
+ },
+ {
+ input: 'xxxx-xx-xx',
+ output: false,
+ },
+ {
+ input: '9999-99-19',
+ output: false,
+ },
+ {
+ input: '2019-19-23',
+ output: false,
+ },
+ {
+ input: '2019-09-23',
+ output: true,
+ },
+ {
+ input: '2019-09-23 x',
+ output: false,
+ },
+ {
+ input: '2019-09-29 0:0:0',
+ output: false,
+ },
+ {
+ input: '2019-09-29 00:00:00',
+ output: true,
+ },
+ {
+ input: '2019-09-29 24:24:24',
+ output: false,
+ },
+ {
+ input: '2019-09-29 23:24:24',
+ output: true,
+ },
+ {
+ input: '2019-09-29 23:24:24 ',
+ output: false,
+ },
+ ].forEach(({ input, output }) => {
+ it(`returns ${output} for ${input}`, () => {
+ expect(isDateTimePickerInputValid(input)).toBe(output);
+ });
+ });
+});
diff --git a/spec/javascripts/notes/components/discussion_filter_spec.js b/spec/javascripts/notes/components/discussion_filter_spec.js
index 1c366aee8e2..7524de36ac5 100644
--- a/spec/javascripts/notes/components/discussion_filter_spec.js
+++ b/spec/javascripts/notes/components/discussion_filter_spec.js
@@ -160,5 +160,28 @@ describe('DiscussionFilter component', () => {
done();
});
});
+
+ it('fetches discussions when there is a hash', done => {
+ window.location.hash = `note_${discussionMock.notes[0].id}`;
+ vm.currentValue = discussionFiltersMock[2].value;
+ spyOn(vm, 'selectFilter');
+ vm.handleLocationHash();
+
+ vm.$nextTick(() => {
+ expect(vm.selectFilter).toHaveBeenCalled();
+ done();
+ });
+ });
+
+ it('does not fetch discussions when there is no hash', done => {
+ window.location.hash = '';
+ spyOn(vm, 'selectFilter');
+ vm.handleLocationHash();
+
+ vm.$nextTick(() => {
+ expect(vm.selectFilter).not.toHaveBeenCalled();
+ done();
+ });
+ });
});
});
diff --git a/spec/javascripts/notes/stores/mutation_spec.js b/spec/javascripts/notes/stores/mutation_spec.js
index 4a640d589fb..ade4725dd68 100644
--- a/spec/javascripts/notes/stores/mutation_spec.js
+++ b/spec/javascripts/notes/stores/mutation_spec.js
@@ -48,9 +48,22 @@ describe('Notes Store mutations', () => {
});
describe('ADD_NEW_REPLY_TO_DISCUSSION', () => {
+ const newReply = Object.assign({}, note, { discussion_id: discussionMock.id });
+
+ let state;
+
+ beforeEach(() => {
+ state = { discussions: [{ ...discussionMock }] };
+ });
+
it('should add a reply to a specific discussion', () => {
- const state = { discussions: [discussionMock] };
- const newReply = Object.assign({}, note, { discussion_id: discussionMock.id });
+ mutations.ADD_NEW_REPLY_TO_DISCUSSION(state, newReply);
+
+ expect(state.discussions[0].notes.length).toEqual(4);
+ });
+
+ it('should not add the note if it already exists in the discussion', () => {
+ mutations.ADD_NEW_REPLY_TO_DISCUSSION(state, newReply);
mutations.ADD_NEW_REPLY_TO_DISCUSSION(state, newReply);
expect(state.discussions[0].notes.length).toEqual(4);
diff --git a/spec/javascripts/pager_spec.js b/spec/javascripts/pager_spec.js
index 93efc139254..c95a8400c6c 100644
--- a/spec/javascripts/pager_spec.js
+++ b/spec/javascripts/pager_spec.js
@@ -63,9 +63,9 @@ describe('pager', () => {
describe('getOld', () => {
const urlRegex = /(.*)some_list(.*)$/;
- function mockSuccess() {
+ function mockSuccess(count = 0) {
axiosMock.onGet(urlRegex).reply(200, {
- count: 0,
+ count,
html: '',
});
}
@@ -142,5 +142,21 @@ describe('pager', () => {
done();
});
});
+
+ it('disables if return count is less than limit', done => {
+ Pager.offset = 0;
+ Pager.limit = 20;
+
+ mockSuccess(1);
+ spyOn(Pager.loading, 'hide');
+ Pager.getOld();
+
+ setTimeout(() => {
+ expect(Pager.loading.hide).toHaveBeenCalled();
+ expect(Pager.disable).toBe(true);
+
+ done();
+ });
+ });
});
});
diff --git a/spec/javascripts/performance_bar/components/detailed_metric_spec.js b/spec/javascripts/performance_bar/components/detailed_metric_spec.js
deleted file mode 100644
index 0486b5fa3db..00000000000
--- a/spec/javascripts/performance_bar/components/detailed_metric_spec.js
+++ /dev/null
@@ -1,109 +0,0 @@
-import Vue from 'vue';
-import detailedMetric from '~/performance_bar/components/detailed_metric.vue';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
-
-describe('detailedMetric', () => {
- let vm;
-
- afterEach(() => {
- vm.$destroy();
- });
-
- describe('when the current request has no details', () => {
- beforeEach(() => {
- vm = mountComponent(Vue.extend(detailedMetric), {
- currentRequest: {},
- metric: 'gitaly',
- header: 'Gitaly calls',
- details: 'details',
- keys: ['feature', 'request'],
- });
- });
-
- it('does not render the element', () => {
- expect(vm.$el.innerHTML).toEqual(undefined);
- });
- });
-
- describe('when the current request has details', () => {
- const requestDetails = [
- { duration: '100', feature: 'find_commit', request: 'abcdef', backtrace: ['hello', 'world'] },
- { duration: '23', feature: 'rebase_in_progress', request: '', backtrace: ['world', 'hello'] },
- ];
-
- beforeEach(() => {
- vm = mountComponent(Vue.extend(detailedMetric), {
- currentRequest: {
- details: {
- gitaly: {
- duration: '123ms',
- calls: '456',
- details: requestDetails,
- },
- },
- },
- metric: 'gitaly',
- header: 'Gitaly calls',
- keys: ['feature', 'request'],
- });
- });
-
- it('diplays details', () => {
- expect(vm.$el.innerText.replace(/\s+/g, ' ')).toContain('123ms / 456');
- });
-
- it('adds a modal with a table of the details', () => {
- vm.$el
- .querySelectorAll('.performance-bar-modal td:nth-child(1)')
- .forEach((duration, index) => {
- expect(duration.innerText).toContain(requestDetails[index].duration);
- });
-
- vm.$el
- .querySelectorAll('.performance-bar-modal td:nth-child(2)')
- .forEach((feature, index) => {
- expect(feature.innerText).toContain(requestDetails[index].feature);
- });
-
- vm.$el
- .querySelectorAll('.performance-bar-modal td:nth-child(2)')
- .forEach((request, index) => {
- expect(request.innerText).toContain(requestDetails[index].request);
- });
-
- expect(vm.$el.querySelector('.text-expander.js-toggle-button')).not.toBeNull();
-
- vm.$el.querySelectorAll('.performance-bar-modal td:nth-child(2)').forEach(request => {
- expect(request.innerText).toContain('world');
- });
- });
-
- it('displays the metric title', () => {
- expect(vm.$el.innerText).toContain('gitaly');
- });
-
- describe('when using a custom metric title', () => {
- beforeEach(() => {
- vm = mountComponent(Vue.extend(detailedMetric), {
- currentRequest: {
- details: {
- gitaly: {
- duration: '123ms',
- calls: '456',
- details: requestDetails,
- },
- },
- },
- metric: 'gitaly',
- title: 'custom',
- header: 'Gitaly calls',
- keys: ['feature', 'request'],
- });
- });
-
- it('displays the custom title', () => {
- expect(vm.$el.innerText).toContain('custom');
- });
- });
- });
-});
diff --git a/spec/javascripts/performance_bar/components/performance_bar_app_spec.js b/spec/javascripts/performance_bar/components/performance_bar_app_spec.js
deleted file mode 100644
index 7926db44429..00000000000
--- a/spec/javascripts/performance_bar/components/performance_bar_app_spec.js
+++ /dev/null
@@ -1,29 +0,0 @@
-import Vue from 'vue';
-import performanceBarApp from '~/performance_bar/components/performance_bar_app.vue';
-import PerformanceBarStore from '~/performance_bar/stores/performance_bar_store';
-
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
-
-describe('performance bar app', () => {
- let vm;
-
- beforeEach(() => {
- const store = new PerformanceBarStore();
-
- vm = mountComponent(Vue.extend(performanceBarApp), {
- store,
- env: 'development',
- requestId: '123',
- peekUrl: '/-/peek/results',
- profileUrl: '?lineprofiler=true',
- });
- });
-
- afterEach(() => {
- vm.$destroy();
- });
-
- it('sets the class to match the environment', () => {
- expect(vm.$el.getAttribute('class')).toContain('development');
- });
-});
diff --git a/spec/javascripts/performance_bar/components/request_selector_spec.js b/spec/javascripts/performance_bar/components/request_selector_spec.js
deleted file mode 100644
index 3c2169de877..00000000000
--- a/spec/javascripts/performance_bar/components/request_selector_spec.js
+++ /dev/null
@@ -1,46 +0,0 @@
-import Vue from 'vue';
-import requestSelector from '~/performance_bar/components/request_selector.vue';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
-
-describe('request selector', () => {
- const requests = [
- { id: '123', url: 'https://gitlab.com/' },
- {
- id: '456',
- url: 'https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/1',
- },
- {
- id: '789',
- url: 'https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/1.json?serializer=widget',
- },
- ];
-
- let vm;
-
- beforeEach(() => {
- vm = mountComponent(Vue.extend(requestSelector), {
- requests,
- currentRequest: requests[1],
- });
- });
-
- afterEach(() => {
- vm.$destroy();
- });
-
- function optionText(requestId) {
- return vm.$el.querySelector(`[value='${requestId}']`).innerText.trim();
- }
-
- it('displays the last component of the path', () => {
- expect(optionText(requests[2].id)).toEqual('1.json?serializer=widget');
- });
-
- it('keeps the last two components of the path when the last component is numeric', () => {
- expect(optionText(requests[1].id)).toEqual('merge_requests/1');
- });
-
- it('ignores trailing slashes', () => {
- expect(optionText(requests[0].id)).toEqual('gitlab.com');
- });
-});
diff --git a/spec/javascripts/pipelines/graph/graph_component_spec.js b/spec/javascripts/pipelines/graph/graph_component_spec.js
index 98e92aff25f..5effbaabcd1 100644
--- a/spec/javascripts/pipelines/graph/graph_component_spec.js
+++ b/spec/javascripts/pipelines/graph/graph_component_spec.js
@@ -1,10 +1,17 @@
import Vue from 'vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
+import PipelineStore from '~/pipelines/stores/pipeline_store';
import graphComponent from '~/pipelines/components/graph/graph_component.vue';
import graphJSON from './mock_data';
+import linkedPipelineJSON from '../linked_pipelines_mock.json';
+import PipelinesMediator from '~/pipelines/pipeline_details_mediator';
describe('graph component', () => {
const GraphComponent = Vue.extend(graphComponent);
+ const store = new PipelineStore();
+ store.storePipeline(linkedPipelineJSON);
+ const mediator = new PipelinesMediator({ endpoint: '' });
+
let component;
beforeEach(() => {
@@ -22,6 +29,7 @@ describe('graph component', () => {
component = mountComponent(GraphComponent, {
isLoading: true,
pipeline: {},
+ mediator,
});
expect(component.$el.querySelector('.loading-icon')).toBeDefined();
@@ -33,6 +41,7 @@ describe('graph component', () => {
component = mountComponent(GraphComponent, {
isLoading: false,
pipeline: graphJSON,
+ mediator,
});
expect(component.$el.classList.contains('js-pipeline-graph')).toEqual(true);
@@ -57,11 +66,205 @@ describe('graph component', () => {
});
});
+ describe('when linked pipelines are present', () => {
+ beforeEach(() => {
+ component = mountComponent(GraphComponent, {
+ isLoading: false,
+ pipeline: store.state.pipeline,
+ mediator,
+ });
+ });
+
+ describe('rendered output', () => {
+ it('should include the pipelines graph', () => {
+ expect(component.$el.classList.contains('js-pipeline-graph')).toEqual(true);
+ });
+
+ it('should not include the loading icon', () => {
+ expect(component.$el.querySelector('.fa-spinner')).toBeNull();
+ });
+
+ it('should include the stage column list', () => {
+ expect(component.$el.querySelector('.stage-column-list')).not.toBeNull();
+ });
+
+ it('should include the no-margin class on the first child', () => {
+ const firstStageColumnElement = component.$el.querySelector(
+ '.stage-column-list .stage-column',
+ );
+
+ expect(firstStageColumnElement.classList.contains('no-margin')).toEqual(true);
+ });
+
+ it('should include the has-only-one-job class on the first child', () => {
+ const firstStageColumnElement = component.$el.querySelector(
+ '.stage-column-list .stage-column',
+ );
+
+ expect(firstStageColumnElement.classList.contains('has-only-one-job')).toEqual(true);
+ });
+
+ it('should include the left-margin class on the second child', () => {
+ const firstStageColumnElement = component.$el.querySelector(
+ '.stage-column-list .stage-column:last-child',
+ );
+
+ expect(firstStageColumnElement.classList.contains('left-margin')).toEqual(true);
+ });
+
+ it('should include the js-has-linked-pipelines flag', () => {
+ expect(component.$el.querySelector('.js-has-linked-pipelines')).not.toBeNull();
+ });
+ });
+
+ describe('computeds and methods', () => {
+ describe('capitalizeStageName', () => {
+ it('it capitalizes the stage name', () => {
+ expect(component.capitalizeStageName('mystage')).toBe('Mystage');
+ });
+ });
+
+ describe('stageConnectorClass', () => {
+ it('it returns left-margin when there is a triggerer', () => {
+ expect(component.stageConnectorClass(0, { groups: ['job'] })).toBe('no-margin');
+ });
+ });
+ });
+
+ describe('linked pipelines components', () => {
+ beforeEach(() => {
+ component = mountComponent(GraphComponent, {
+ isLoading: false,
+ pipeline: store.state.pipeline,
+ mediator,
+ });
+ });
+
+ it('should render an upstream pipelines column', () => {
+ expect(component.$el.querySelector('.linked-pipelines-column')).not.toBeNull();
+ expect(component.$el.innerHTML).toContain('Upstream');
+ });
+
+ it('should render a downstream pipelines column', () => {
+ expect(component.$el.querySelector('.linked-pipelines-column')).not.toBeNull();
+ expect(component.$el.innerHTML).toContain('Downstream');
+ });
+
+ describe('triggered by', () => {
+ describe('on click', () => {
+ it('should emit `onClickTriggeredBy` when triggered by linked pipeline is clicked', () => {
+ spyOn(component, '$emit');
+
+ component.$el.querySelector('#js-linked-pipeline-12').click();
+
+ expect(component.$emit).toHaveBeenCalledWith(
+ 'onClickTriggeredBy',
+ component.pipeline,
+ component.pipeline.triggered_by[0],
+ );
+ });
+ });
+
+ describe('with expanded pipeline', () => {
+ it('should render expanded pipeline', done => {
+ // expand the pipeline
+ store.state.pipeline.triggered_by[0].isExpanded = true;
+
+ component = mountComponent(GraphComponent, {
+ isLoading: false,
+ pipeline: store.state.pipeline,
+ mediator,
+ });
+
+ Vue.nextTick()
+ .then(() => {
+ expect(component.$el.querySelector('.js-upstream-pipeline-12')).not.toBeNull();
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+ });
+
+ describe('triggered', () => {
+ describe('on click', () => {
+ it('should emit `onClickTriggered`', () => {
+ spyOn(component, '$emit');
+
+ component.$el.querySelector('#js-linked-pipeline-34993051').click();
+
+ expect(component.$emit).toHaveBeenCalledWith(
+ 'onClickTriggered',
+ component.pipeline,
+ component.pipeline.triggered[0],
+ );
+ });
+ });
+
+ describe('with expanded pipeline', () => {
+ it('should render expanded pipeline', done => {
+ // expand the pipeline
+ store.state.pipeline.triggered[0].isExpanded = true;
+
+ component = mountComponent(GraphComponent, {
+ isLoading: false,
+ pipeline: store.state.pipeline,
+ mediator,
+ });
+
+ Vue.nextTick()
+ .then(() => {
+ expect(
+ component.$el.querySelector('.js-downstream-pipeline-34993051'),
+ ).not.toBeNull();
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+ });
+ });
+ });
+
+ describe('when linked pipelines are not present', () => {
+ beforeEach(() => {
+ const pipeline = Object.assign(linkedPipelineJSON, { triggered: null, triggered_by: null });
+ component = mountComponent(GraphComponent, {
+ isLoading: false,
+ pipeline,
+ mediator,
+ });
+ });
+
+ describe('rendered output', () => {
+ it('should include the first column with a no margin', () => {
+ const firstColumn = component.$el.querySelector('.stage-column:first-child');
+
+ expect(firstColumn.classList.contains('no-margin')).toEqual(true);
+ });
+
+ it('should not render a linked pipelines column', () => {
+ expect(component.$el.querySelector('.linked-pipelines-column')).toBeNull();
+ });
+ });
+
+ describe('stageConnectorClass', () => {
+ it('it returns left-margin when no triggerer and there is one job', () => {
+ expect(component.stageConnectorClass(0, { groups: ['job'] })).toBe('no-margin');
+ });
+
+ it('it returns left-margin when no triggerer and not the first stage', () => {
+ expect(component.stageConnectorClass(99, { groups: ['job'] })).toBe('left-margin');
+ });
+ });
+ });
+
describe('capitalizeStageName', () => {
it('capitalizes and escapes stage name', () => {
component = mountComponent(GraphComponent, {
isLoading: false,
pipeline: graphJSON,
+ mediator,
});
expect(
diff --git a/spec/javascripts/pipelines/graph/linked_pipeline_spec.js b/spec/javascripts/pipelines/graph/linked_pipeline_spec.js
new file mode 100644
index 00000000000..8d3abf094b6
--- /dev/null
+++ b/spec/javascripts/pipelines/graph/linked_pipeline_spec.js
@@ -0,0 +1,116 @@
+import Vue from 'vue';
+import LinkedPipelineComponent from '~/pipelines/components/graph/linked_pipeline.vue';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
+import mockData from './linked_pipelines_mock_data';
+
+const mockPipeline = mockData.triggered[0];
+
+describe('Linked pipeline', () => {
+ const Component = Vue.extend(LinkedPipelineComponent);
+ let vm;
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('rendered output', () => {
+ const props = {
+ pipeline: mockPipeline,
+ };
+
+ beforeEach(() => {
+ vm = mountComponent(Component, props);
+ });
+
+ it('should render a list item as the containing element', () => {
+ expect(vm.$el.tagName).toBe('LI');
+ });
+
+ it('should render a button', () => {
+ const linkElement = vm.$el.querySelector('.js-linked-pipeline-content');
+
+ expect(linkElement).not.toBeNull();
+ });
+
+ it('should render the project name', () => {
+ expect(vm.$el.innerText).toContain(props.pipeline.project.name);
+ });
+
+ it('should render an svg within the status container', () => {
+ const pipelineStatusElement = vm.$el.querySelector('.js-linked-pipeline-status');
+
+ expect(pipelineStatusElement.querySelector('svg')).not.toBeNull();
+ });
+
+ it('should render the pipeline status icon svg', () => {
+ expect(vm.$el.querySelector('.js-ci-status-icon-running')).not.toBeNull();
+ expect(vm.$el.querySelector('.js-ci-status-icon-running').innerHTML).toContain('<svg');
+ });
+
+ it('should have a ci-status child component', () => {
+ expect(vm.$el.querySelector('.js-linked-pipeline-status')).not.toBeNull();
+ });
+
+ it('should render the pipeline id', () => {
+ expect(vm.$el.innerText).toContain(`#${props.pipeline.id}`);
+ });
+
+ it('should correctly compute the tooltip text', () => {
+ expect(vm.tooltipText).toContain(mockPipeline.project.name);
+ expect(vm.tooltipText).toContain(mockPipeline.details.status.label);
+ });
+
+ it('should render the tooltip text as the title attribute', () => {
+ const tooltipRef = vm.$el.querySelector('.js-linked-pipeline-content');
+ const titleAttr = tooltipRef.getAttribute('data-original-title');
+
+ expect(titleAttr).toContain(mockPipeline.project.name);
+ expect(titleAttr).toContain(mockPipeline.details.status.label);
+ });
+
+ it('does not render the loading icon when isLoading is false', () => {
+ expect(vm.$el.querySelector('.js-linked-pipeline-loading')).toBeNull();
+ });
+ });
+
+ describe('when isLoading is true', () => {
+ const props = {
+ pipeline: { ...mockPipeline, isLoading: true },
+ };
+
+ beforeEach(() => {
+ vm = mountComponent(Component, props);
+ });
+
+ it('renders a loading icon', () => {
+ expect(vm.$el.querySelector('.js-linked-pipeline-loading')).not.toBeNull();
+ });
+ });
+
+ describe('on click', () => {
+ const props = {
+ pipeline: mockPipeline,
+ };
+
+ beforeEach(() => {
+ vm = mountComponent(Component, props);
+ });
+
+ it('emits `pipelineClicked` event', () => {
+ spyOn(vm, '$emit');
+ vm.$el.querySelector('button').click();
+
+ expect(vm.$emit).toHaveBeenCalledWith('pipelineClicked');
+ });
+
+ it('should emit `bv::hide::tooltip` to close the tooltip', () => {
+ spyOn(vm.$root, '$emit');
+ vm.$el.querySelector('button').click();
+
+ expect(vm.$root.$emit.calls.argsFor(0)).toEqual([
+ 'bv::hide::tooltip',
+ 'js-linked-pipeline-132',
+ ]);
+ });
+ });
+});
diff --git a/spec/javascripts/pipelines/graph/linked_pipelines_column_spec.js b/spec/javascripts/pipelines/graph/linked_pipelines_column_spec.js
new file mode 100644
index 00000000000..1f835dc4dee
--- /dev/null
+++ b/spec/javascripts/pipelines/graph/linked_pipelines_column_spec.js
@@ -0,0 +1,38 @@
+import Vue from 'vue';
+import LinkedPipelinesColumn from '~/pipelines/components/graph/linked_pipelines_column.vue';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
+import mockData from './linked_pipelines_mock_data';
+
+describe('Linked Pipelines Column', () => {
+ const Component = Vue.extend(LinkedPipelinesColumn);
+ const props = {
+ columnTitle: 'Upstream',
+ linkedPipelines: mockData.triggered,
+ graphPosition: 'right',
+ };
+ let vm;
+
+ beforeEach(() => {
+ vm = mountComponent(Component, props);
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('renders the pipeline orientation', () => {
+ const titleElement = vm.$el.querySelector('.linked-pipelines-column-title');
+
+ expect(titleElement.innerText).toContain(props.columnTitle);
+ });
+
+ it('has the correct number of linked pipeline child components', () => {
+ expect(vm.$children.length).toBe(props.linkedPipelines.length);
+ });
+
+ it('renders the correct number of linked pipelines', () => {
+ const linkedPipelineElements = vm.$el.querySelectorAll('.linked-pipeline');
+
+ expect(linkedPipelineElements.length).toBe(props.linkedPipelines.length);
+ });
+});
diff --git a/spec/javascripts/pipelines/graph/linked_pipelines_mock_data.js b/spec/javascripts/pipelines/graph/linked_pipelines_mock_data.js
new file mode 100644
index 00000000000..f794b8484a7
--- /dev/null
+++ b/spec/javascripts/pipelines/graph/linked_pipelines_mock_data.js
@@ -0,0 +1,407 @@
+export default {
+ triggered_by: {
+ id: 129,
+ active: true,
+ path: '/gitlab-org/gitlab-foss/pipelines/129',
+ project: {
+ name: 'GitLabCE',
+ },
+ details: {
+ status: {
+ icon: 'status_running',
+ text: 'running',
+ label: 'running',
+ group: 'running',
+ has_details: true,
+ details_path: '/gitlab-org/gitlab-foss/pipelines/129',
+ favicon:
+ '/assets/ci_favicons/dev/favicon_status_running-c3ad2fc53ea6079c174e5b6c1351ff349e99ec3af5a5622fb77b0fe53ea279c1.ico',
+ },
+ },
+ flags: {
+ latest: false,
+ triggered: false,
+ stuck: false,
+ yaml_errors: false,
+ retryable: true,
+ cancelable: true,
+ },
+ ref: {
+ name: '7-5-stable',
+ path: '/gitlab-org/gitlab-foss/commits/7-5-stable',
+ tag: false,
+ branch: true,
+ },
+ commit: {
+ id: '23433d4d8b20d7e45c103d0b6048faad38a130ab',
+ short_id: '23433d4d',
+ title: 'Version 7.5.0.rc1',
+ created_at: '2014-11-17T15:44:14.000+01:00',
+ parent_ids: ['30ac909f30f58d319b42ed1537664483894b18cd'],
+ message: 'Version 7.5.0.rc1\n',
+ author_name: 'Jacob Vosmaer',
+ author_email: 'contact@jacobvosmaer.nl',
+ authored_date: '2014-11-17T15:44:14.000+01:00',
+ committer_name: 'Jacob Vosmaer',
+ committer_email: 'contact@jacobvosmaer.nl',
+ committed_date: '2014-11-17T15:44:14.000+01:00',
+ author_gravatar_url:
+ 'http://www.gravatar.com/avatar/e66d11c0eedf8c07b3b18fca46599807?s=80&d=identicon',
+ commit_url:
+ 'http://localhost:3000/gitlab-org/gitlab-foss/commit/23433d4d8b20d7e45c103d0b6048faad38a130ab',
+ commit_path: '/gitlab-org/gitlab-foss/commit/23433d4d8b20d7e45c103d0b6048faad38a130ab',
+ },
+ retry_path: '/gitlab-org/gitlab-foss/pipelines/129/retry',
+ cancel_path: '/gitlab-org/gitlab-foss/pipelines/129/cancel',
+ created_at: '2017-05-24T14:46:20.090Z',
+ updated_at: '2017-05-24T14:46:29.906Z',
+ },
+ triggered: [
+ {
+ id: 132,
+ active: true,
+ path: '/gitlab-org/gitlab-foss/pipelines/132',
+ project: {
+ name: 'GitLabCE',
+ },
+ details: {
+ status: {
+ icon: 'status_running',
+ text: 'running',
+ label: 'running',
+ group: 'running',
+ has_details: true,
+ details_path: '/gitlab-org/gitlab-foss/pipelines/132',
+ favicon:
+ '/assets/ci_favicons/dev/favicon_status_running-c3ad2fc53ea6079c174e5b6c1351ff349e99ec3af5a5622fb77b0fe53ea279c1.ico',
+ },
+ },
+ flags: {
+ latest: false,
+ triggered: false,
+ stuck: false,
+ yaml_errors: false,
+ retryable: true,
+ cancelable: true,
+ },
+ ref: {
+ name: 'crowd',
+ path: '/gitlab-org/gitlab-foss/commits/crowd',
+ tag: false,
+ branch: true,
+ },
+ commit: {
+ id: 'b9d58c4cecd06be74c3cc32ccfb522b31544ab2e',
+ short_id: 'b9d58c4c',
+ title: 'getting user keys publically through http without any authentication, the github…',
+ created_at: '2013-10-03T12:50:33.000+05:30',
+ parent_ids: ['e219cf7246c6a0495e4507deaffeba11e79f13b8'],
+ message:
+ 'getting user keys publically through http without any authentication, the github way. E.g: http://github.com/devaroop.keys\n\nchangelog updated to include ssh key retrieval feature update\n',
+ author_name: 'devaroop',
+ author_email: 'devaroop123@yahoo.co.in',
+ authored_date: '2013-10-02T20:39:29.000+05:30',
+ committer_name: 'devaroop',
+ committer_email: 'devaroop123@yahoo.co.in',
+ committed_date: '2013-10-03T12:50:33.000+05:30',
+ author_gravatar_url:
+ 'http://www.gravatar.com/avatar/35df4b155ec66a3127d53459941cf8a2?s=80&d=identicon',
+ commit_url:
+ 'http://localhost:3000/gitlab-org/gitlab-foss/commit/b9d58c4cecd06be74c3cc32ccfb522b31544ab2e',
+ commit_path: '/gitlab-org/gitlab-foss/commit/b9d58c4cecd06be74c3cc32ccfb522b31544ab2e',
+ },
+ retry_path: '/gitlab-org/gitlab-foss/pipelines/132/retry',
+ cancel_path: '/gitlab-org/gitlab-foss/pipelines/132/cancel',
+ created_at: '2017-05-24T14:46:24.644Z',
+ updated_at: '2017-05-24T14:48:55.226Z',
+ },
+ {
+ id: 133,
+ active: true,
+ path: '/gitlab-org/gitlab-foss/pipelines/133',
+ project: {
+ name: 'GitLabCE',
+ },
+ details: {
+ status: {
+ icon: 'status_running',
+ text: 'running',
+ label: 'running',
+ group: 'running',
+ has_details: true,
+ details_path: '/gitlab-org/gitlab-foss/pipelines/133',
+ favicon:
+ '/assets/ci_favicons/dev/favicon_status_running-c3ad2fc53ea6079c174e5b6c1351ff349e99ec3af5a5622fb77b0fe53ea279c1.ico',
+ },
+ },
+ flags: {
+ latest: false,
+ triggered: false,
+ stuck: false,
+ yaml_errors: false,
+ retryable: true,
+ cancelable: true,
+ },
+ ref: {
+ name: 'crowd',
+ path: '/gitlab-org/gitlab-foss/commits/crowd',
+ tag: false,
+ branch: true,
+ },
+ commit: {
+ id: 'b6bd4856a33df3d144be66c4ed1f1396009bb08b',
+ short_id: 'b6bd4856',
+ title: 'getting user keys publically through http without any authentication, the github…',
+ created_at: '2013-10-02T20:39:29.000+05:30',
+ parent_ids: ['e219cf7246c6a0495e4507deaffeba11e79f13b8'],
+ message:
+ 'getting user keys publically through http without any authentication, the github way. E.g: http://github.com/devaroop.keys\n',
+ author_name: 'devaroop',
+ author_email: 'devaroop123@yahoo.co.in',
+ authored_date: '2013-10-02T20:39:29.000+05:30',
+ committer_name: 'devaroop',
+ committer_email: 'devaroop123@yahoo.co.in',
+ committed_date: '2013-10-02T20:39:29.000+05:30',
+ author_gravatar_url:
+ 'http://www.gravatar.com/avatar/35df4b155ec66a3127d53459941cf8a2?s=80&d=identicon',
+ commit_url:
+ 'http://localhost:3000/gitlab-org/gitlab-foss/commit/b6bd4856a33df3d144be66c4ed1f1396009bb08b',
+ commit_path: '/gitlab-org/gitlab-foss/commit/b6bd4856a33df3d144be66c4ed1f1396009bb08b',
+ },
+ retry_path: '/gitlab-org/gitlab-foss/pipelines/133/retry',
+ cancel_path: '/gitlab-org/gitlab-foss/pipelines/133/cancel',
+ created_at: '2017-05-24T14:46:24.648Z',
+ updated_at: '2017-05-24T14:48:59.673Z',
+ },
+ {
+ id: 130,
+ active: true,
+ path: '/gitlab-org/gitlab-foss/pipelines/130',
+ project: {
+ name: 'GitLabCE',
+ },
+ details: {
+ status: {
+ icon: 'status_running',
+ text: 'running',
+ label: 'running',
+ group: 'running',
+ has_details: true,
+ details_path: '/gitlab-org/gitlab-foss/pipelines/130',
+ favicon:
+ '/assets/ci_favicons/dev/favicon_status_running-c3ad2fc53ea6079c174e5b6c1351ff349e99ec3af5a5622fb77b0fe53ea279c1.ico',
+ },
+ },
+ flags: {
+ latest: false,
+ triggered: false,
+ stuck: false,
+ yaml_errors: false,
+ retryable: true,
+ cancelable: true,
+ },
+ ref: {
+ name: 'crowd',
+ path: '/gitlab-org/gitlab-foss/commits/crowd',
+ tag: false,
+ branch: true,
+ },
+ commit: {
+ id: '6d7ced4a2311eeff037c5575cca1868a6d3f586f',
+ short_id: '6d7ced4a',
+ title: 'Whitespace fixes to patch',
+ created_at: '2013-10-08T13:53:22.000-05:00',
+ parent_ids: ['1875141a963a4238bda29011d8f7105839485253'],
+ message: 'Whitespace fixes to patch\n',
+ author_name: 'Dale Hamel',
+ author_email: 'dale.hamel@srvthe.net',
+ authored_date: '2013-10-08T13:53:22.000-05:00',
+ committer_name: 'Dale Hamel',
+ committer_email: 'dale.hamel@invenia.ca',
+ committed_date: '2013-10-08T13:53:22.000-05:00',
+ author_gravatar_url:
+ 'http://www.gravatar.com/avatar/cd08930e69fa5ad1a669206e7bafe476?s=80&d=identicon',
+ commit_url:
+ 'http://localhost:3000/gitlab-org/gitlab-foss/commit/6d7ced4a2311eeff037c5575cca1868a6d3f586f',
+ commit_path: '/gitlab-org/gitlab-foss/commit/6d7ced4a2311eeff037c5575cca1868a6d3f586f',
+ },
+ retry_path: '/gitlab-org/gitlab-foss/pipelines/130/retry',
+ cancel_path: '/gitlab-org/gitlab-foss/pipelines/130/cancel',
+ created_at: '2017-05-24T14:46:24.630Z',
+ updated_at: '2017-05-24T14:49:45.091Z',
+ },
+ {
+ id: 131,
+ active: true,
+ path: '/gitlab-org/gitlab-foss/pipelines/132',
+ project: {
+ name: 'GitLabCE',
+ },
+ details: {
+ status: {
+ icon: 'status_running',
+ text: 'running',
+ label: 'running',
+ group: 'running',
+ has_details: true,
+ details_path: '/gitlab-org/gitlab-foss/pipelines/132',
+ favicon:
+ '/assets/ci_favicons/dev/favicon_status_running-c3ad2fc53ea6079c174e5b6c1351ff349e99ec3af5a5622fb77b0fe53ea279c1.ico',
+ },
+ },
+ flags: {
+ latest: false,
+ triggered: false,
+ stuck: false,
+ yaml_errors: false,
+ retryable: true,
+ cancelable: true,
+ },
+ ref: {
+ name: 'crowd',
+ path: '/gitlab-org/gitlab-foss/commits/crowd',
+ tag: false,
+ branch: true,
+ },
+ commit: {
+ id: 'b9d58c4cecd06be74c3cc32ccfb522b31544ab2e',
+ short_id: 'b9d58c4c',
+ title: 'getting user keys publically through http without any authentication, the github…',
+ created_at: '2013-10-03T12:50:33.000+05:30',
+ parent_ids: ['e219cf7246c6a0495e4507deaffeba11e79f13b8'],
+ message:
+ 'getting user keys publically through http without any authentication, the github way. E.g: http://github.com/devaroop.keys\n\nchangelog updated to include ssh key retrieval feature update\n',
+ author_name: 'devaroop',
+ author_email: 'devaroop123@yahoo.co.in',
+ authored_date: '2013-10-02T20:39:29.000+05:30',
+ committer_name: 'devaroop',
+ committer_email: 'devaroop123@yahoo.co.in',
+ committed_date: '2013-10-03T12:50:33.000+05:30',
+ author_gravatar_url:
+ 'http://www.gravatar.com/avatar/35df4b155ec66a3127d53459941cf8a2?s=80&d=identicon',
+ commit_url:
+ 'http://localhost:3000/gitlab-org/gitlab-foss/commit/b9d58c4cecd06be74c3cc32ccfb522b31544ab2e',
+ commit_path: '/gitlab-org/gitlab-foss/commit/b9d58c4cecd06be74c3cc32ccfb522b31544ab2e',
+ },
+ retry_path: '/gitlab-org/gitlab-foss/pipelines/132/retry',
+ cancel_path: '/gitlab-org/gitlab-foss/pipelines/132/cancel',
+ created_at: '2017-05-24T14:46:24.644Z',
+ updated_at: '2017-05-24T14:48:55.226Z',
+ },
+ {
+ id: 134,
+ active: true,
+ path: '/gitlab-org/gitlab-foss/pipelines/133',
+ project: {
+ name: 'GitLabCE',
+ },
+ details: {
+ status: {
+ icon: 'status_running',
+ text: 'running',
+ label: 'running',
+ group: 'running',
+ has_details: true,
+ details_path: '/gitlab-org/gitlab-foss/pipelines/133',
+ favicon:
+ '/assets/ci_favicons/dev/favicon_status_running-c3ad2fc53ea6079c174e5b6c1351ff349e99ec3af5a5622fb77b0fe53ea279c1.ico',
+ },
+ },
+ flags: {
+ latest: false,
+ triggered: false,
+ stuck: false,
+ yaml_errors: false,
+ retryable: true,
+ cancelable: true,
+ },
+ ref: {
+ name: 'crowd',
+ path: '/gitlab-org/gitlab-foss/commits/crowd',
+ tag: false,
+ branch: true,
+ },
+ commit: {
+ id: 'b6bd4856a33df3d144be66c4ed1f1396009bb08b',
+ short_id: 'b6bd4856',
+ title: 'getting user keys publically through http without any authentication, the github…',
+ created_at: '2013-10-02T20:39:29.000+05:30',
+ parent_ids: ['e219cf7246c6a0495e4507deaffeba11e79f13b8'],
+ message:
+ 'getting user keys publically through http without any authentication, the github way. E.g: http://github.com/devaroop.keys\n',
+ author_name: 'devaroop',
+ author_email: 'devaroop123@yahoo.co.in',
+ authored_date: '2013-10-02T20:39:29.000+05:30',
+ committer_name: 'devaroop',
+ committer_email: 'devaroop123@yahoo.co.in',
+ committed_date: '2013-10-02T20:39:29.000+05:30',
+ author_gravatar_url:
+ 'http://www.gravatar.com/avatar/35df4b155ec66a3127d53459941cf8a2?s=80&d=identicon',
+ commit_url:
+ 'http://localhost:3000/gitlab-org/gitlab-foss/commit/b6bd4856a33df3d144be66c4ed1f1396009bb08b',
+ commit_path: '/gitlab-org/gitlab-foss/commit/b6bd4856a33df3d144be66c4ed1f1396009bb08b',
+ },
+ retry_path: '/gitlab-org/gitlab-foss/pipelines/133/retry',
+ cancel_path: '/gitlab-org/gitlab-foss/pipelines/133/cancel',
+ created_at: '2017-05-24T14:46:24.648Z',
+ updated_at: '2017-05-24T14:48:59.673Z',
+ },
+ {
+ id: 135,
+ active: true,
+ path: '/gitlab-org/gitlab-foss/pipelines/130',
+ project: {
+ name: 'GitLabCE',
+ },
+ details: {
+ status: {
+ icon: 'status_running',
+ text: 'running',
+ label: 'running',
+ group: 'running',
+ has_details: true,
+ details_path: '/gitlab-org/gitlab-foss/pipelines/130',
+ favicon:
+ '/assets/ci_favicons/dev/favicon_status_running-c3ad2fc53ea6079c174e5b6c1351ff349e99ec3af5a5622fb77b0fe53ea279c1.ico',
+ },
+ },
+ flags: {
+ latest: false,
+ triggered: false,
+ stuck: false,
+ yaml_errors: false,
+ retryable: true,
+ cancelable: true,
+ },
+ ref: {
+ name: 'crowd',
+ path: '/gitlab-org/gitlab-foss/commits/crowd',
+ tag: false,
+ branch: true,
+ },
+ commit: {
+ id: '6d7ced4a2311eeff037c5575cca1868a6d3f586f',
+ short_id: '6d7ced4a',
+ title: 'Whitespace fixes to patch',
+ created_at: '2013-10-08T13:53:22.000-05:00',
+ parent_ids: ['1875141a963a4238bda29011d8f7105839485253'],
+ message: 'Whitespace fixes to patch\n',
+ author_name: 'Dale Hamel',
+ author_email: 'dale.hamel@srvthe.net',
+ authored_date: '2013-10-08T13:53:22.000-05:00',
+ committer_name: 'Dale Hamel',
+ committer_email: 'dale.hamel@invenia.ca',
+ committed_date: '2013-10-08T13:53:22.000-05:00',
+ author_gravatar_url:
+ 'http://www.gravatar.com/avatar/cd08930e69fa5ad1a669206e7bafe476?s=80&d=identicon',
+ commit_url:
+ 'http://localhost:3000/gitlab-org/gitlab-foss/commit/6d7ced4a2311eeff037c5575cca1868a6d3f586f',
+ commit_path: '/gitlab-org/gitlab-foss/commit/6d7ced4a2311eeff037c5575cca1868a6d3f586f',
+ },
+ retry_path: '/gitlab-org/gitlab-foss/pipelines/130/retry',
+ cancel_path: '/gitlab-org/gitlab-foss/pipelines/130/cancel',
+ created_at: '2017-05-24T14:46:24.630Z',
+ updated_at: '2017-05-24T14:49:45.091Z',
+ },
+ ],
+};
diff --git a/spec/javascripts/pipelines/linked_pipelines_mock.json b/spec/javascripts/pipelines/linked_pipelines_mock.json
new file mode 100644
index 00000000000..b498903f804
--- /dev/null
+++ b/spec/javascripts/pipelines/linked_pipelines_mock.json
@@ -0,0 +1,3532 @@
+{
+ "id": 23211253,
+ "user": {
+ "id": 3585,
+ "name": "Achilleas Pipinellis",
+ "username": "axil",
+ "state": "active",
+ "avatar_url": "https://assets.gitlab-static.net/uploads/-/system/user/avatar/3585/avatar.png",
+ "web_url": "https://gitlab.com/axil",
+ "status_tooltip_html": "\u003cspan class=\"user-status-emoji has-tooltip\" title=\"I like pizza\" data-html=\"true\" data-placement=\"top\"\u003e\u003cgl-emoji title=\"slice of pizza\" data-name=\"pizza\" data-unicode-version=\"6.0\"\u003e🍕\u003c/gl-emoji\u003e\u003c/span\u003e",
+ "path": "/axil"
+ },
+ "active": false,
+ "coverage": null,
+ "source": "push",
+ "created_at": "2018-06-05T11:31:30.452Z",
+ "updated_at": "2018-10-31T16:35:31.305Z",
+ "path": "/gitlab-org/gitlab-runner/pipelines/23211253",
+ "flags": {
+ "latest": false,
+ "stuck": false,
+ "auto_devops": false,
+ "merge_request": false,
+ "yaml_errors": false,
+ "retryable": false,
+ "cancelable": false,
+ "failure_reason": false
+ },
+ "details": {
+ "status": {
+ "icon": "status_success",
+ "text": "passed",
+ "label": "passed",
+ "group": "success",
+ "tooltip": "passed",
+ "has_details": true,
+ "details_path": "/gitlab-org/gitlab-runner/pipelines/23211253",
+ "illustration": null,
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png"
+ },
+ "duration": 53,
+ "finished_at": "2018-10-31T16:35:31.299Z",
+ "stages": [
+ {
+ "name": "prebuild",
+ "title": "prebuild: passed",
+ "groups": [
+ {
+ "name": "review-docs-deploy",
+ "size": 1,
+ "status": {
+ "icon": "status_success",
+ "text": "passed",
+ "label": "manual play action",
+ "group": "success",
+ "tooltip": "passed",
+ "has_details": true,
+ "details_path": "/gitlab-org/gitlab-runner/-/jobs/72469032",
+ "illustration": {
+ "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg",
+ "size": "svg-394",
+ "title": "This job requires a manual action",
+ "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments"
+ },
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png",
+ "action": {
+ "icon": "play",
+ "title": "Play",
+ "path": "/gitlab-org/gitlab-runner/-/jobs/72469032/play",
+ "method": "post",
+ "button_title": "Trigger this manual action"
+ }
+ },
+ "jobs": [
+ {
+ "id": 72469032,
+ "name": "review-docs-deploy",
+ "started": "2018-10-31T16:34:58.778Z",
+ "archived": false,
+ "build_path": "/gitlab-org/gitlab-runner/-/jobs/72469032",
+ "retry_path": "/gitlab-org/gitlab-runner/-/jobs/72469032/retry",
+ "play_path": "/gitlab-org/gitlab-runner/-/jobs/72469032/play",
+ "playable": true,
+ "scheduled": false,
+ "created_at": "2018-06-05T11:31:30.495Z",
+ "updated_at": "2018-10-31T16:35:31.251Z",
+ "status": {
+ "icon": "status_success",
+ "text": "passed",
+ "label": "manual play action",
+ "group": "success",
+ "tooltip": "passed",
+ "has_details": true,
+ "details_path": "/gitlab-org/gitlab-runner/-/jobs/72469032",
+ "illustration": {
+ "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg",
+ "size": "svg-394",
+ "title": "This job requires a manual action",
+ "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments"
+ },
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png",
+ "action": {
+ "icon": "play",
+ "title": "Play",
+ "path": "/gitlab-org/gitlab-runner/-/jobs/72469032/play",
+ "method": "post",
+ "button_title": "Trigger this manual action"
+ }
+ }
+ }
+ ]
+ }
+ ],
+ "status": {
+ "icon": "status_success",
+ "text": "passed",
+ "label": "passed",
+ "group": "success",
+ "tooltip": "passed",
+ "has_details": true,
+ "details_path": "/gitlab-org/gitlab-runner/pipelines/23211253#prebuild",
+ "illustration": null,
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png"
+ },
+ "path": "/gitlab-org/gitlab-runner/pipelines/23211253#prebuild",
+ "dropdown_path": "/gitlab-org/gitlab-runner/pipelines/23211253/stage.json?stage=prebuild"
+ },
+ {
+ "name": "test",
+ "title": "test: passed",
+ "groups": [
+ {
+ "name": "docs check links",
+ "size": 1,
+ "status": {
+ "icon": "status_success",
+ "text": "passed",
+ "label": "passed",
+ "group": "success",
+ "tooltip": "passed",
+ "has_details": true,
+ "details_path": "/gitlab-org/gitlab-runner/-/jobs/72469033",
+ "illustration": {
+ "image": "https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
+ "size": "svg-430",
+ "title": "This job does not have a trace."
+ },
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png",
+ "action": {
+ "icon": "retry",
+ "title": "Retry",
+ "path": "/gitlab-org/gitlab-runner/-/jobs/72469033/retry",
+ "method": "post",
+ "button_title": "Retry this job"
+ }
+ },
+ "jobs": [
+ {
+ "id": 72469033,
+ "name": "docs check links",
+ "started": "2018-06-05T11:31:33.240Z",
+ "archived": false,
+ "build_path": "/gitlab-org/gitlab-runner/-/jobs/72469033",
+ "retry_path": "/gitlab-org/gitlab-runner/-/jobs/72469033/retry",
+ "playable": false,
+ "scheduled": false,
+ "created_at": "2018-06-05T11:31:30.627Z",
+ "updated_at": "2018-06-05T11:31:54.363Z",
+ "status": {
+ "icon": "status_success",
+ "text": "passed",
+ "label": "passed",
+ "group": "success",
+ "tooltip": "passed",
+ "has_details": true,
+ "details_path": "/gitlab-org/gitlab-runner/-/jobs/72469033",
+ "illustration": {
+ "image": "https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
+ "size": "svg-430",
+ "title": "This job does not have a trace."
+ },
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png",
+ "action": {
+ "icon": "retry",
+ "title": "Retry",
+ "path": "/gitlab-org/gitlab-runner/-/jobs/72469033/retry",
+ "method": "post",
+ "button_title": "Retry this job"
+ }
+ }
+ }
+ ]
+ }
+ ],
+ "status": {
+ "icon": "status_success",
+ "text": "passed",
+ "label": "passed",
+ "group": "success",
+ "tooltip": "passed",
+ "has_details": true,
+ "details_path": "/gitlab-org/gitlab-runner/pipelines/23211253#test",
+ "illustration": null,
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png"
+ },
+ "path": "/gitlab-org/gitlab-runner/pipelines/23211253#test",
+ "dropdown_path": "/gitlab-org/gitlab-runner/pipelines/23211253/stage.json?stage=test"
+ },
+ {
+ "name": "cleanup",
+ "title": "cleanup: skipped",
+ "groups": [
+ {
+ "name": "review-docs-cleanup",
+ "size": 1,
+ "status": {
+ "icon": "status_manual",
+ "text": "manual",
+ "label": "manual stop action",
+ "group": "manual",
+ "tooltip": "manual action",
+ "has_details": true,
+ "details_path": "/gitlab-org/gitlab-runner/-/jobs/72469034",
+ "illustration": {
+ "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg",
+ "size": "svg-394",
+ "title": "This job requires a manual action",
+ "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments"
+ },
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png",
+ "action": {
+ "icon": "stop",
+ "title": "Stop",
+ "path": "/gitlab-org/gitlab-runner/-/jobs/72469034/play",
+ "method": "post",
+ "button_title": "Stop this environment"
+ }
+ },
+ "jobs": [
+ {
+ "id": 72469034,
+ "name": "review-docs-cleanup",
+ "started": null,
+ "archived": false,
+ "build_path": "/gitlab-org/gitlab-runner/-/jobs/72469034",
+ "play_path": "/gitlab-org/gitlab-runner/-/jobs/72469034/play",
+ "playable": true,
+ "scheduled": false,
+ "created_at": "2018-06-05T11:31:30.760Z",
+ "updated_at": "2018-06-05T11:31:56.037Z",
+ "status": {
+ "icon": "status_manual",
+ "text": "manual",
+ "label": "manual stop action",
+ "group": "manual",
+ "tooltip": "manual action",
+ "has_details": true,
+ "details_path": "/gitlab-org/gitlab-runner/-/jobs/72469034",
+ "illustration": {
+ "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg",
+ "size": "svg-394",
+ "title": "This job requires a manual action",
+ "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments"
+ },
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png",
+ "action": {
+ "icon": "stop",
+ "title": "Stop",
+ "path": "/gitlab-org/gitlab-runner/-/jobs/72469034/play",
+ "method": "post",
+ "button_title": "Stop this environment"
+ }
+ }
+ }
+ ]
+ }
+ ],
+ "status": {
+ "icon": "status_skipped",
+ "text": "skipped",
+ "label": "skipped",
+ "group": "skipped",
+ "tooltip": "skipped",
+ "has_details": true,
+ "details_path": "/gitlab-org/gitlab-runner/pipelines/23211253#cleanup",
+ "illustration": null,
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png"
+ },
+ "path": "/gitlab-org/gitlab-runner/pipelines/23211253#cleanup",
+ "dropdown_path": "/gitlab-org/gitlab-runner/pipelines/23211253/stage.json?stage=cleanup"
+ }
+ ],
+ "artifacts": [],
+ "manual_actions": [
+ {
+ "name": "review-docs-cleanup",
+ "path": "/gitlab-org/gitlab-runner/-/jobs/72469034/play",
+ "playable": true,
+ "scheduled": false
+ },
+ {
+ "name": "review-docs-deploy",
+ "path": "/gitlab-org/gitlab-runner/-/jobs/72469032/play",
+ "playable": true,
+ "scheduled": false
+ }
+ ],
+ "scheduled_actions": []
+ },
+ "ref": {
+ "name": "docs/add-development-guide-to-readme",
+ "path": "/gitlab-org/gitlab-runner/commits/docs/add-development-guide-to-readme",
+ "tag": false,
+ "branch": true,
+ "merge_request": false
+ },
+ "commit": {
+ "id": "8083eb0a920572214d0dccedd7981f05d535ad46",
+ "short_id": "8083eb0a",
+ "title": "Add link to development guide in readme",
+ "created_at": "2018-06-05T11:30:48.000Z",
+ "parent_ids": ["1d7cf79b5a1a2121b9474ac20d61c1b8f621289d"],
+ "message": "Add link to development guide in readme\n\nCloses https://gitlab.com/gitlab-org/gitlab-runner/issues/3122\n",
+ "author_name": "Achilleas Pipinellis",
+ "author_email": "axil@gitlab.com",
+ "authored_date": "2018-06-05T11:30:48.000Z",
+ "committer_name": "Achilleas Pipinellis",
+ "committer_email": "axil@gitlab.com",
+ "committed_date": "2018-06-05T11:30:48.000Z",
+ "author": {
+ "id": 3585,
+ "name": "Achilleas Pipinellis",
+ "username": "axil",
+ "state": "active",
+ "avatar_url": "https://assets.gitlab-static.net/uploads/-/system/user/avatar/3585/avatar.png",
+ "web_url": "https://gitlab.com/axil",
+ "status_tooltip_html": null,
+ "path": "/axil"
+ },
+ "author_gravatar_url": "https://secure.gravatar.com/avatar/1d37af00eec153a8333a4ce18e9aea41?s=80\u0026d=identicon",
+ "commit_url": "https://gitlab.com/gitlab-org/gitlab-runner/commit/8083eb0a920572214d0dccedd7981f05d535ad46",
+ "commit_path": "/gitlab-org/gitlab-runner/commit/8083eb0a920572214d0dccedd7981f05d535ad46"
+ },
+ "triggered_by": {
+ "id": 12,
+ "user": {
+ "id": 376774,
+ "name": "Alessio Caiazza",
+ "username": "nolith",
+ "state": "active",
+ "avatar_url": "https://assets.gitlab-static.net/uploads/-/system/user/avatar/376774/avatar.png",
+ "web_url": "https://gitlab.com/nolith",
+ "status_tooltip_html": null,
+ "path": "/nolith"
+ },
+ "active": false,
+ "coverage": null,
+ "source": "pipeline",
+ "path": "/gitlab-com/gitlab-docs/pipelines/34993051",
+ "details": {
+ "status": {
+ "icon": "status_failed",
+ "text": "failed",
+ "label": "failed",
+ "group": "failed",
+ "tooltip": "failed",
+ "has_details": true,
+ "details_path": "/gitlab-com/gitlab-docs/pipelines/34993051",
+ "illustration": null,
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_failed-41304d7f7e3828808b0c26771f0309e55296819a9beea3ea9fbf6689d9857c12.png"
+ },
+ "duration": 118,
+ "finished_at": "2018-10-31T16:41:40.615Z",
+ "stages": [
+ {
+ "name": "build-images",
+ "title": "build-images: skipped",
+ "groups": [
+ {
+ "name": "image:bootstrap",
+ "size": 1,
+ "status": {
+ "icon": "status_manual",
+ "text": "manual",
+ "label": "manual play action",
+ "group": "manual",
+ "tooltip": "manual action",
+ "has_details": true,
+ "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982853",
+ "illustration": {
+ "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg",
+ "size": "svg-394",
+ "title": "This job requires a manual action",
+ "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments"
+ },
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png",
+ "action": {
+ "icon": "play",
+ "title": "Play",
+ "path": "/gitlab-com/gitlab-docs/-/jobs/114982853/play",
+ "method": "post",
+ "button_title": "Trigger this manual action"
+ }
+ },
+ "jobs": [
+ {
+ "id": 11421321982853,
+ "name": "image:bootstrap",
+ "started": null,
+ "archived": false,
+ "build_path": "/gitlab-com/gitlab-docs/-/jobs/114982853",
+ "play_path": "/gitlab-com/gitlab-docs/-/jobs/114982853/play",
+ "playable": true,
+ "scheduled": false,
+ "created_at": "2018-10-31T16:35:23.704Z",
+ "updated_at": "2018-10-31T16:35:24.118Z",
+ "status": {
+ "icon": "status_manual",
+ "text": "manual",
+ "label": "manual play action",
+ "group": "manual",
+ "tooltip": "manual action",
+ "has_details": true,
+ "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982853",
+ "illustration": {
+ "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg",
+ "size": "svg-394",
+ "title": "This job requires a manual action",
+ "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments"
+ },
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png",
+ "action": {
+ "icon": "play",
+ "title": "Play",
+ "path": "/gitlab-com/gitlab-docs/-/jobs/114982853/play",
+ "method": "post",
+ "button_title": "Trigger this manual action"
+ }
+ }
+ }
+ ]
+ },
+ {
+ "name": "image:builder-onbuild",
+ "size": 1,
+ "status": {
+ "icon": "status_manual",
+ "text": "manual",
+ "label": "manual play action",
+ "group": "manual",
+ "tooltip": "manual action",
+ "has_details": true,
+ "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982854",
+ "illustration": {
+ "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg",
+ "size": "svg-394",
+ "title": "This job requires a manual action",
+ "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments"
+ },
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png",
+ "action": {
+ "icon": "play",
+ "title": "Play",
+ "path": "/gitlab-com/gitlab-docs/-/jobs/114982854/play",
+ "method": "post",
+ "button_title": "Trigger this manual action"
+ }
+ },
+ "jobs": [
+ {
+ "id": 1149822131854,
+ "name": "image:builder-onbuild",
+ "started": null,
+ "archived": false,
+ "build_path": "/gitlab-com/gitlab-docs/-/jobs/114982854",
+ "play_path": "/gitlab-com/gitlab-docs/-/jobs/114982854/play",
+ "playable": true,
+ "scheduled": false,
+ "created_at": "2018-10-31T16:35:23.728Z",
+ "updated_at": "2018-10-31T16:35:24.070Z",
+ "status": {
+ "icon": "status_manual",
+ "text": "manual",
+ "label": "manual play action",
+ "group": "manual",
+ "tooltip": "manual action",
+ "has_details": true,
+ "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982854",
+ "illustration": {
+ "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg",
+ "size": "svg-394",
+ "title": "This job requires a manual action",
+ "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments"
+ },
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png",
+ "action": {
+ "icon": "play",
+ "title": "Play",
+ "path": "/gitlab-com/gitlab-docs/-/jobs/114982854/play",
+ "method": "post",
+ "button_title": "Trigger this manual action"
+ }
+ }
+ }
+ ]
+ },
+ {
+ "name": "image:nginx-onbuild",
+ "size": 1,
+ "status": {
+ "icon": "status_manual",
+ "text": "manual",
+ "label": "manual play action",
+ "group": "manual",
+ "tooltip": "manual action",
+ "has_details": true,
+ "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982855",
+ "illustration": {
+ "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg",
+ "size": "svg-394",
+ "title": "This job requires a manual action",
+ "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments"
+ },
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png",
+ "action": {
+ "icon": "play",
+ "title": "Play",
+ "path": "/gitlab-com/gitlab-docs/-/jobs/114982855/play",
+ "method": "post",
+ "button_title": "Trigger this manual action"
+ }
+ },
+ "jobs": [
+ {
+ "id": 11498285523424,
+ "name": "image:nginx-onbuild",
+ "started": null,
+ "archived": false,
+ "build_path": "/gitlab-com/gitlab-docs/-/jobs/114982855",
+ "play_path": "/gitlab-com/gitlab-docs/-/jobs/114982855/play",
+ "playable": true,
+ "scheduled": false,
+ "created_at": "2018-10-31T16:35:23.753Z",
+ "updated_at": "2018-10-31T16:35:24.033Z",
+ "status": {
+ "icon": "status_manual",
+ "text": "manual",
+ "label": "manual play action",
+ "group": "manual",
+ "tooltip": "manual action",
+ "has_details": true,
+ "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982855",
+ "illustration": {
+ "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg",
+ "size": "svg-394",
+ "title": "This job requires a manual action",
+ "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments"
+ },
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png",
+ "action": {
+ "icon": "play",
+ "title": "Play",
+ "path": "/gitlab-com/gitlab-docs/-/jobs/114982855/play",
+ "method": "post",
+ "button_title": "Trigger this manual action"
+ }
+ }
+ }
+ ]
+ }
+ ],
+ "status": {
+ "icon": "status_skipped",
+ "text": "skipped",
+ "label": "skipped",
+ "group": "skipped",
+ "tooltip": "skipped",
+ "has_details": true,
+ "details_path": "/gitlab-com/gitlab-docs/pipelines/34993051#build-images",
+ "illustration": null,
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png"
+ },
+ "path": "/gitlab-com/gitlab-docs/pipelines/34993051#build-images",
+ "dropdown_path": "/gitlab-com/gitlab-docs/pipelines/34993051/stage.json?stage=build-images"
+ },
+ {
+ "name": "build",
+ "title": "build: failed",
+ "groups": [
+ {
+ "name": "compile_dev",
+ "size": 1,
+ "status": {
+ "icon": "status_failed",
+ "text": "failed",
+ "label": "failed",
+ "group": "failed",
+ "tooltip": "failed - (script failure)",
+ "has_details": true,
+ "details_path": "/gitlab-com/gitlab-docs/-/jobs/114984694",
+ "illustration": {
+ "image": "https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
+ "size": "svg-430",
+ "title": "This job does not have a trace."
+ },
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_failed-41304d7f7e3828808b0c26771f0309e55296819a9beea3ea9fbf6689d9857c12.png",
+ "action": {
+ "icon": "retry",
+ "title": "Retry",
+ "path": "/h5bp/html5-boilerplate/-/jobs/528/retry",
+ "method": "post",
+ "button_title": "Retry this job"
+ }
+ },
+ "jobs": [
+ {
+ "id": 1149846949786,
+ "name": "compile_dev",
+ "started": "2018-10-31T16:39:41.598Z",
+ "archived": false,
+ "build_path": "/gitlab-com/gitlab-docs/-/jobs/114984694",
+ "retry_path": "/gitlab-com/gitlab-docs/-/jobs/114984694/retry",
+ "playable": false,
+ "scheduled": false,
+ "created_at": "2018-10-31T16:39:41.138Z",
+ "updated_at": "2018-10-31T16:41:40.072Z",
+ "status": {
+ "icon": "status_failed",
+ "text": "failed",
+ "label": "failed",
+ "group": "failed",
+ "tooltip": "failed - (script failure)",
+ "has_details": true,
+ "details_path": "/gitlab-com/gitlab-docs/-/jobs/114984694",
+ "illustration": {
+ "image": "https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
+ "size": "svg-430",
+ "title": "This job does not have a trace."
+ },
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_failed-41304d7f7e3828808b0c26771f0309e55296819a9beea3ea9fbf6689d9857c12.png",
+ "action": {
+ "icon": "retry",
+ "title": "Retry",
+ "path": "/h5bp/html5-boilerplate/-/jobs/528/retry",
+ "method": "post",
+ "button_title": "Retry this job"
+ }
+ },
+ "recoverable": false
+ }
+ ]
+ }
+ ],
+ "status": {
+ "icon": "status_failed",
+ "text": "failed",
+ "label": "failed",
+ "group": "failed",
+ "tooltip": "failed",
+ "has_details": true,
+ "details_path": "/gitlab-com/gitlab-docs/pipelines/34993051#build",
+ "illustration": null,
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_failed-41304d7f7e3828808b0c26771f0309e55296819a9beea3ea9fbf6689d9857c12.png"
+ },
+ "path": "/gitlab-com/gitlab-docs/pipelines/34993051#build",
+ "dropdown_path": "/gitlab-com/gitlab-docs/pipelines/34993051/stage.json?stage=build"
+ },
+ {
+ "name": "deploy",
+ "title": "deploy: skipped",
+ "groups": [
+ {
+ "name": "review",
+ "size": 1,
+ "status": {
+ "icon": "status_skipped",
+ "text": "skipped",
+ "label": "skipped",
+ "group": "skipped",
+ "tooltip": "skipped",
+ "has_details": true,
+ "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982857",
+ "illustration": {
+ "image": "https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
+ "size": "svg-430",
+ "title": "This job has been skipped"
+ },
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png"
+ },
+ "jobs": [
+ {
+ "id": 11498282342357,
+ "name": "review",
+ "started": null,
+ "archived": false,
+ "build_path": "/gitlab-com/gitlab-docs/-/jobs/114982857",
+ "playable": false,
+ "scheduled": false,
+ "created_at": "2018-10-31T16:35:23.805Z",
+ "updated_at": "2018-10-31T16:41:40.569Z",
+ "status": {
+ "icon": "status_skipped",
+ "text": "skipped",
+ "label": "skipped",
+ "group": "skipped",
+ "tooltip": "skipped",
+ "has_details": true,
+ "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982857",
+ "illustration": {
+ "image": "https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
+ "size": "svg-430",
+ "title": "This job has been skipped"
+ },
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png"
+ }
+ }
+ ]
+ },
+ {
+ "name": "review_stop",
+ "size": 1,
+ "status": {
+ "icon": "status_skipped",
+ "text": "skipped",
+ "label": "skipped",
+ "group": "skipped",
+ "tooltip": "skipped",
+ "has_details": true,
+ "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982858",
+ "illustration": {
+ "image": "https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
+ "size": "svg-430",
+ "title": "This job has been skipped"
+ },
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png"
+ },
+ "jobs": [
+ {
+ "id": 114982858,
+ "name": "review_stop",
+ "started": null,
+ "archived": false,
+ "build_path": "/gitlab-com/gitlab-docs/-/jobs/114982858",
+ "playable": false,
+ "scheduled": false,
+ "created_at": "2018-10-31T16:35:23.840Z",
+ "updated_at": "2018-10-31T16:41:40.480Z",
+ "status": {
+ "icon": "status_skipped",
+ "text": "skipped",
+ "label": "skipped",
+ "group": "skipped",
+ "tooltip": "skipped",
+ "has_details": true,
+ "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982858",
+ "illustration": {
+ "image": "https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
+ "size": "svg-430",
+ "title": "This job has been skipped"
+ },
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png"
+ }
+ }
+ ]
+ }
+ ],
+ "status": {
+ "icon": "status_skipped",
+ "text": "skipped",
+ "label": "skipped",
+ "group": "skipped",
+ "tooltip": "skipped",
+ "has_details": true,
+ "details_path": "/gitlab-com/gitlab-docs/pipelines/34993051#deploy",
+ "illustration": null,
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png"
+ },
+ "path": "/gitlab-com/gitlab-docs/pipelines/34993051#deploy",
+ "dropdown_path": "/gitlab-com/gitlab-docs/pipelines/34993051/stage.json?stage=deploy"
+ }
+ ],
+ "artifacts": [],
+ "manual_actions": [
+ {
+ "name": "image:bootstrap",
+ "path": "/gitlab-com/gitlab-docs/-/jobs/114982853/play",
+ "playable": true,
+ "scheduled": false
+ },
+ {
+ "name": "image:builder-onbuild",
+ "path": "/gitlab-com/gitlab-docs/-/jobs/114982854/play",
+ "playable": true,
+ "scheduled": false
+ },
+ {
+ "name": "image:nginx-onbuild",
+ "path": "/gitlab-com/gitlab-docs/-/jobs/114982855/play",
+ "playable": true,
+ "scheduled": false
+ },
+ {
+ "name": "review_stop",
+ "path": "/gitlab-com/gitlab-docs/-/jobs/114982858/play",
+ "playable": false,
+ "scheduled": false
+ }
+ ],
+ "scheduled_actions": []
+ },
+ "project": {
+ "id": 1794617,
+ "name": "Test",
+ "full_path": "/gitlab-com/gitlab-docs",
+ "full_name": "GitLab.com / GitLab Docs"
+ },
+ "triggered_by": {
+ "id": 349932310342451,
+ "user": {
+ "id": 376774,
+ "name": "Alessio Caiazza",
+ "username": "nolith",
+ "state": "active",
+ "avatar_url": "https://assets.gitlab-static.net/uploads/-/system/user/avatar/376774/avatar.png",
+ "web_url": "https://gitlab.com/nolith",
+ "status_tooltip_html": null,
+ "path": "/nolith"
+ },
+ "active": false,
+ "coverage": null,
+ "source": "pipeline",
+ "path": "/gitlab-com/gitlab-docs/pipelines/34993051",
+ "details": {
+ "status": {
+ "icon": "status_failed",
+ "text": "failed",
+ "label": "failed",
+ "group": "failed",
+ "tooltip": "failed",
+ "has_details": true,
+ "details_path": "/gitlab-com/gitlab-docs/pipelines/34993051",
+ "illustration": null,
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_failed-41304d7f7e3828808b0c26771f0309e55296819a9beea3ea9fbf6689d9857c12.png"
+ },
+ "duration": 118,
+ "finished_at": "2018-10-31T16:41:40.615Z",
+ "stages": [
+ {
+ "name": "build-images",
+ "title": "build-images: skipped",
+ "groups": [
+ {
+ "name": "image:bootstrap",
+ "size": 1,
+ "status": {
+ "icon": "status_manual",
+ "text": "manual",
+ "label": "manual play action",
+ "group": "manual",
+ "tooltip": "manual action",
+ "has_details": true,
+ "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982853",
+ "illustration": {
+ "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg",
+ "size": "svg-394",
+ "title": "This job requires a manual action",
+ "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments"
+ },
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png",
+ "action": {
+ "icon": "play",
+ "title": "Play",
+ "path": "/gitlab-com/gitlab-docs/-/jobs/114982853/play",
+ "method": "post",
+ "button_title": "Trigger this manual action"
+ }
+ },
+ "jobs": [
+ {
+ "id": 11421321982853,
+ "name": "image:bootstrap",
+ "started": null,
+ "archived": false,
+ "build_path": "/gitlab-com/gitlab-docs/-/jobs/114982853",
+ "play_path": "/gitlab-com/gitlab-docs/-/jobs/114982853/play",
+ "playable": true,
+ "scheduled": false,
+ "created_at": "2018-10-31T16:35:23.704Z",
+ "updated_at": "2018-10-31T16:35:24.118Z",
+ "status": {
+ "icon": "status_manual",
+ "text": "manual",
+ "label": "manual play action",
+ "group": "manual",
+ "tooltip": "manual action",
+ "has_details": true,
+ "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982853",
+ "illustration": {
+ "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg",
+ "size": "svg-394",
+ "title": "This job requires a manual action",
+ "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments"
+ },
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png",
+ "action": {
+ "icon": "play",
+ "title": "Play",
+ "path": "/gitlab-com/gitlab-docs/-/jobs/114982853/play",
+ "method": "post",
+ "button_title": "Trigger this manual action"
+ }
+ }
+ }
+ ]
+ },
+ {
+ "name": "image:builder-onbuild",
+ "size": 1,
+ "status": {
+ "icon": "status_manual",
+ "text": "manual",
+ "label": "manual play action",
+ "group": "manual",
+ "tooltip": "manual action",
+ "has_details": true,
+ "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982854",
+ "illustration": {
+ "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg",
+ "size": "svg-394",
+ "title": "This job requires a manual action",
+ "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments"
+ },
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png",
+ "action": {
+ "icon": "play",
+ "title": "Play",
+ "path": "/gitlab-com/gitlab-docs/-/jobs/114982854/play",
+ "method": "post",
+ "button_title": "Trigger this manual action"
+ }
+ },
+ "jobs": [
+ {
+ "id": 1149822131854,
+ "name": "image:builder-onbuild",
+ "started": null,
+ "archived": false,
+ "build_path": "/gitlab-com/gitlab-docs/-/jobs/114982854",
+ "play_path": "/gitlab-com/gitlab-docs/-/jobs/114982854/play",
+ "playable": true,
+ "scheduled": false,
+ "created_at": "2018-10-31T16:35:23.728Z",
+ "updated_at": "2018-10-31T16:35:24.070Z",
+ "status": {
+ "icon": "status_manual",
+ "text": "manual",
+ "label": "manual play action",
+ "group": "manual",
+ "tooltip": "manual action",
+ "has_details": true,
+ "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982854",
+ "illustration": {
+ "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg",
+ "size": "svg-394",
+ "title": "This job requires a manual action",
+ "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments"
+ },
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png",
+ "action": {
+ "icon": "play",
+ "title": "Play",
+ "path": "/gitlab-com/gitlab-docs/-/jobs/114982854/play",
+ "method": "post",
+ "button_title": "Trigger this manual action"
+ }
+ }
+ }
+ ]
+ },
+ {
+ "name": "image:nginx-onbuild",
+ "size": 1,
+ "status": {
+ "icon": "status_manual",
+ "text": "manual",
+ "label": "manual play action",
+ "group": "manual",
+ "tooltip": "manual action",
+ "has_details": true,
+ "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982855",
+ "illustration": {
+ "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg",
+ "size": "svg-394",
+ "title": "This job requires a manual action",
+ "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments"
+ },
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png",
+ "action": {
+ "icon": "play",
+ "title": "Play",
+ "path": "/gitlab-com/gitlab-docs/-/jobs/114982855/play",
+ "method": "post",
+ "button_title": "Trigger this manual action"
+ }
+ },
+ "jobs": [
+ {
+ "id": 11498285523424,
+ "name": "image:nginx-onbuild",
+ "started": null,
+ "archived": false,
+ "build_path": "/gitlab-com/gitlab-docs/-/jobs/114982855",
+ "play_path": "/gitlab-com/gitlab-docs/-/jobs/114982855/play",
+ "playable": true,
+ "scheduled": false,
+ "created_at": "2018-10-31T16:35:23.753Z",
+ "updated_at": "2018-10-31T16:35:24.033Z",
+ "status": {
+ "icon": "status_manual",
+ "text": "manual",
+ "label": "manual play action",
+ "group": "manual",
+ "tooltip": "manual action",
+ "has_details": true,
+ "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982855",
+ "illustration": {
+ "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg",
+ "size": "svg-394",
+ "title": "This job requires a manual action",
+ "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments"
+ },
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png",
+ "action": {
+ "icon": "play",
+ "title": "Play",
+ "path": "/gitlab-com/gitlab-docs/-/jobs/114982855/play",
+ "method": "post",
+ "button_title": "Trigger this manual action"
+ }
+ }
+ }
+ ]
+ }
+ ],
+ "status": {
+ "icon": "status_skipped",
+ "text": "skipped",
+ "label": "skipped",
+ "group": "skipped",
+ "tooltip": "skipped",
+ "has_details": true,
+ "details_path": "/gitlab-com/gitlab-docs/pipelines/34993051#build-images",
+ "illustration": null,
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png"
+ },
+ "path": "/gitlab-com/gitlab-docs/pipelines/34993051#build-images",
+ "dropdown_path": "/gitlab-com/gitlab-docs/pipelines/34993051/stage.json?stage=build-images"
+ },
+ {
+ "name": "build",
+ "title": "build: failed",
+ "groups": [
+ {
+ "name": "compile_dev",
+ "size": 1,
+ "status": {
+ "icon": "status_failed",
+ "text": "failed",
+ "label": "failed",
+ "group": "failed",
+ "tooltip": "failed - (script failure)",
+ "has_details": true,
+ "details_path": "/gitlab-com/gitlab-docs/-/jobs/114984694",
+ "illustration": {
+ "image": "https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
+ "size": "svg-430",
+ "title": "This job does not have a trace."
+ },
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_failed-41304d7f7e3828808b0c26771f0309e55296819a9beea3ea9fbf6689d9857c12.png",
+ "action": {
+ "icon": "retry",
+ "title": "Retry",
+ "path": "/gitlab-com/gitlab-docs/-/jobs/114984694/retry",
+ "method": "post",
+ "button_title": "Retry this job"
+ }
+ },
+ "jobs": [
+ {
+ "id": 1149846949786,
+ "name": "compile_dev",
+ "started": "2018-10-31T16:39:41.598Z",
+ "archived": false,
+ "build_path": "/gitlab-com/gitlab-docs/-/jobs/114984694",
+ "retry_path": "/gitlab-com/gitlab-docs/-/jobs/114984694/retry",
+ "playable": false,
+ "scheduled": false,
+ "created_at": "2018-10-31T16:39:41.138Z",
+ "updated_at": "2018-10-31T16:41:40.072Z",
+ "status": {
+ "icon": "status_failed",
+ "text": "failed",
+ "label": "failed",
+ "group": "failed",
+ "tooltip": "failed - (script failure)",
+ "has_details": true,
+ "details_path": "/gitlab-com/gitlab-docs/-/jobs/114984694",
+ "illustration": {
+ "image": "https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
+ "size": "svg-430",
+ "title": "This job does not have a trace."
+ },
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_failed-41304d7f7e3828808b0c26771f0309e55296819a9beea3ea9fbf6689d9857c12.png",
+ "action": {
+ "icon": "retry",
+ "title": "Retry",
+ "path": "/gitlab-com/gitlab-docs/-/jobs/114984694/retry",
+ "method": "post",
+ "button_title": "Retry this job"
+ }
+ },
+ "recoverable": false
+ }
+ ]
+ }
+ ],
+ "status": {
+ "icon": "status_failed",
+ "text": "failed",
+ "label": "failed",
+ "group": "failed",
+ "tooltip": "failed",
+ "has_details": true,
+ "details_path": "/gitlab-com/gitlab-docs/pipelines/34993051#build",
+ "illustration": null,
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_failed-41304d7f7e3828808b0c26771f0309e55296819a9beea3ea9fbf6689d9857c12.png"
+ },
+ "path": "/gitlab-com/gitlab-docs/pipelines/34993051#build",
+ "dropdown_path": "/gitlab-com/gitlab-docs/pipelines/34993051/stage.json?stage=build"
+ },
+ {
+ "name": "deploy",
+ "title": "deploy: skipped",
+ "groups": [
+ {
+ "name": "review",
+ "size": 1,
+ "status": {
+ "icon": "status_skipped",
+ "text": "skipped",
+ "label": "skipped",
+ "group": "skipped",
+ "tooltip": "skipped",
+ "has_details": true,
+ "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982857",
+ "illustration": {
+ "image": "https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
+ "size": "svg-430",
+ "title": "This job has been skipped"
+ },
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png"
+ },
+ "jobs": [
+ {
+ "id": 11498282342357,
+ "name": "review",
+ "started": null,
+ "archived": false,
+ "build_path": "/gitlab-com/gitlab-docs/-/jobs/114982857",
+ "playable": false,
+ "scheduled": false,
+ "created_at": "2018-10-31T16:35:23.805Z",
+ "updated_at": "2018-10-31T16:41:40.569Z",
+ "status": {
+ "icon": "status_skipped",
+ "text": "skipped",
+ "label": "skipped",
+ "group": "skipped",
+ "tooltip": "skipped",
+ "has_details": true,
+ "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982857",
+ "illustration": {
+ "image": "https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
+ "size": "svg-430",
+ "title": "This job has been skipped"
+ },
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png"
+ }
+ }
+ ]
+ },
+ {
+ "name": "review_stop",
+ "size": 1,
+ "status": {
+ "icon": "status_skipped",
+ "text": "skipped",
+ "label": "skipped",
+ "group": "skipped",
+ "tooltip": "skipped",
+ "has_details": true,
+ "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982858",
+ "illustration": {
+ "image": "https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
+ "size": "svg-430",
+ "title": "This job has been skipped"
+ },
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png"
+ },
+ "jobs": [
+ {
+ "id": 114982858,
+ "name": "review_stop",
+ "started": null,
+ "archived": false,
+ "build_path": "/gitlab-com/gitlab-docs/-/jobs/114982858",
+ "playable": false,
+ "scheduled": false,
+ "created_at": "2018-10-31T16:35:23.840Z",
+ "updated_at": "2018-10-31T16:41:40.480Z",
+ "status": {
+ "icon": "status_skipped",
+ "text": "skipped",
+ "label": "skipped",
+ "group": "skipped",
+ "tooltip": "skipped",
+ "has_details": true,
+ "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982858",
+ "illustration": {
+ "image": "https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
+ "size": "svg-430",
+ "title": "This job has been skipped"
+ },
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png"
+ }
+ }
+ ]
+ }
+ ],
+ "status": {
+ "icon": "status_skipped",
+ "text": "skipped",
+ "label": "skipped",
+ "group": "skipped",
+ "tooltip": "skipped",
+ "has_details": true,
+ "details_path": "/gitlab-com/gitlab-docs/pipelines/34993051#deploy",
+ "illustration": null,
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png"
+ },
+ "path": "/gitlab-com/gitlab-docs/pipelines/34993051#deploy",
+ "dropdown_path": "/gitlab-com/gitlab-docs/pipelines/34993051/stage.json?stage=deploy"
+ }
+ ],
+ "artifacts": [],
+ "manual_actions": [
+ {
+ "name": "image:bootstrap",
+ "path": "/gitlab-com/gitlab-docs/-/jobs/114982853/play",
+ "playable": true,
+ "scheduled": false
+ },
+ {
+ "name": "image:builder-onbuild",
+ "path": "/gitlab-com/gitlab-docs/-/jobs/114982854/play",
+ "playable": true,
+ "scheduled": false
+ },
+ {
+ "name": "image:nginx-onbuild",
+ "path": "/gitlab-com/gitlab-docs/-/jobs/114982855/play",
+ "playable": true,
+ "scheduled": false
+ },
+ {
+ "name": "review_stop",
+ "path": "/gitlab-com/gitlab-docs/-/jobs/114982858/play",
+ "playable": false,
+ "scheduled": false
+ }
+ ],
+ "scheduled_actions": []
+ },
+ "project": {
+ "id": 1794617,
+ "name": "GitLab Docs",
+ "full_path": "/gitlab-com/gitlab-docs",
+ "full_name": "GitLab.com / GitLab Docs"
+ }
+ },
+ "triggered": []
+ },
+ "triggered": [
+ {
+ "id": 34993051,
+ "user": {
+ "id": 376774,
+ "name": "Alessio Caiazza",
+ "username": "nolith",
+ "state": "active",
+ "avatar_url": "https://assets.gitlab-static.net/uploads/-/system/user/avatar/376774/avatar.png",
+ "web_url": "https://gitlab.com/nolith",
+ "status_tooltip_html": null,
+ "path": "/nolith"
+ },
+ "active": false,
+ "coverage": null,
+ "source": "pipeline",
+ "path": "/gitlab-com/gitlab-docs/pipelines/34993051",
+ "details": {
+ "status": {
+ "icon": "status_failed",
+ "text": "failed",
+ "label": "failed",
+ "group": "failed",
+ "tooltip": "failed",
+ "has_details": true,
+ "details_path": "/gitlab-com/gitlab-docs/pipelines/34993051",
+ "illustration": null,
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_failed-41304d7f7e3828808b0c26771f0309e55296819a9beea3ea9fbf6689d9857c12.png"
+ },
+ "duration": 118,
+ "finished_at": "2018-10-31T16:41:40.615Z",
+ "stages": [
+ {
+ "name": "build-images",
+ "title": "build-images: skipped",
+ "groups": [
+ {
+ "name": "image:bootstrap",
+ "size": 1,
+ "status": {
+ "icon": "status_manual",
+ "text": "manual",
+ "label": "manual play action",
+ "group": "manual",
+ "tooltip": "manual action",
+ "has_details": true,
+ "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982853",
+ "illustration": {
+ "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg",
+ "size": "svg-394",
+ "title": "This job requires a manual action",
+ "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments"
+ },
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png",
+ "action": {
+ "icon": "play",
+ "title": "Play",
+ "path": "/gitlab-com/gitlab-docs/-/jobs/114982853/play",
+ "method": "post",
+ "button_title": "Trigger this manual action"
+ }
+ },
+ "jobs": [
+ {
+ "id": 114982853,
+ "name": "image:bootstrap",
+ "started": null,
+ "archived": false,
+ "build_path": "/gitlab-com/gitlab-docs/-/jobs/114982853",
+ "play_path": "/gitlab-com/gitlab-docs/-/jobs/114982853/play",
+ "playable": true,
+ "scheduled": false,
+ "created_at": "2018-10-31T16:35:23.704Z",
+ "updated_at": "2018-10-31T16:35:24.118Z",
+ "status": {
+ "icon": "status_manual",
+ "text": "manual",
+ "label": "manual play action",
+ "group": "manual",
+ "tooltip": "manual action",
+ "has_details": true,
+ "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982853",
+ "illustration": {
+ "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg",
+ "size": "svg-394",
+ "title": "This job requires a manual action",
+ "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments"
+ },
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png",
+ "action": {
+ "icon": "play",
+ "title": "Play",
+ "path": "/gitlab-com/gitlab-docs/-/jobs/114982853/play",
+ "method": "post",
+ "button_title": "Trigger this manual action"
+ }
+ }
+ }
+ ]
+ },
+ {
+ "name": "image:builder-onbuild",
+ "size": 1,
+ "status": {
+ "icon": "status_manual",
+ "text": "manual",
+ "label": "manual play action",
+ "group": "manual",
+ "tooltip": "manual action",
+ "has_details": true,
+ "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982854",
+ "illustration": {
+ "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg",
+ "size": "svg-394",
+ "title": "This job requires a manual action",
+ "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments"
+ },
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png",
+ "action": {
+ "icon": "play",
+ "title": "Play",
+ "path": "/gitlab-com/gitlab-docs/-/jobs/114982854/play",
+ "method": "post",
+ "button_title": "Trigger this manual action"
+ }
+ },
+ "jobs": [
+ {
+ "id": 114982854,
+ "name": "image:builder-onbuild",
+ "started": null,
+ "archived": false,
+ "build_path": "/gitlab-com/gitlab-docs/-/jobs/114982854",
+ "play_path": "/gitlab-com/gitlab-docs/-/jobs/114982854/play",
+ "playable": true,
+ "scheduled": false,
+ "created_at": "2018-10-31T16:35:23.728Z",
+ "updated_at": "2018-10-31T16:35:24.070Z",
+ "status": {
+ "icon": "status_manual",
+ "text": "manual",
+ "label": "manual play action",
+ "group": "manual",
+ "tooltip": "manual action",
+ "has_details": true,
+ "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982854",
+ "illustration": {
+ "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg",
+ "size": "svg-394",
+ "title": "This job requires a manual action",
+ "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments"
+ },
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png",
+ "action": {
+ "icon": "play",
+ "title": "Play",
+ "path": "/gitlab-com/gitlab-docs/-/jobs/114982854/play",
+ "method": "post",
+ "button_title": "Trigger this manual action"
+ }
+ }
+ }
+ ]
+ },
+ {
+ "name": "image:nginx-onbuild",
+ "size": 1,
+ "status": {
+ "icon": "status_manual",
+ "text": "manual",
+ "label": "manual play action",
+ "group": "manual",
+ "tooltip": "manual action",
+ "has_details": true,
+ "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982855",
+ "illustration": {
+ "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg",
+ "size": "svg-394",
+ "title": "This job requires a manual action",
+ "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments"
+ },
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png",
+ "action": {
+ "icon": "play",
+ "title": "Play",
+ "path": "/gitlab-com/gitlab-docs/-/jobs/114982855/play",
+ "method": "post",
+ "button_title": "Trigger this manual action"
+ }
+ },
+ "jobs": [
+ {
+ "id": 114982855,
+ "name": "image:nginx-onbuild",
+ "started": null,
+ "archived": false,
+ "build_path": "/gitlab-com/gitlab-docs/-/jobs/114982855",
+ "play_path": "/gitlab-com/gitlab-docs/-/jobs/114982855/play",
+ "playable": true,
+ "scheduled": false,
+ "created_at": "2018-10-31T16:35:23.753Z",
+ "updated_at": "2018-10-31T16:35:24.033Z",
+ "status": {
+ "icon": "status_manual",
+ "text": "manual",
+ "label": "manual play action",
+ "group": "manual",
+ "tooltip": "manual action",
+ "has_details": true,
+ "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982855",
+ "illustration": {
+ "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg",
+ "size": "svg-394",
+ "title": "This job requires a manual action",
+ "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments"
+ },
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png",
+ "action": {
+ "icon": "play",
+ "title": "Play",
+ "path": "/gitlab-com/gitlab-docs/-/jobs/114982855/play",
+ "method": "post",
+ "button_title": "Trigger this manual action"
+ }
+ }
+ }
+ ]
+ }
+ ],
+ "status": {
+ "icon": "status_skipped",
+ "text": "skipped",
+ "label": "skipped",
+ "group": "skipped",
+ "tooltip": "skipped",
+ "has_details": true,
+ "details_path": "/gitlab-com/gitlab-docs/pipelines/34993051#build-images",
+ "illustration": null,
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png"
+ },
+ "path": "/gitlab-com/gitlab-docs/pipelines/34993051#build-images",
+ "dropdown_path": "/gitlab-com/gitlab-docs/pipelines/34993051/stage.json?stage=build-images"
+ },
+ {
+ "name": "build",
+ "title": "build: failed",
+ "groups": [
+ {
+ "name": "compile_dev",
+ "size": 1,
+ "status": {
+ "icon": "status_failed",
+ "text": "failed",
+ "label": "failed",
+ "group": "failed",
+ "tooltip": "failed - (script failure)",
+ "has_details": true,
+ "details_path": "/gitlab-com/gitlab-docs/-/jobs/114984694",
+ "illustration": {
+ "image": "https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
+ "size": "svg-430",
+ "title": "This job does not have a trace."
+ },
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_failed-41304d7f7e3828808b0c26771f0309e55296819a9beea3ea9fbf6689d9857c12.png",
+ "action": {
+ "icon": "retry",
+ "title": "Retry",
+ "path": "/h5bp/html5-boilerplate/-/jobs/528/retry",
+ "method": "post",
+ "button_title": "Retry this job"
+ }
+ },
+ "jobs": [
+ {
+ "id": 114984694,
+ "name": "compile_dev",
+ "started": "2018-10-31T16:39:41.598Z",
+ "archived": false,
+ "build_path": "/gitlab-com/gitlab-docs/-/jobs/114984694",
+ "retry_path": "/gitlab-com/gitlab-docs/-/jobs/114984694/retry",
+ "playable": false,
+ "scheduled": false,
+ "created_at": "2018-10-31T16:39:41.138Z",
+ "updated_at": "2018-10-31T16:41:40.072Z",
+ "status": {
+ "icon": "status_failed",
+ "text": "failed",
+ "label": "failed",
+ "group": "failed",
+ "tooltip": "failed - (script failure)",
+ "has_details": true,
+ "details_path": "/gitlab-com/gitlab-docs/-/jobs/114984694",
+ "illustration": {
+ "image": "https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
+ "size": "svg-430",
+ "title": "This job does not have a trace."
+ },
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_failed-41304d7f7e3828808b0c26771f0309e55296819a9beea3ea9fbf6689d9857c12.png",
+ "action": {
+ "icon": "retry",
+ "title": "Retry",
+ "path": "/h5bp/html5-boilerplate/-/jobs/528/retry",
+ "method": "post",
+ "button_title": "Retry this job"
+ }
+ },
+ "recoverable": false
+ }
+ ]
+ }
+ ],
+ "status": {
+ "icon": "status_failed",
+ "text": "failed",
+ "label": "failed",
+ "group": "failed",
+ "tooltip": "failed",
+ "has_details": true,
+ "details_path": "/gitlab-com/gitlab-docs/pipelines/34993051#build",
+ "illustration": null,
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_failed-41304d7f7e3828808b0c26771f0309e55296819a9beea3ea9fbf6689d9857c12.png"
+ },
+ "path": "/gitlab-com/gitlab-docs/pipelines/34993051#build",
+ "dropdown_path": "/gitlab-com/gitlab-docs/pipelines/34993051/stage.json?stage=build"
+ },
+ {
+ "name": "deploy",
+ "title": "deploy: skipped",
+ "groups": [
+ {
+ "name": "review",
+ "size": 1,
+ "status": {
+ "icon": "status_skipped",
+ "text": "skipped",
+ "label": "skipped",
+ "group": "skipped",
+ "tooltip": "skipped",
+ "has_details": true,
+ "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982857",
+ "illustration": {
+ "image": "https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
+ "size": "svg-430",
+ "title": "This job has been skipped"
+ },
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png"
+ },
+ "jobs": [
+ {
+ "id": 114982857,
+ "name": "review",
+ "started": null,
+ "archived": false,
+ "build_path": "/gitlab-com/gitlab-docs/-/jobs/114982857",
+ "playable": false,
+ "scheduled": false,
+ "created_at": "2018-10-31T16:35:23.805Z",
+ "updated_at": "2018-10-31T16:41:40.569Z",
+ "status": {
+ "icon": "status_skipped",
+ "text": "skipped",
+ "label": "skipped",
+ "group": "skipped",
+ "tooltip": "skipped",
+ "has_details": true,
+ "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982857",
+ "illustration": {
+ "image": "https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
+ "size": "svg-430",
+ "title": "This job has been skipped"
+ },
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png"
+ }
+ }
+ ]
+ },
+ {
+ "name": "review_stop",
+ "size": 1,
+ "status": {
+ "icon": "status_skipped",
+ "text": "skipped",
+ "label": "skipped",
+ "group": "skipped",
+ "tooltip": "skipped",
+ "has_details": true,
+ "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982858",
+ "illustration": {
+ "image": "https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
+ "size": "svg-430",
+ "title": "This job has been skipped"
+ },
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png"
+ },
+ "jobs": [
+ {
+ "id": 114982858,
+ "name": "review_stop",
+ "started": null,
+ "archived": false,
+ "build_path": "/gitlab-com/gitlab-docs/-/jobs/114982858",
+ "playable": false,
+ "scheduled": false,
+ "created_at": "2018-10-31T16:35:23.840Z",
+ "updated_at": "2018-10-31T16:41:40.480Z",
+ "status": {
+ "icon": "status_skipped",
+ "text": "skipped",
+ "label": "skipped",
+ "group": "skipped",
+ "tooltip": "skipped",
+ "has_details": true,
+ "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982858",
+ "illustration": {
+ "image": "https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
+ "size": "svg-430",
+ "title": "This job has been skipped"
+ },
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png"
+ }
+ }
+ ]
+ }
+ ],
+ "status": {
+ "icon": "status_skipped",
+ "text": "skipped",
+ "label": "skipped",
+ "group": "skipped",
+ "tooltip": "skipped",
+ "has_details": true,
+ "details_path": "/gitlab-com/gitlab-docs/pipelines/34993051#deploy",
+ "illustration": null,
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png"
+ },
+ "path": "/gitlab-com/gitlab-docs/pipelines/34993051#deploy",
+ "dropdown_path": "/gitlab-com/gitlab-docs/pipelines/34993051/stage.json?stage=deploy"
+ }
+ ],
+ "artifacts": [],
+ "manual_actions": [
+ {
+ "name": "image:bootstrap",
+ "path": "/gitlab-com/gitlab-docs/-/jobs/114982853/play",
+ "playable": true,
+ "scheduled": false
+ },
+ {
+ "name": "image:builder-onbuild",
+ "path": "/gitlab-com/gitlab-docs/-/jobs/114982854/play",
+ "playable": true,
+ "scheduled": false
+ },
+ {
+ "name": "image:nginx-onbuild",
+ "path": "/gitlab-com/gitlab-docs/-/jobs/114982855/play",
+ "playable": true,
+ "scheduled": false
+ },
+ {
+ "name": "review_stop",
+ "path": "/gitlab-com/gitlab-docs/-/jobs/114982858/play",
+ "playable": false,
+ "scheduled": false
+ }
+ ],
+ "scheduled_actions": []
+ },
+ "project": {
+ "id": 1794617,
+ "name": "GitLab Docs",
+ "full_path": "/gitlab-com/gitlab-docs",
+ "full_name": "GitLab.com / GitLab Docs"
+ }
+ },
+ {
+ "id": 34993052,
+ "user": {
+ "id": 376774,
+ "name": "Alessio Caiazza",
+ "username": "nolith",
+ "state": "active",
+ "avatar_url": "https://assets.gitlab-static.net/uploads/-/system/user/avatar/376774/avatar.png",
+ "web_url": "https://gitlab.com/nolith",
+ "status_tooltip_html": null,
+ "path": "/nolith"
+ },
+ "active": false,
+ "coverage": null,
+ "source": "pipeline",
+ "path": "/gitlab-com/gitlab-docs/pipelines/34993051",
+ "details": {
+ "status": {
+ "icon": "status_failed",
+ "text": "failed",
+ "label": "failed",
+ "group": "failed",
+ "tooltip": "failed",
+ "has_details": true,
+ "details_path": "/gitlab-com/gitlab-docs/pipelines/34993051",
+ "illustration": null,
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_failed-41304d7f7e3828808b0c26771f0309e55296819a9beea3ea9fbf6689d9857c12.png"
+ },
+ "duration": 118,
+ "finished_at": "2018-10-31T16:41:40.615Z",
+ "stages": [
+ {
+ "name": "build-images",
+ "title": "build-images: skipped",
+ "groups": [
+ {
+ "name": "image:bootstrap",
+ "size": 1,
+ "status": {
+ "icon": "status_manual",
+ "text": "manual",
+ "label": "manual play action",
+ "group": "manual",
+ "tooltip": "manual action",
+ "has_details": true,
+ "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982853",
+ "illustration": {
+ "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg",
+ "size": "svg-394",
+ "title": "This job requires a manual action",
+ "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments"
+ },
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png",
+ "action": {
+ "icon": "play",
+ "title": "Play",
+ "path": "/gitlab-com/gitlab-docs/-/jobs/114982853/play",
+ "method": "post",
+ "button_title": "Trigger this manual action"
+ }
+ },
+ "jobs": [
+ {
+ "id": 114982853,
+ "name": "image:bootstrap",
+ "started": null,
+ "archived": false,
+ "build_path": "/gitlab-com/gitlab-docs/-/jobs/114982853",
+ "play_path": "/gitlab-com/gitlab-docs/-/jobs/114982853/play",
+ "playable": true,
+ "scheduled": false,
+ "created_at": "2018-10-31T16:35:23.704Z",
+ "updated_at": "2018-10-31T16:35:24.118Z",
+ "status": {
+ "icon": "status_manual",
+ "text": "manual",
+ "label": "manual play action",
+ "group": "manual",
+ "tooltip": "manual action",
+ "has_details": true,
+ "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982853",
+ "illustration": {
+ "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg",
+ "size": "svg-394",
+ "title": "This job requires a manual action",
+ "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments"
+ },
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png",
+ "action": {
+ "icon": "play",
+ "title": "Play",
+ "path": "/gitlab-com/gitlab-docs/-/jobs/114982853/play",
+ "method": "post",
+ "button_title": "Trigger this manual action"
+ }
+ }
+ }
+ ]
+ },
+ {
+ "name": "image:builder-onbuild",
+ "size": 1,
+ "status": {
+ "icon": "status_manual",
+ "text": "manual",
+ "label": "manual play action",
+ "group": "manual",
+ "tooltip": "manual action",
+ "has_details": true,
+ "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982854",
+ "illustration": {
+ "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg",
+ "size": "svg-394",
+ "title": "This job requires a manual action",
+ "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments"
+ },
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png",
+ "action": {
+ "icon": "play",
+ "title": "Play",
+ "path": "/gitlab-com/gitlab-docs/-/jobs/114982854/play",
+ "method": "post",
+ "button_title": "Trigger this manual action"
+ }
+ },
+ "jobs": [
+ {
+ "id": 114982854,
+ "name": "image:builder-onbuild",
+ "started": null,
+ "archived": false,
+ "build_path": "/gitlab-com/gitlab-docs/-/jobs/114982854",
+ "play_path": "/gitlab-com/gitlab-docs/-/jobs/114982854/play",
+ "playable": true,
+ "scheduled": false,
+ "created_at": "2018-10-31T16:35:23.728Z",
+ "updated_at": "2018-10-31T16:35:24.070Z",
+ "status": {
+ "icon": "status_manual",
+ "text": "manual",
+ "label": "manual play action",
+ "group": "manual",
+ "tooltip": "manual action",
+ "has_details": true,
+ "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982854",
+ "illustration": {
+ "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg",
+ "size": "svg-394",
+ "title": "This job requires a manual action",
+ "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments"
+ },
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png",
+ "action": {
+ "icon": "play",
+ "title": "Play",
+ "path": "/gitlab-com/gitlab-docs/-/jobs/114982854/play",
+ "method": "post",
+ "button_title": "Trigger this manual action"
+ }
+ }
+ }
+ ]
+ },
+ {
+ "name": "image:nginx-onbuild",
+ "size": 1,
+ "status": {
+ "icon": "status_manual",
+ "text": "manual",
+ "label": "manual play action",
+ "group": "manual",
+ "tooltip": "manual action",
+ "has_details": true,
+ "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982855",
+ "illustration": {
+ "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg",
+ "size": "svg-394",
+ "title": "This job requires a manual action",
+ "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments"
+ },
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png",
+ "action": {
+ "icon": "play",
+ "title": "Play",
+ "path": "/gitlab-com/gitlab-docs/-/jobs/114982855/play",
+ "method": "post",
+ "button_title": "Trigger this manual action"
+ }
+ },
+ "jobs": [
+ {
+ "id": 1224982855,
+ "name": "image:nginx-onbuild",
+ "started": null,
+ "archived": false,
+ "build_path": "/gitlab-com/gitlab-docs/-/jobs/114982855",
+ "play_path": "/gitlab-com/gitlab-docs/-/jobs/114982855/play",
+ "playable": true,
+ "scheduled": false,
+ "created_at": "2018-10-31T16:35:23.753Z",
+ "updated_at": "2018-10-31T16:35:24.033Z",
+ "status": {
+ "icon": "status_manual",
+ "text": "manual",
+ "label": "manual play action",
+ "group": "manual",
+ "tooltip": "manual action",
+ "has_details": true,
+ "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982855",
+ "illustration": {
+ "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg",
+ "size": "svg-394",
+ "title": "This job requires a manual action",
+ "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments"
+ },
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png",
+ "action": {
+ "icon": "play",
+ "title": "Play",
+ "path": "/gitlab-com/gitlab-docs/-/jobs/114982855/play",
+ "method": "post",
+ "button_title": "Trigger this manual action"
+ }
+ }
+ }
+ ]
+ }
+ ],
+ "status": {
+ "icon": "status_skipped",
+ "text": "skipped",
+ "label": "skipped",
+ "group": "skipped",
+ "tooltip": "skipped",
+ "has_details": true,
+ "details_path": "/gitlab-com/gitlab-docs/pipelines/34993051#build-images",
+ "illustration": null,
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png"
+ },
+ "path": "/gitlab-com/gitlab-docs/pipelines/34993051#build-images",
+ "dropdown_path": "/gitlab-com/gitlab-docs/pipelines/34993051/stage.json?stage=build-images"
+ },
+ {
+ "name": "build",
+ "title": "build: failed",
+ "groups": [
+ {
+ "name": "compile_dev",
+ "size": 1,
+ "status": {
+ "icon": "status_failed",
+ "text": "failed",
+ "label": "failed",
+ "group": "failed",
+ "tooltip": "failed - (script failure)",
+ "has_details": true,
+ "details_path": "/gitlab-com/gitlab-docs/-/jobs/114984694",
+ "illustration": {
+ "image": "https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
+ "size": "svg-430",
+ "title": "This job does not have a trace."
+ },
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_failed-41304d7f7e3828808b0c26771f0309e55296819a9beea3ea9fbf6689d9857c12.png",
+ "action": {
+ "icon": "retry",
+ "title": "Retry",
+ "path": "/gitlab-com/gitlab-docs/-/jobs/114984694/retry",
+ "method": "post",
+ "button_title": "Retry this job"
+ }
+ },
+ "jobs": [
+ {
+ "id": 1123984694,
+ "name": "compile_dev",
+ "started": "2018-10-31T16:39:41.598Z",
+ "archived": false,
+ "build_path": "/gitlab-com/gitlab-docs/-/jobs/114984694",
+ "retry_path": "/gitlab-com/gitlab-docs/-/jobs/114984694/retry",
+ "playable": false,
+ "scheduled": false,
+ "created_at": "2018-10-31T16:39:41.138Z",
+ "updated_at": "2018-10-31T16:41:40.072Z",
+ "status": {
+ "icon": "status_failed",
+ "text": "failed",
+ "label": "failed",
+ "group": "failed",
+ "tooltip": "failed - (script failure)",
+ "has_details": true,
+ "details_path": "/gitlab-com/gitlab-docs/-/jobs/114984694",
+ "illustration": {
+ "image": "https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
+ "size": "svg-430",
+ "title": "This job does not have a trace."
+ },
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_failed-41304d7f7e3828808b0c26771f0309e55296819a9beea3ea9fbf6689d9857c12.png",
+ "action": {
+ "icon": "retry",
+ "title": "Retry",
+ "path": "/gitlab-com/gitlab-docs/-/jobs/114984694/retry",
+ "method": "post",
+ "button_title": "Retry this job"
+ }
+ },
+ "recoverable": false
+ }
+ ]
+ }
+ ],
+ "status": {
+ "icon": "status_failed",
+ "text": "failed",
+ "label": "failed",
+ "group": "failed",
+ "tooltip": "failed",
+ "has_details": true,
+ "details_path": "/gitlab-com/gitlab-docs/pipelines/34993051#build",
+ "illustration": null,
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_failed-41304d7f7e3828808b0c26771f0309e55296819a9beea3ea9fbf6689d9857c12.png"
+ },
+ "path": "/gitlab-com/gitlab-docs/pipelines/34993051#build",
+ "dropdown_path": "/gitlab-com/gitlab-docs/pipelines/34993051/stage.json?stage=build"
+ },
+ {
+ "name": "deploy",
+ "title": "deploy: skipped",
+ "groups": [
+ {
+ "name": "review",
+ "size": 1,
+ "status": {
+ "icon": "status_skipped",
+ "text": "skipped",
+ "label": "skipped",
+ "group": "skipped",
+ "tooltip": "skipped",
+ "has_details": true,
+ "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982857",
+ "illustration": {
+ "image": "https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
+ "size": "svg-430",
+ "title": "This job has been skipped"
+ },
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png"
+ },
+ "jobs": [
+ {
+ "id": 1143232982857,
+ "name": "review",
+ "started": null,
+ "archived": false,
+ "build_path": "/gitlab-com/gitlab-docs/-/jobs/114982857",
+ "playable": false,
+ "scheduled": false,
+ "created_at": "2018-10-31T16:35:23.805Z",
+ "updated_at": "2018-10-31T16:41:40.569Z",
+ "status": {
+ "icon": "status_skipped",
+ "text": "skipped",
+ "label": "skipped",
+ "group": "skipped",
+ "tooltip": "skipped",
+ "has_details": true,
+ "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982857",
+ "illustration": {
+ "image": "https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
+ "size": "svg-430",
+ "title": "This job has been skipped"
+ },
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png"
+ }
+ }
+ ]
+ },
+ {
+ "name": "review_stop",
+ "size": 1,
+ "status": {
+ "icon": "status_skipped",
+ "text": "skipped",
+ "label": "skipped",
+ "group": "skipped",
+ "tooltip": "skipped",
+ "has_details": true,
+ "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982858",
+ "illustration": {
+ "image": "https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
+ "size": "svg-430",
+ "title": "This job has been skipped"
+ },
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png"
+ },
+ "jobs": [
+ {
+ "id": 114921313182858,
+ "name": "review_stop",
+ "started": null,
+ "archived": false,
+ "build_path": "/gitlab-com/gitlab-docs/-/jobs/114982858",
+ "playable": false,
+ "scheduled": false,
+ "created_at": "2018-10-31T16:35:23.840Z",
+ "updated_at": "2018-10-31T16:41:40.480Z",
+ "status": {
+ "icon": "status_skipped",
+ "text": "skipped",
+ "label": "skipped",
+ "group": "skipped",
+ "tooltip": "skipped",
+ "has_details": true,
+ "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982858",
+ "illustration": {
+ "image": "https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
+ "size": "svg-430",
+ "title": "This job has been skipped"
+ },
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png"
+ }
+ }
+ ]
+ }
+ ],
+ "status": {
+ "icon": "status_skipped",
+ "text": "skipped",
+ "label": "skipped",
+ "group": "skipped",
+ "tooltip": "skipped",
+ "has_details": true,
+ "details_path": "/gitlab-com/gitlab-docs/pipelines/34993051#deploy",
+ "illustration": null,
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png"
+ },
+ "path": "/gitlab-com/gitlab-docs/pipelines/34993051#deploy",
+ "dropdown_path": "/gitlab-com/gitlab-docs/pipelines/34993051/stage.json?stage=deploy"
+ }
+ ],
+ "artifacts": [],
+ "manual_actions": [
+ {
+ "name": "image:bootstrap",
+ "path": "/gitlab-com/gitlab-docs/-/jobs/114982853/play",
+ "playable": true,
+ "scheduled": false
+ },
+ {
+ "name": "image:builder-onbuild",
+ "path": "/gitlab-com/gitlab-docs/-/jobs/114982854/play",
+ "playable": true,
+ "scheduled": false
+ },
+ {
+ "name": "image:nginx-onbuild",
+ "path": "/gitlab-com/gitlab-docs/-/jobs/114982855/play",
+ "playable": true,
+ "scheduled": false
+ },
+ {
+ "name": "review_stop",
+ "path": "/gitlab-com/gitlab-docs/-/jobs/114982858/play",
+ "playable": false,
+ "scheduled": false
+ }
+ ],
+ "scheduled_actions": []
+ },
+ "project": {
+ "id": 1794617,
+ "name": "GitLab Docs",
+ "full_path": "/gitlab-com/gitlab-docs",
+ "full_name": "GitLab.com / GitLab Docs"
+ },
+ "triggered": [
+ {
+ "id": 26,
+ "user": null,
+ "active": false,
+ "coverage": null,
+ "source": "push",
+ "created_at": "2019-01-06T17:48:37.599Z",
+ "updated_at": "2019-01-06T17:48:38.371Z",
+ "path": "/h5bp/html5-boilerplate/pipelines/26",
+ "flags": {
+ "latest": true,
+ "stuck": false,
+ "auto_devops": false,
+ "merge_request": false,
+ "yaml_errors": false,
+ "retryable": true,
+ "cancelable": false,
+ "failure_reason": false
+ },
+ "details": {
+ "status": {
+ "icon": "status_warning",
+ "text": "passed",
+ "label": "passed with warnings",
+ "group": "success-with-warnings",
+ "tooltip": "passed",
+ "has_details": true,
+ "details_path": "/h5bp/html5-boilerplate/pipelines/26",
+ "illustration": null,
+ "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png"
+ },
+ "duration": null,
+ "finished_at": "2019-01-06T17:48:38.370Z",
+ "stages": [
+ {
+ "name": "build",
+ "title": "build: passed",
+ "groups": [
+ {
+ "name": "build:linux",
+ "size": 1,
+ "status": {
+ "icon": "status_success",
+ "text": "passed",
+ "label": "passed",
+ "group": "success",
+ "tooltip": "passed",
+ "has_details": true,
+ "details_path": "/h5bp/html5-boilerplate/-/jobs/526",
+ "illustration": {
+ "image": "/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
+ "size": "svg-430",
+ "title": "This job does not have a trace."
+ },
+ "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png",
+ "action": {
+ "icon": "retry",
+ "title": "Retry",
+ "path": "/h5bp/html5-boilerplate/-/jobs/526/retry",
+ "method": "post",
+ "button_title": "Retry this job"
+ }
+ },
+ "jobs": [
+ {
+ "id": 526,
+ "name": "build:linux",
+ "started": "2019-01-06T08:48:20.236Z",
+ "archived": false,
+ "build_path": "/h5bp/html5-boilerplate/-/jobs/526",
+ "retry_path": "/h5bp/html5-boilerplate/-/jobs/526/retry",
+ "playable": false,
+ "scheduled": false,
+ "created_at": "2019-01-06T17:48:37.806Z",
+ "updated_at": "2019-01-06T17:48:37.806Z",
+ "status": {
+ "icon": "status_success",
+ "text": "passed",
+ "label": "passed",
+ "group": "success",
+ "tooltip": "passed",
+ "has_details": true,
+ "details_path": "/h5bp/html5-boilerplate/-/jobs/526",
+ "illustration": {
+ "image": "/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
+ "size": "svg-430",
+ "title": "This job does not have a trace."
+ },
+ "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png",
+ "action": {
+ "icon": "retry",
+ "title": "Retry",
+ "path": "/h5bp/html5-boilerplate/-/jobs/526/retry",
+ "method": "post",
+ "button_title": "Retry this job"
+ }
+ }
+ }
+ ]
+ },
+ {
+ "name": "build:osx",
+ "size": 1,
+ "status": {
+ "icon": "status_success",
+ "text": "passed",
+ "label": "passed",
+ "group": "success",
+ "tooltip": "passed",
+ "has_details": true,
+ "details_path": "/h5bp/html5-boilerplate/-/jobs/527",
+ "illustration": {
+ "image": "/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
+ "size": "svg-430",
+ "title": "This job does not have a trace."
+ },
+ "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png",
+ "action": {
+ "icon": "retry",
+ "title": "Retry",
+ "path": "/h5bp/html5-boilerplate/-/jobs/527/retry",
+ "method": "post",
+ "button_title": "Retry this job"
+ }
+ },
+ "jobs": [
+ {
+ "id": 527,
+ "name": "build:osx",
+ "started": "2019-01-06T07:48:20.237Z",
+ "archived": false,
+ "build_path": "/h5bp/html5-boilerplate/-/jobs/527",
+ "retry_path": "/h5bp/html5-boilerplate/-/jobs/527/retry",
+ "playable": false,
+ "scheduled": false,
+ "created_at": "2019-01-06T17:48:37.846Z",
+ "updated_at": "2019-01-06T17:48:37.846Z",
+ "status": {
+ "icon": "status_success",
+ "text": "passed",
+ "label": "passed",
+ "group": "success",
+ "tooltip": "passed",
+ "has_details": true,
+ "details_path": "/h5bp/html5-boilerplate/-/jobs/527",
+ "illustration": {
+ "image": "/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
+ "size": "svg-430",
+ "title": "This job does not have a trace."
+ },
+ "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png",
+ "action": {
+ "icon": "retry",
+ "title": "Retry",
+ "path": "/h5bp/html5-boilerplate/-/jobs/527/retry",
+ "method": "post",
+ "button_title": "Retry this job"
+ }
+ }
+ }
+ ]
+ }
+ ],
+ "status": {
+ "icon": "status_success",
+ "text": "passed",
+ "label": "passed",
+ "group": "success",
+ "tooltip": "passed",
+ "has_details": true,
+ "details_path": "/h5bp/html5-boilerplate/pipelines/26#build",
+ "illustration": null,
+ "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png"
+ },
+ "path": "/h5bp/html5-boilerplate/pipelines/26#build",
+ "dropdown_path": "/h5bp/html5-boilerplate/pipelines/26/stage.json?stage=build"
+ },
+ {
+ "name": "test",
+ "title": "test: passed with warnings",
+ "groups": [
+ {
+ "name": "jenkins",
+ "size": 1,
+ "status": {
+ "icon": "status_success",
+ "text": "passed",
+ "label": null,
+ "group": "success",
+ "tooltip": null,
+ "has_details": false,
+ "details_path": null,
+ "illustration": null,
+ "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png"
+ },
+ "jobs": [
+ {
+ "id": 546,
+ "name": "jenkins",
+ "started": "2019-01-06T11:48:20.237Z",
+ "archived": false,
+ "build_path": "/h5bp/html5-boilerplate/-/jobs/546",
+ "playable": false,
+ "scheduled": false,
+ "created_at": "2019-01-06T17:48:38.359Z",
+ "updated_at": "2019-01-06T17:48:38.359Z",
+ "status": {
+ "icon": "status_success",
+ "text": "passed",
+ "label": null,
+ "group": "success",
+ "tooltip": null,
+ "has_details": false,
+ "details_path": null,
+ "illustration": null,
+ "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png"
+ }
+ }
+ ]
+ },
+ {
+ "name": "rspec:linux",
+ "size": 3,
+ "status": {
+ "icon": "status_success",
+ "text": "passed",
+ "label": "passed",
+ "group": "success",
+ "tooltip": "passed",
+ "has_details": false,
+ "details_path": null,
+ "illustration": null,
+ "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png"
+ },
+ "jobs": [
+ {
+ "id": 528,
+ "name": "rspec:linux 0 3",
+ "started": "2019-01-06T09:48:20.237Z",
+ "archived": false,
+ "build_path": "/h5bp/html5-boilerplate/-/jobs/528",
+ "retry_path": "/h5bp/html5-boilerplate/-/jobs/528/retry",
+ "playable": false,
+ "scheduled": false,
+ "created_at": "2019-01-06T17:48:37.885Z",
+ "updated_at": "2019-01-06T17:48:37.885Z",
+ "status": {
+ "icon": "status_success",
+ "text": "passed",
+ "label": "passed",
+ "group": "success",
+ "tooltip": "passed",
+ "has_details": true,
+ "details_path": "/h5bp/html5-boilerplate/-/jobs/528",
+ "illustration": {
+ "image": "/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
+ "size": "svg-430",
+ "title": "This job does not have a trace."
+ },
+ "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png",
+ "action": {
+ "icon": "retry",
+ "title": "Retry",
+ "path": "/h5bp/html5-boilerplate/-/jobs/528/retry",
+ "method": "post",
+ "button_title": "Retry this job"
+ }
+ }
+ },
+ {
+ "id": 529,
+ "name": "rspec:linux 1 3",
+ "started": "2019-01-06T09:48:20.237Z",
+ "archived": false,
+ "build_path": "/h5bp/html5-boilerplate/-/jobs/529",
+ "retry_path": "/h5bp/html5-boilerplate/-/jobs/529/retry",
+ "playable": false,
+ "scheduled": false,
+ "created_at": "2019-01-06T17:48:37.907Z",
+ "updated_at": "2019-01-06T17:48:37.907Z",
+ "status": {
+ "icon": "status_success",
+ "text": "passed",
+ "label": "passed",
+ "group": "success",
+ "tooltip": "passed",
+ "has_details": true,
+ "details_path": "/h5bp/html5-boilerplate/-/jobs/529",
+ "illustration": {
+ "image": "/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
+ "size": "svg-430",
+ "title": "This job does not have a trace."
+ },
+ "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png",
+ "action": {
+ "icon": "retry",
+ "title": "Retry",
+ "path": "/h5bp/html5-boilerplate/-/jobs/529/retry",
+ "method": "post",
+ "button_title": "Retry this job"
+ }
+ }
+ },
+ {
+ "id": 530,
+ "name": "rspec:linux 2 3",
+ "started": "2019-01-06T09:48:20.237Z",
+ "archived": false,
+ "build_path": "/h5bp/html5-boilerplate/-/jobs/530",
+ "retry_path": "/h5bp/html5-boilerplate/-/jobs/530/retry",
+ "playable": false,
+ "scheduled": false,
+ "created_at": "2019-01-06T17:48:37.927Z",
+ "updated_at": "2019-01-06T17:48:37.927Z",
+ "status": {
+ "icon": "status_success",
+ "text": "passed",
+ "label": "passed",
+ "group": "success",
+ "tooltip": "passed",
+ "has_details": true,
+ "details_path": "/h5bp/html5-boilerplate/-/jobs/530",
+ "illustration": {
+ "image": "/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
+ "size": "svg-430",
+ "title": "This job does not have a trace."
+ },
+ "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png",
+ "action": {
+ "icon": "retry",
+ "title": "Retry",
+ "path": "/h5bp/html5-boilerplate/-/jobs/530/retry",
+ "method": "post",
+ "button_title": "Retry this job"
+ }
+ }
+ }
+ ]
+ },
+ {
+ "name": "rspec:osx",
+ "size": 1,
+ "status": {
+ "icon": "status_success",
+ "text": "passed",
+ "label": "passed",
+ "group": "success",
+ "tooltip": "passed",
+ "has_details": true,
+ "details_path": "/h5bp/html5-boilerplate/-/jobs/535",
+ "illustration": {
+ "image": "/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
+ "size": "svg-430",
+ "title": "This job does not have a trace."
+ },
+ "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png",
+ "action": {
+ "icon": "retry",
+ "title": "Retry",
+ "path": "/h5bp/html5-boilerplate/-/jobs/535/retry",
+ "method": "post",
+ "button_title": "Retry this job"
+ }
+ },
+ "jobs": [
+ {
+ "id": 535,
+ "name": "rspec:osx",
+ "started": "2019-01-06T09:48:20.237Z",
+ "archived": false,
+ "build_path": "/h5bp/html5-boilerplate/-/jobs/535",
+ "retry_path": "/h5bp/html5-boilerplate/-/jobs/535/retry",
+ "playable": false,
+ "scheduled": false,
+ "created_at": "2019-01-06T17:48:38.018Z",
+ "updated_at": "2019-01-06T17:48:38.018Z",
+ "status": {
+ "icon": "status_success",
+ "text": "passed",
+ "label": "passed",
+ "group": "success",
+ "tooltip": "passed",
+ "has_details": true,
+ "details_path": "/h5bp/html5-boilerplate/-/jobs/535",
+ "illustration": {
+ "image": "/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
+ "size": "svg-430",
+ "title": "This job does not have a trace."
+ },
+ "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png",
+ "action": {
+ "icon": "retry",
+ "title": "Retry",
+ "path": "/h5bp/html5-boilerplate/-/jobs/535/retry",
+ "method": "post",
+ "button_title": "Retry this job"
+ }
+ }
+ }
+ ]
+ },
+ {
+ "name": "rspec:windows",
+ "size": 3,
+ "status": {
+ "icon": "status_success",
+ "text": "passed",
+ "label": "passed",
+ "group": "success",
+ "tooltip": "passed",
+ "has_details": false,
+ "details_path": null,
+ "illustration": null,
+ "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png"
+ },
+ "jobs": [
+ {
+ "id": 531,
+ "name": "rspec:windows 0 3",
+ "started": "2019-01-06T09:48:20.237Z",
+ "archived": false,
+ "build_path": "/h5bp/html5-boilerplate/-/jobs/531",
+ "retry_path": "/h5bp/html5-boilerplate/-/jobs/531/retry",
+ "playable": false,
+ "scheduled": false,
+ "created_at": "2019-01-06T17:48:37.944Z",
+ "updated_at": "2019-01-06T17:48:37.944Z",
+ "status": {
+ "icon": "status_success",
+ "text": "passed",
+ "label": "passed",
+ "group": "success",
+ "tooltip": "passed",
+ "has_details": true,
+ "details_path": "/h5bp/html5-boilerplate/-/jobs/531",
+ "illustration": {
+ "image": "/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
+ "size": "svg-430",
+ "title": "This job does not have a trace."
+ },
+ "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png",
+ "action": {
+ "icon": "retry",
+ "title": "Retry",
+ "path": "/h5bp/html5-boilerplate/-/jobs/531/retry",
+ "method": "post",
+ "button_title": "Retry this job"
+ }
+ }
+ },
+ {
+ "id": 532,
+ "name": "rspec:windows 1 3",
+ "started": "2019-01-06T09:48:20.237Z",
+ "archived": false,
+ "build_path": "/h5bp/html5-boilerplate/-/jobs/532",
+ "retry_path": "/h5bp/html5-boilerplate/-/jobs/532/retry",
+ "playable": false,
+ "scheduled": false,
+ "created_at": "2019-01-06T17:48:37.962Z",
+ "updated_at": "2019-01-06T17:48:37.962Z",
+ "status": {
+ "icon": "status_success",
+ "text": "passed",
+ "label": "passed",
+ "group": "success",
+ "tooltip": "passed",
+ "has_details": true,
+ "details_path": "/h5bp/html5-boilerplate/-/jobs/532",
+ "illustration": {
+ "image": "/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
+ "size": "svg-430",
+ "title": "This job does not have a trace."
+ },
+ "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png",
+ "action": {
+ "icon": "retry",
+ "title": "Retry",
+ "path": "/h5bp/html5-boilerplate/-/jobs/532/retry",
+ "method": "post",
+ "button_title": "Retry this job"
+ }
+ }
+ },
+ {
+ "id": 534,
+ "name": "rspec:windows 2 3",
+ "started": "2019-01-06T09:48:20.237Z",
+ "archived": false,
+ "build_path": "/h5bp/html5-boilerplate/-/jobs/534",
+ "retry_path": "/h5bp/html5-boilerplate/-/jobs/534/retry",
+ "playable": false,
+ "scheduled": false,
+ "created_at": "2019-01-06T17:48:37.999Z",
+ "updated_at": "2019-01-06T17:48:37.999Z",
+ "status": {
+ "icon": "status_success",
+ "text": "passed",
+ "label": "passed",
+ "group": "success",
+ "tooltip": "passed",
+ "has_details": true,
+ "details_path": "/h5bp/html5-boilerplate/-/jobs/534",
+ "illustration": {
+ "image": "/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
+ "size": "svg-430",
+ "title": "This job does not have a trace."
+ },
+ "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png",
+ "action": {
+ "icon": "retry",
+ "title": "Retry",
+ "path": "/h5bp/html5-boilerplate/-/jobs/534/retry",
+ "method": "post",
+ "button_title": "Retry this job"
+ }
+ }
+ }
+ ]
+ },
+ {
+ "name": "spinach:linux",
+ "size": 1,
+ "status": {
+ "icon": "status_success",
+ "text": "passed",
+ "label": "passed",
+ "group": "success",
+ "tooltip": "passed",
+ "has_details": true,
+ "details_path": "/h5bp/html5-boilerplate/-/jobs/536",
+ "illustration": {
+ "image": "/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
+ "size": "svg-430",
+ "title": "This job does not have a trace."
+ },
+ "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png",
+ "action": {
+ "icon": "retry",
+ "title": "Retry",
+ "path": "/h5bp/html5-boilerplate/-/jobs/536/retry",
+ "method": "post",
+ "button_title": "Retry this job"
+ }
+ },
+ "jobs": [
+ {
+ "id": 536,
+ "name": "spinach:linux",
+ "started": "2019-01-06T09:48:20.237Z",
+ "archived": false,
+ "build_path": "/h5bp/html5-boilerplate/-/jobs/536",
+ "retry_path": "/h5bp/html5-boilerplate/-/jobs/536/retry",
+ "playable": false,
+ "scheduled": false,
+ "created_at": "2019-01-06T17:48:38.050Z",
+ "updated_at": "2019-01-06T17:48:38.050Z",
+ "status": {
+ "icon": "status_success",
+ "text": "passed",
+ "label": "passed",
+ "group": "success",
+ "tooltip": "passed",
+ "has_details": true,
+ "details_path": "/h5bp/html5-boilerplate/-/jobs/536",
+ "illustration": {
+ "image": "/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
+ "size": "svg-430",
+ "title": "This job does not have a trace."
+ },
+ "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png",
+ "action": {
+ "icon": "retry",
+ "title": "Retry",
+ "path": "/h5bp/html5-boilerplate/-/jobs/536/retry",
+ "method": "post",
+ "button_title": "Retry this job"
+ }
+ }
+ }
+ ]
+ },
+ {
+ "name": "spinach:osx",
+ "size": 1,
+ "status": {
+ "icon": "status_warning",
+ "text": "failed",
+ "label": "failed (allowed to fail)",
+ "group": "failed-with-warnings",
+ "tooltip": "failed - (unknown failure) (allowed to fail)",
+ "has_details": true,
+ "details_path": "/h5bp/html5-boilerplate/-/jobs/537",
+ "illustration": {
+ "image": "/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
+ "size": "svg-430",
+ "title": "This job does not have a trace."
+ },
+ "favicon": "/assets/ci_favicons/favicon_status_failed-41304d7f7e3828808b0c26771f0309e55296819a9beea3ea9fbf6689d9857c12.png",
+ "action": {
+ "icon": "retry",
+ "title": "Retry",
+ "path": "/h5bp/html5-boilerplate/-/jobs/537/retry",
+ "method": "post",
+ "button_title": "Retry this job"
+ }
+ },
+ "jobs": [
+ {
+ "id": 537,
+ "name": "spinach:osx",
+ "started": "2019-01-06T09:48:20.237Z",
+ "archived": false,
+ "build_path": "/h5bp/html5-boilerplate/-/jobs/537",
+ "retry_path": "/h5bp/html5-boilerplate/-/jobs/537/retry",
+ "playable": false,
+ "scheduled": false,
+ "created_at": "2019-01-06T17:48:38.069Z",
+ "updated_at": "2019-01-06T17:48:38.069Z",
+ "status": {
+ "icon": "status_warning",
+ "text": "failed",
+ "label": "failed (allowed to fail)",
+ "group": "failed-with-warnings",
+ "tooltip": "failed - (unknown failure) (allowed to fail)",
+ "has_details": true,
+ "details_path": "/h5bp/html5-boilerplate/-/jobs/537",
+ "illustration": {
+ "image": "/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
+ "size": "svg-430",
+ "title": "This job does not have a trace."
+ },
+ "favicon": "/assets/ci_favicons/favicon_status_failed-41304d7f7e3828808b0c26771f0309e55296819a9beea3ea9fbf6689d9857c12.png",
+ "action": {
+ "icon": "retry",
+ "title": "Retry",
+ "path": "/h5bp/html5-boilerplate/-/jobs/537/retry",
+ "method": "post",
+ "button_title": "Retry this job"
+ }
+ },
+ "callout_message": "There is an unknown failure, please try again",
+ "recoverable": true
+ }
+ ]
+ }
+ ],
+ "status": {
+ "icon": "status_warning",
+ "text": "passed",
+ "label": "passed with warnings",
+ "group": "success-with-warnings",
+ "tooltip": "passed",
+ "has_details": true,
+ "details_path": "/h5bp/html5-boilerplate/pipelines/26#test",
+ "illustration": null,
+ "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png"
+ },
+ "path": "/h5bp/html5-boilerplate/pipelines/26#test",
+ "dropdown_path": "/h5bp/html5-boilerplate/pipelines/26/stage.json?stage=test"
+ },
+ {
+ "name": "security",
+ "title": "security: passed",
+ "groups": [
+ {
+ "name": "container_scanning",
+ "size": 1,
+ "status": {
+ "icon": "status_success",
+ "text": "passed",
+ "label": "passed",
+ "group": "success",
+ "tooltip": "passed",
+ "has_details": true,
+ "details_path": "/h5bp/html5-boilerplate/-/jobs/541",
+ "illustration": {
+ "image": "/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
+ "size": "svg-430",
+ "title": "This job does not have a trace."
+ },
+ "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png",
+ "action": {
+ "icon": "retry",
+ "title": "Retry",
+ "path": "/h5bp/html5-boilerplate/-/jobs/541/retry",
+ "method": "post",
+ "button_title": "Retry this job"
+ }
+ },
+ "jobs": [
+ {
+ "id": 541,
+ "name": "container_scanning",
+ "started": "2019-01-06T09:48:20.237Z",
+ "archived": false,
+ "build_path": "/h5bp/html5-boilerplate/-/jobs/541",
+ "retry_path": "/h5bp/html5-boilerplate/-/jobs/541/retry",
+ "playable": false,
+ "scheduled": false,
+ "created_at": "2019-01-06T17:48:38.186Z",
+ "updated_at": "2019-01-06T17:48:38.186Z",
+ "status": {
+ "icon": "status_success",
+ "text": "passed",
+ "label": "passed",
+ "group": "success",
+ "tooltip": "passed",
+ "has_details": true,
+ "details_path": "/h5bp/html5-boilerplate/-/jobs/541",
+ "illustration": {
+ "image": "/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
+ "size": "svg-430",
+ "title": "This job does not have a trace."
+ },
+ "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png",
+ "action": {
+ "icon": "retry",
+ "title": "Retry",
+ "path": "/h5bp/html5-boilerplate/-/jobs/541/retry",
+ "method": "post",
+ "button_title": "Retry this job"
+ }
+ }
+ }
+ ]
+ },
+ {
+ "name": "dast",
+ "size": 1,
+ "status": {
+ "icon": "status_success",
+ "text": "passed",
+ "label": "passed",
+ "group": "success",
+ "tooltip": "passed",
+ "has_details": true,
+ "details_path": "/h5bp/html5-boilerplate/-/jobs/538",
+ "illustration": {
+ "image": "/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
+ "size": "svg-430",
+ "title": "This job does not have a trace."
+ },
+ "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png",
+ "action": {
+ "icon": "retry",
+ "title": "Retry",
+ "path": "/h5bp/html5-boilerplate/-/jobs/538/retry",
+ "method": "post",
+ "button_title": "Retry this job"
+ }
+ },
+ "jobs": [
+ {
+ "id": 538,
+ "name": "dast",
+ "started": "2019-01-06T09:48:20.237Z",
+ "archived": false,
+ "build_path": "/h5bp/html5-boilerplate/-/jobs/538",
+ "retry_path": "/h5bp/html5-boilerplate/-/jobs/538/retry",
+ "playable": false,
+ "scheduled": false,
+ "created_at": "2019-01-06T17:48:38.087Z",
+ "updated_at": "2019-01-06T17:48:38.087Z",
+ "status": {
+ "icon": "status_success",
+ "text": "passed",
+ "label": "passed",
+ "group": "success",
+ "tooltip": "passed",
+ "has_details": true,
+ "details_path": "/h5bp/html5-boilerplate/-/jobs/538",
+ "illustration": {
+ "image": "/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
+ "size": "svg-430",
+ "title": "This job does not have a trace."
+ },
+ "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png",
+ "action": {
+ "icon": "retry",
+ "title": "Retry",
+ "path": "/h5bp/html5-boilerplate/-/jobs/538/retry",
+ "method": "post",
+ "button_title": "Retry this job"
+ }
+ }
+ }
+ ]
+ },
+ {
+ "name": "dependency_scanning",
+ "size": 1,
+ "status": {
+ "icon": "status_success",
+ "text": "passed",
+ "label": "passed",
+ "group": "success",
+ "tooltip": "passed",
+ "has_details": true,
+ "details_path": "/h5bp/html5-boilerplate/-/jobs/540",
+ "illustration": {
+ "image": "/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
+ "size": "svg-430",
+ "title": "This job does not have a trace."
+ },
+ "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png",
+ "action": {
+ "icon": "retry",
+ "title": "Retry",
+ "path": "/h5bp/html5-boilerplate/-/jobs/540/retry",
+ "method": "post",
+ "button_title": "Retry this job"
+ }
+ },
+ "jobs": [
+ {
+ "id": 540,
+ "name": "dependency_scanning",
+ "started": "2019-01-06T09:48:20.237Z",
+ "archived": false,
+ "build_path": "/h5bp/html5-boilerplate/-/jobs/540",
+ "retry_path": "/h5bp/html5-boilerplate/-/jobs/540/retry",
+ "playable": false,
+ "scheduled": false,
+ "created_at": "2019-01-06T17:48:38.153Z",
+ "updated_at": "2019-01-06T17:48:38.153Z",
+ "status": {
+ "icon": "status_success",
+ "text": "passed",
+ "label": "passed",
+ "group": "success",
+ "tooltip": "passed",
+ "has_details": true,
+ "details_path": "/h5bp/html5-boilerplate/-/jobs/540",
+ "illustration": {
+ "image": "/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
+ "size": "svg-430",
+ "title": "This job does not have a trace."
+ },
+ "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png",
+ "action": {
+ "icon": "retry",
+ "title": "Retry",
+ "path": "/h5bp/html5-boilerplate/-/jobs/540/retry",
+ "method": "post",
+ "button_title": "Retry this job"
+ }
+ }
+ }
+ ]
+ },
+ {
+ "name": "sast",
+ "size": 1,
+ "status": {
+ "icon": "status_success",
+ "text": "passed",
+ "label": "passed",
+ "group": "success",
+ "tooltip": "passed",
+ "has_details": true,
+ "details_path": "/h5bp/html5-boilerplate/-/jobs/539",
+ "illustration": {
+ "image": "/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
+ "size": "svg-430",
+ "title": "This job does not have a trace."
+ },
+ "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png",
+ "action": {
+ "icon": "retry",
+ "title": "Retry",
+ "path": "/h5bp/html5-boilerplate/-/jobs/539/retry",
+ "method": "post",
+ "button_title": "Retry this job"
+ }
+ },
+ "jobs": [
+ {
+ "id": 539,
+ "name": "sast",
+ "started": "2019-01-06T09:48:20.237Z",
+ "archived": false,
+ "build_path": "/h5bp/html5-boilerplate/-/jobs/539",
+ "retry_path": "/h5bp/html5-boilerplate/-/jobs/539/retry",
+ "playable": false,
+ "scheduled": false,
+ "created_at": "2019-01-06T17:48:38.121Z",
+ "updated_at": "2019-01-06T17:48:38.121Z",
+ "status": {
+ "icon": "status_success",
+ "text": "passed",
+ "label": "passed",
+ "group": "success",
+ "tooltip": "passed",
+ "has_details": true,
+ "details_path": "/h5bp/html5-boilerplate/-/jobs/539",
+ "illustration": {
+ "image": "/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
+ "size": "svg-430",
+ "title": "This job does not have a trace."
+ },
+ "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png",
+ "action": {
+ "icon": "retry",
+ "title": "Retry",
+ "path": "/h5bp/html5-boilerplate/-/jobs/539/retry",
+ "method": "post",
+ "button_title": "Retry this job"
+ }
+ }
+ }
+ ]
+ }
+ ],
+ "status": {
+ "icon": "status_success",
+ "text": "passed",
+ "label": "passed",
+ "group": "success",
+ "tooltip": "passed",
+ "has_details": true,
+ "details_path": "/h5bp/html5-boilerplate/pipelines/26#security",
+ "illustration": null,
+ "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png"
+ },
+ "path": "/h5bp/html5-boilerplate/pipelines/26#security",
+ "dropdown_path": "/h5bp/html5-boilerplate/pipelines/26/stage.json?stage=security"
+ },
+ {
+ "name": "deploy",
+ "title": "deploy: passed",
+ "groups": [
+ {
+ "name": "production",
+ "size": 1,
+ "status": {
+ "icon": "status_skipped",
+ "text": "skipped",
+ "label": "skipped",
+ "group": "skipped",
+ "tooltip": "skipped",
+ "has_details": true,
+ "details_path": "/h5bp/html5-boilerplate/-/jobs/544",
+ "illustration": {
+ "image": "/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
+ "size": "svg-430",
+ "title": "This job has been skipped"
+ },
+ "favicon": "/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png"
+ },
+ "jobs": [
+ {
+ "id": 544,
+ "name": "production",
+ "started": null,
+ "archived": false,
+ "build_path": "/h5bp/html5-boilerplate/-/jobs/544",
+ "playable": false,
+ "scheduled": false,
+ "created_at": "2019-01-06T17:48:38.313Z",
+ "updated_at": "2019-01-06T17:48:38.313Z",
+ "status": {
+ "icon": "status_skipped",
+ "text": "skipped",
+ "label": "skipped",
+ "group": "skipped",
+ "tooltip": "skipped",
+ "has_details": true,
+ "details_path": "/h5bp/html5-boilerplate/-/jobs/544",
+ "illustration": {
+ "image": "/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
+ "size": "svg-430",
+ "title": "This job has been skipped"
+ },
+ "favicon": "/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png"
+ }
+ }
+ ]
+ },
+ {
+ "name": "staging",
+ "size": 1,
+ "status": {
+ "icon": "status_success",
+ "text": "passed",
+ "label": "passed",
+ "group": "success",
+ "tooltip": "passed",
+ "has_details": true,
+ "details_path": "/h5bp/html5-boilerplate/-/jobs/542",
+ "illustration": {
+ "image": "/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
+ "size": "svg-430",
+ "title": "This job does not have a trace."
+ },
+ "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png",
+ "action": {
+ "icon": "retry",
+ "title": "Retry",
+ "path": "/h5bp/html5-boilerplate/-/jobs/542/retry",
+ "method": "post",
+ "button_title": "Retry this job"
+ }
+ },
+ "jobs": [
+ {
+ "id": 542,
+ "name": "staging",
+ "started": "2019-01-06T11:48:20.237Z",
+ "archived": false,
+ "build_path": "/h5bp/html5-boilerplate/-/jobs/542",
+ "retry_path": "/h5bp/html5-boilerplate/-/jobs/542/retry",
+ "playable": false,
+ "scheduled": false,
+ "created_at": "2019-01-06T17:48:38.219Z",
+ "updated_at": "2019-01-06T17:48:38.219Z",
+ "status": {
+ "icon": "status_success",
+ "text": "passed",
+ "label": "passed",
+ "group": "success",
+ "tooltip": "passed",
+ "has_details": true,
+ "details_path": "/h5bp/html5-boilerplate/-/jobs/542",
+ "illustration": {
+ "image": "/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
+ "size": "svg-430",
+ "title": "This job does not have a trace."
+ },
+ "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png",
+ "action": {
+ "icon": "retry",
+ "title": "Retry",
+ "path": "/h5bp/html5-boilerplate/-/jobs/542/retry",
+ "method": "post",
+ "button_title": "Retry this job"
+ }
+ }
+ }
+ ]
+ },
+ {
+ "name": "stop staging",
+ "size": 1,
+ "status": {
+ "icon": "status_skipped",
+ "text": "skipped",
+ "label": "skipped",
+ "group": "skipped",
+ "tooltip": "skipped",
+ "has_details": true,
+ "details_path": "/h5bp/html5-boilerplate/-/jobs/543",
+ "illustration": {
+ "image": "/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
+ "size": "svg-430",
+ "title": "This job has been skipped"
+ },
+ "favicon": "/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png"
+ },
+ "jobs": [
+ {
+ "id": 543,
+ "name": "stop staging",
+ "started": null,
+ "archived": false,
+ "build_path": "/h5bp/html5-boilerplate/-/jobs/543",
+ "playable": false,
+ "scheduled": false,
+ "created_at": "2019-01-06T17:48:38.283Z",
+ "updated_at": "2019-01-06T17:48:38.283Z",
+ "status": {
+ "icon": "status_skipped",
+ "text": "skipped",
+ "label": "skipped",
+ "group": "skipped",
+ "tooltip": "skipped",
+ "has_details": true,
+ "details_path": "/h5bp/html5-boilerplate/-/jobs/543",
+ "illustration": {
+ "image": "/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
+ "size": "svg-430",
+ "title": "This job has been skipped"
+ },
+ "favicon": "/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png"
+ }
+ }
+ ]
+ }
+ ],
+ "status": {
+ "icon": "status_success",
+ "text": "passed",
+ "label": "passed",
+ "group": "success",
+ "tooltip": "passed",
+ "has_details": true,
+ "details_path": "/h5bp/html5-boilerplate/pipelines/26#deploy",
+ "illustration": null,
+ "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png"
+ },
+ "path": "/h5bp/html5-boilerplate/pipelines/26#deploy",
+ "dropdown_path": "/h5bp/html5-boilerplate/pipelines/26/stage.json?stage=deploy"
+ },
+ {
+ "name": "notify",
+ "title": "notify: passed",
+ "groups": [
+ {
+ "name": "slack",
+ "size": 1,
+ "status": {
+ "icon": "status_success",
+ "text": "passed",
+ "label": "manual play action",
+ "group": "success",
+ "tooltip": "passed",
+ "has_details": true,
+ "details_path": "/h5bp/html5-boilerplate/-/jobs/545",
+ "illustration": {
+ "image": "/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg",
+ "size": "svg-394",
+ "title": "This job requires a manual action",
+ "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments"
+ },
+ "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png",
+ "action": {
+ "icon": "play",
+ "title": "Play",
+ "path": "/h5bp/html5-boilerplate/-/jobs/545/play",
+ "method": "post",
+ "button_title": "Trigger this manual action"
+ }
+ },
+ "jobs": [
+ {
+ "id": 545,
+ "name": "slack",
+ "started": null,
+ "archived": false,
+ "build_path": "/h5bp/html5-boilerplate/-/jobs/545",
+ "retry_path": "/h5bp/html5-boilerplate/-/jobs/545/retry",
+ "play_path": "/h5bp/html5-boilerplate/-/jobs/545/play",
+ "playable": true,
+ "scheduled": false,
+ "created_at": "2019-01-06T17:48:38.341Z",
+ "updated_at": "2019-01-06T17:48:38.341Z",
+ "status": {
+ "icon": "status_success",
+ "text": "passed",
+ "label": "manual play action",
+ "group": "success",
+ "tooltip": "passed",
+ "has_details": true,
+ "details_path": "/h5bp/html5-boilerplate/-/jobs/545",
+ "illustration": {
+ "image": "/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg",
+ "size": "svg-394",
+ "title": "This job requires a manual action",
+ "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments"
+ },
+ "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png",
+ "action": {
+ "icon": "play",
+ "title": "Play",
+ "path": "/h5bp/html5-boilerplate/-/jobs/545/play",
+ "method": "post",
+ "button_title": "Trigger this manual action"
+ }
+ }
+ }
+ ]
+ }
+ ],
+ "status": {
+ "icon": "status_success",
+ "text": "passed",
+ "label": "passed",
+ "group": "success",
+ "tooltip": "passed",
+ "has_details": true,
+ "details_path": "/h5bp/html5-boilerplate/pipelines/26#notify",
+ "illustration": null,
+ "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png"
+ },
+ "path": "/h5bp/html5-boilerplate/pipelines/26#notify",
+ "dropdown_path": "/h5bp/html5-boilerplate/pipelines/26/stage.json?stage=notify"
+ }
+ ],
+ "artifacts": [
+ {
+ "name": "build:linux",
+ "expired": null,
+ "expire_at": null,
+ "path": "/h5bp/html5-boilerplate/-/jobs/526/artifacts/download",
+ "browse_path": "/h5bp/html5-boilerplate/-/jobs/526/artifacts/browse"
+ },
+ {
+ "name": "build:osx",
+ "expired": null,
+ "expire_at": null,
+ "path": "/h5bp/html5-boilerplate/-/jobs/527/artifacts/download",
+ "browse_path": "/h5bp/html5-boilerplate/-/jobs/527/artifacts/browse"
+ }
+ ],
+ "manual_actions": [
+ {
+ "name": "stop staging",
+ "path": "/h5bp/html5-boilerplate/-/jobs/543/play",
+ "playable": false,
+ "scheduled": false
+ },
+ {
+ "name": "production",
+ "path": "/h5bp/html5-boilerplate/-/jobs/544/play",
+ "playable": false,
+ "scheduled": false
+ },
+ {
+ "name": "slack",
+ "path": "/h5bp/html5-boilerplate/-/jobs/545/play",
+ "playable": true,
+ "scheduled": false
+ }
+ ],
+ "scheduled_actions": []
+ },
+ "ref": {
+ "name": "master",
+ "path": "/h5bp/html5-boilerplate/commits/master",
+ "tag": false,
+ "branch": true,
+ "merge_request": false
+ },
+ "commit": {
+ "id": "bad98c453eab56d20057f3929989251d45cd1a8b",
+ "short_id": "bad98c45",
+ "title": "remove instances of shrink-to-fit=no (#2103)",
+ "created_at": "2018-12-17T20:52:18.000Z",
+ "parent_ids": ["49130f6cfe9ff1f749015d735649a2bc6f66cf3a"],
+ "message": "remove instances of shrink-to-fit=no (#2103)\n\ncloses #2102\r\n\r\nPer my findings, the need for it as a default was rectified with the release of iOS 9.3, where the viewport no longer shrunk to accommodate overflow, as was introduced in iOS 9.",
+ "author_name": "Scott O'Hara",
+ "author_email": "scottaohara@users.noreply.github.com",
+ "authored_date": "2018-12-17T20:52:18.000Z",
+ "committer_name": "Rob Larsen",
+ "committer_email": "rob@drunkenfist.com",
+ "committed_date": "2018-12-17T20:52:18.000Z",
+ "author": null,
+ "author_gravatar_url": "https://www.gravatar.com/avatar/6d597df7cf998d16cbe00ccac063b31e?s=80\u0026d=identicon",
+ "commit_url": "http://localhost:3001/h5bp/html5-boilerplate/commit/bad98c453eab56d20057f3929989251d45cd1a8b",
+ "commit_path": "/h5bp/html5-boilerplate/commit/bad98c453eab56d20057f3929989251d45cd1a8b"
+ },
+ "retry_path": "/h5bp/html5-boilerplate/pipelines/26/retry",
+ "triggered_by": {
+ "id": 4,
+ "user": null,
+ "active": false,
+ "coverage": null,
+ "source": "push",
+ "path": "/gitlab-org/gitlab-test/pipelines/4",
+ "details": {
+ "status": {
+ "icon": "status_warning",
+ "text": "passed",
+ "label": "passed with warnings",
+ "group": "success-with-warnings",
+ "tooltip": "passed",
+ "has_details": true,
+ "details_path": "/gitlab-org/gitlab-test/pipelines/4",
+ "illustration": null,
+ "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png"
+ }
+ },
+ "project": {
+ "id": 1,
+ "name": "Gitlab Test",
+ "full_path": "/gitlab-org/gitlab-test",
+ "full_name": "Gitlab Org / Gitlab Test"
+ }
+ },
+ "triggered": [],
+ "project": {
+ "id": 1794617,
+ "name": "GitLab Docs",
+ "full_path": "/gitlab-com/gitlab-docs",
+ "full_name": "GitLab.com / GitLab Docs"
+ }
+ }
+ ]
+ }
+ ]
+}
diff --git a/spec/javascripts/pipelines/stores/pipeline.json b/spec/javascripts/pipelines/stores/pipeline.json
new file mode 100644
index 00000000000..7d5891d3d52
--- /dev/null
+++ b/spec/javascripts/pipelines/stores/pipeline.json
@@ -0,0 +1,167 @@
+{
+ "id": 37232567,
+ "user": {
+ "id": 113870,
+ "name": "Phil Hughes",
+ "username": "iamphill",
+ "state": "active",
+ "avatar_url": "https://secure.gravatar.com/avatar/533a51534470a11062df393543eab649?s=80\u0026d=identicon",
+ "web_url": "https://gitlab.com/iamphill",
+ "status_tooltip_html": null,
+ "path": "/iamphill"
+ },
+ "active": false,
+ "coverage": null,
+ "source": "push",
+ "created_at": "2018-11-20T10:22:52.617Z",
+ "updated_at": "2018-11-20T10:24:09.511Z",
+ "path": "/gitlab-org/gl-vue-cli/pipelines/37232567",
+ "flags": {
+ "latest": true,
+ "stuck": false,
+ "auto_devops": false,
+ "yaml_errors": false,
+ "retryable": false,
+ "cancelable": false,
+ "failure_reason": false
+ },
+ "details": {
+ "status": {
+ "icon": "status_success",
+ "text": "passed",
+ "label": "passed",
+ "group": "success",
+ "tooltip": "passed",
+ "has_details": true,
+ "details_path": "/gitlab-org/gl-vue-cli/pipelines/37232567",
+ "illustration": null,
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png"
+ },
+ "duration": 65,
+ "finished_at": "2018-11-20T10:24:09.483Z",
+ "stages": [
+ {
+ "name": "test",
+ "title": "test: passed",
+ "groups": [
+ {
+ "name": "eslint",
+ "size": 1,
+ "status": {
+ "icon": "status_success",
+ "text": "passed",
+ "label": "passed",
+ "group": "success",
+ "tooltip": "passed",
+ "has_details": true,
+ "details_path": "/gitlab-org/gl-vue-cli/-/jobs/122845352",
+ "illustration": {
+ "image": "https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
+ "size": "svg-430",
+ "title": "This job does not have a trace."
+ },
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png",
+ "action": {
+ "icon": "retry",
+ "title": "Retry",
+ "path": "/gitlab-org/gl-vue-cli/-/jobs/122845352/retry",
+ "method": "post",
+ "button_title": "Retry this job"
+ }
+ },
+ "jobs": [
+ {
+ "id": 122845352,
+ "name": "eslint",
+ "started": "2018-11-20T10:22:53.369Z",
+ "archived": false,
+ "build_path": "/gitlab-org/gl-vue-cli/-/jobs/122845352",
+ "retry_path": "/gitlab-org/gl-vue-cli/-/jobs/122845352/retry",
+ "playable": false,
+ "scheduled": false,
+ "created_at": "2018-11-20T10:22:52.630Z",
+ "updated_at": "2018-11-20T10:23:58.948Z",
+ "status": {
+ "icon": "status_success",
+ "text": "passed",
+ "label": "passed",
+ "group": "success",
+ "tooltip": "passed",
+ "has_details": true,
+ "details_path": "/gitlab-org/gl-vue-cli/-/jobs/122845352",
+ "illustration": {
+ "image": "https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
+ "size": "svg-430",
+ "title": "This job does not have a trace."
+ },
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png",
+ "action": {
+ "icon": "retry",
+ "title": "Retry",
+ "path": "/gitlab-org/gl-vue-cli/-/jobs/122845352/retry",
+ "method": "post",
+ "button_title": "Retry this job"
+ }
+ }
+ }
+ ]
+ }
+ ],
+ "status": {
+ "icon": "status_success",
+ "text": "passed",
+ "label": "passed",
+ "group": "success",
+ "tooltip": "passed",
+ "has_details": true,
+ "details_path": "/gitlab-org/gl-vue-cli/pipelines/37232567#test",
+ "illustration": null,
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png"
+ },
+ "path": "/gitlab-org/gl-vue-cli/pipelines/37232567#test",
+ "dropdown_path": "/gitlab-org/gl-vue-cli/pipelines/37232567/stage.json?stage=test"
+ }
+ ],
+ "artifacts": [],
+ "manual_actions": [],
+ "scheduled_actions": []
+ },
+ "ref": {
+ "name": "master",
+ "path": "/gitlab-org/gl-vue-cli/commits/master",
+ "tag": false,
+ "branch": true
+ },
+ "commit": {
+ "id": "8f179601d481950bcb67032caeb33d1c24dde6bd",
+ "short_id": "8f179601",
+ "title": "Merge branch 'gl-cli-startt' into 'master'",
+ "created_at": "2018-11-20T10:22:51.000Z",
+ "parent_ids": [
+ "781d78fcd3d6c17ccf208f0cf0ab47c3e5397118",
+ "d227a0bb858c48eeee7393fcade1a33748f35183"
+ ],
+ "message": "Merge branch 'gl-cli-startt' into 'master'\n\nFirst iteration of the CLI\n\nCloses gitlab-foss#53657\n\nSee merge request gitlab-org/gl-vue-cli!2",
+ "author_name": "Phil Hughes",
+ "author_email": "me@iamphill.com",
+ "authored_date": "2018-11-20T10:22:51.000Z",
+ "committer_name": "Phil Hughes",
+ "committer_email": "me@iamphill.com",
+ "committed_date": "2018-11-20T10:22:51.000Z",
+ "author": {
+ "id": 113870,
+ "name": "Phil Hughes",
+ "username": "iamphill",
+ "state": "active",
+ "avatar_url": "https://secure.gravatar.com/avatar/533a51534470a11062df393543eab649?s=80\u0026d=identicon",
+ "web_url": "https://gitlab.com/iamphill",
+ "status_tooltip_html": null,
+ "path": "/iamphill"
+ },
+ "author_gravatar_url": "https://secure.gravatar.com/avatar/533a51534470a11062df393543eab649?s=80\u0026d=identicon",
+ "commit_url": "https://gitlab.com/gitlab-org/gl-vue-cli/commit/8f179601d481950bcb67032caeb33d1c24dde6bd",
+ "commit_path": "/gitlab-org/gl-vue-cli/commit/8f179601d481950bcb67032caeb33d1c24dde6bd"
+ },
+ "triggered_by": null,
+ "triggered": []
+}
diff --git a/spec/javascripts/pipelines/stores/pipeline_store.js b/spec/javascripts/pipelines/stores/pipeline_store.js
new file mode 100644
index 00000000000..4a0b3bf4c02
--- /dev/null
+++ b/spec/javascripts/pipelines/stores/pipeline_store.js
@@ -0,0 +1,165 @@
+import PipelineStore from '~/pipelines/stores/pipeline_store';
+import LinkedPipelines from '../linked_pipelines_mock.json';
+
+describe('EE Pipeline store', () => {
+ let store;
+ let data;
+
+ beforeEach(() => {
+ store = new PipelineStore();
+ data = Object.assign({}, LinkedPipelines);
+ });
+
+ describe('storePipeline', () => {
+ beforeAll(() => {
+ store.storePipeline(data);
+ });
+
+ describe('triggered_by', () => {
+ it('sets triggered_by as an array', () => {
+ expect(store.state.pipeline.triggered_by.length).toEqual(1);
+ });
+
+ it('adds isExpanding & isLoading keys set to false', () => {
+ expect(store.state.pipeline.triggered_by[0].isExpanded).toEqual(false);
+ expect(store.state.pipeline.triggered_by[0].isLoading).toEqual(false);
+ });
+
+ it('parses nested triggered_by', () => {
+ expect(store.state.pipeline.triggered_by[0].triggered_by.length).toEqual(1);
+ expect(store.state.pipeline.triggered_by[0].triggered_by[0].isExpanded).toEqual(false);
+ expect(store.state.pipeline.triggered_by[0].triggered_by[0].isLoading).toEqual(false);
+ });
+ });
+
+ describe('triggered', () => {
+ it('adds isExpanding & isLoading keys set to false for each triggered pipeline', () => {
+ store.state.pipeline.triggered.forEach(pipeline => {
+ expect(pipeline.isExpanded).toEqual(false);
+ expect(pipeline.isLoading).toEqual(false);
+ });
+ });
+
+ it('parses nested triggered pipelines', () => {
+ store.state.pipeline.triggered[1].triggered.forEach(pipeline => {
+ expect(pipeline.isExpanded).toEqual(false);
+ expect(pipeline.isLoading).toEqual(false);
+ });
+ });
+ });
+ });
+
+ describe('resetTriggeredByPipeline', () => {
+ beforeEach(() => {
+ store.storePipeline(data);
+ });
+
+ it('closes the pipeline & nested ones', () => {
+ store.state.pipeline.triggered_by[0].isExpanded = true;
+ store.state.pipeline.triggered_by[0].triggered_by[0].isExpanded = true;
+
+ store.resetTriggeredByPipeline(store.state.pipeline, store.state.pipeline.triggered_by[0]);
+
+ expect(store.state.pipeline.triggered_by[0].isExpanded).toEqual(false);
+ expect(store.state.pipeline.triggered_by[0].triggered_by[0].isExpanded).toEqual(false);
+ });
+ });
+
+ describe('openTriggeredByPipeline', () => {
+ beforeEach(() => {
+ store.storePipeline(data);
+ });
+
+ it('opens the given pipeline', () => {
+ store.openTriggeredByPipeline(store.state.pipeline, store.state.pipeline.triggered_by[0]);
+
+ expect(store.state.pipeline.triggered_by[0].isExpanded).toEqual(true);
+ });
+ });
+
+ describe('closeTriggeredByPipeline', () => {
+ beforeEach(() => {
+ store.storePipeline(data);
+ });
+
+ it('closes the given pipeline', () => {
+ // open it first
+ store.openTriggeredByPipeline(store.state.pipeline, store.state.pipeline.triggered_by[0]);
+
+ store.closeTriggeredByPipeline(store.state.pipeline, store.state.pipeline.triggered_by[0]);
+
+ expect(store.state.pipeline.triggered_by[0].isExpanded).toEqual(false);
+ });
+ });
+
+ describe('resetTriggeredPipelines', () => {
+ beforeEach(() => {
+ store.storePipeline(data);
+ });
+
+ it('closes the pipeline & nested ones', () => {
+ store.state.pipeline.triggered[0].isExpanded = true;
+ store.state.pipeline.triggered[0].triggered[0].isExpanded = true;
+
+ store.resetTriggeredPipeline(store.state.pipeline, store.state.pipeline.triggered[0]);
+
+ expect(store.state.pipeline.triggered[0].isExpanded).toEqual(false);
+ expect(store.state.pipeline.triggered[0].triggered[0].isExpanded).toEqual(false);
+ });
+ });
+
+ describe('openTriggeredPipeline', () => {
+ beforeEach(() => {
+ store.storePipeline(data);
+ });
+
+ it('opens the given pipeline', () => {
+ store.openTriggeredPipeline(store.state.pipeline, store.state.pipeline.triggered[0]);
+
+ expect(store.state.pipeline.triggered[0].isExpanded).toEqual(true);
+ });
+ });
+
+ describe('closeTriggeredPipeline', () => {
+ beforeEach(() => {
+ store.storePipeline(data);
+ });
+
+ it('closes the given pipeline', () => {
+ // open it first
+ store.openTriggeredPipeline(store.state.pipeline, store.state.pipeline.triggered[0]);
+
+ store.closeTriggeredPipeline(store.state.pipeline, store.state.pipeline.triggered[0]);
+
+ expect(store.state.pipeline.triggered[0].isExpanded).toEqual(false);
+ });
+ });
+
+ describe('toggleLoading', () => {
+ beforeEach(() => {
+ store.storePipeline(data);
+ });
+
+ it('toggles the isLoading property for the given pipeline', () => {
+ store.togglePipeline(store.state.pipeline.triggered[0]);
+
+ expect(store.state.pipeline.triggered[0].isLoading).toEqual(true);
+ });
+ });
+
+ describe('addExpandedPipelineToRequestData', () => {
+ it('pushes the given id to expandedPipelines array', () => {
+ store.addExpandedPipelineToRequestData('213231');
+
+ expect(store.state.expandedPipelines).toEqual(['213231']);
+ });
+ });
+
+ describe('removeExpandedPipelineToRequestData', () => {
+ it('pushes the given id to expandedPipelines array', () => {
+ store.removeExpandedPipelineToRequestData('213231');
+
+ expect(store.state.expandedPipelines).toEqual([]);
+ });
+ });
+});
diff --git a/spec/javascripts/pipelines/stores/pipeline_with_triggered.json b/spec/javascripts/pipelines/stores/pipeline_with_triggered.json
new file mode 100644
index 00000000000..1fa15e45792
--- /dev/null
+++ b/spec/javascripts/pipelines/stores/pipeline_with_triggered.json
@@ -0,0 +1,381 @@
+{
+ "id": 23211253,
+ "user": {
+ "id": 3585,
+ "name": "Achilleas Pipinellis",
+ "username": "axil",
+ "state": "active",
+ "avatar_url": "https://assets.gitlab-static.net/uploads/-/system/user/avatar/3585/avatar.png",
+ "web_url": "https://gitlab.com/axil",
+ "status_tooltip_html": "\u003cspan class=\"user-status-emoji has-tooltip\" title=\"\" data-html=\"true\" data-placement=\"top\"\u003e\u003cgl-emoji title=\"trumpet\" data-name=\"trumpet\" data-unicode-version=\"6.0\"\u003e🎺\u003c/gl-emoji\u003e\u003c/span\u003e",
+ "path": "/axil"
+ },
+ "active": false,
+ "coverage": null,
+ "source": "push",
+ "created_at": "2018-06-05T11:31:30.452Z",
+ "updated_at": "2018-10-31T16:35:31.305Z",
+ "path": "/gitlab-org/gitlab-runner/pipelines/23211253",
+ "flags": {
+ "latest": false,
+ "stuck": false,
+ "auto_devops": false,
+ "yaml_errors": false,
+ "retryable": false,
+ "cancelable": false,
+ "failure_reason": false
+ },
+ "details": {
+ "status": {
+ "icon": "status_success",
+ "text": "passed",
+ "label": "passed",
+ "group": "success",
+ "tooltip": "passed",
+ "has_details": true,
+ "details_path": "/gitlab-org/gitlab-runner/pipelines/23211253",
+ "illustration": null,
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png"
+ },
+ "duration": 53,
+ "finished_at": "2018-10-31T16:35:31.299Z",
+ "stages": [
+ {
+ "name": "prebuild",
+ "title": "prebuild: passed",
+ "groups": [
+ {
+ "name": "review-docs-deploy",
+ "size": 1,
+ "status": {
+ "icon": "status_success",
+ "text": "passed",
+ "label": "manual play action",
+ "group": "success",
+ "tooltip": "passed",
+ "has_details": true,
+ "details_path": "/gitlab-org/gitlab-runner/-/jobs/72469032",
+ "illustration": {
+ "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg",
+ "size": "svg-394",
+ "title": "This job requires a manual action",
+ "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments"
+ },
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png",
+ "action": {
+ "icon": "play",
+ "title": "Play",
+ "path": "/gitlab-org/gitlab-runner/-/jobs/72469032/play",
+ "method": "post",
+ "button_title": "Trigger this manual action"
+ }
+ },
+ "jobs": [
+ {
+ "id": 72469032,
+ "name": "review-docs-deploy",
+ "started": "2018-10-31T16:34:58.778Z",
+ "archived": false,
+ "build_path": "/gitlab-org/gitlab-runner/-/jobs/72469032",
+ "retry_path": "/gitlab-org/gitlab-runner/-/jobs/72469032/retry",
+ "play_path": "/gitlab-org/gitlab-runner/-/jobs/72469032/play",
+ "playable": true,
+ "scheduled": false,
+ "created_at": "2018-06-05T11:31:30.495Z",
+ "updated_at": "2018-10-31T16:35:31.251Z",
+ "status": {
+ "icon": "status_success",
+ "text": "passed",
+ "label": "manual play action",
+ "group": "success",
+ "tooltip": "passed",
+ "has_details": true,
+ "details_path": "/gitlab-org/gitlab-runner/-/jobs/72469032",
+ "illustration": {
+ "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg",
+ "size": "svg-394",
+ "title": "This job requires a manual action",
+ "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments"
+ },
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png",
+ "action": {
+ "icon": "play",
+ "title": "Play",
+ "path": "/gitlab-org/gitlab-runner/-/jobs/72469032/play",
+ "method": "post",
+ "button_title": "Trigger this manual action"
+ }
+ }
+ }
+ ]
+ }
+ ],
+ "status": {
+ "icon": "status_success",
+ "text": "passed",
+ "label": "passed",
+ "group": "success",
+ "tooltip": "passed",
+ "has_details": true,
+ "details_path": "/gitlab-org/gitlab-runner/pipelines/23211253#prebuild",
+ "illustration": null,
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png"
+ },
+ "path": "/gitlab-org/gitlab-runner/pipelines/23211253#prebuild",
+ "dropdown_path": "/gitlab-org/gitlab-runner/pipelines/23211253/stage.json?stage=prebuild"
+ },
+ {
+ "name": "test",
+ "title": "test: passed",
+ "groups": [
+ {
+ "name": "docs check links",
+ "size": 1,
+ "status": {
+ "icon": "status_success",
+ "text": "passed",
+ "label": "passed",
+ "group": "success",
+ "tooltip": "passed",
+ "has_details": true,
+ "details_path": "/gitlab-org/gitlab-runner/-/jobs/72469033",
+ "illustration": {
+ "image": "https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
+ "size": "svg-430",
+ "title": "This job does not have a trace."
+ },
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png",
+ "action": {
+ "icon": "retry",
+ "title": "Retry",
+ "path": "/gitlab-org/gitlab-runner/-/jobs/72469033/retry",
+ "method": "post",
+ "button_title": "Retry this job"
+ }
+ },
+ "jobs": [
+ {
+ "id": 72469033,
+ "name": "docs check links",
+ "started": "2018-06-05T11:31:33.240Z",
+ "archived": false,
+ "build_path": "/gitlab-org/gitlab-runner/-/jobs/72469033",
+ "retry_path": "/gitlab-org/gitlab-runner/-/jobs/72469033/retry",
+ "playable": false,
+ "scheduled": false,
+ "created_at": "2018-06-05T11:31:30.627Z",
+ "updated_at": "2018-06-05T11:31:54.363Z",
+ "status": {
+ "icon": "status_success",
+ "text": "passed",
+ "label": "passed",
+ "group": "success",
+ "tooltip": "passed",
+ "has_details": true,
+ "details_path": "/gitlab-org/gitlab-runner/-/jobs/72469033",
+ "illustration": {
+ "image": "https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
+ "size": "svg-430",
+ "title": "This job does not have a trace."
+ },
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png",
+ "action": {
+ "icon": "retry",
+ "title": "Retry",
+ "path": "/gitlab-org/gitlab-runner/-/jobs/72469033/retry",
+ "method": "post",
+ "button_title": "Retry this job"
+ }
+ }
+ }
+ ]
+ }
+ ],
+ "status": {
+ "icon": "status_success",
+ "text": "passed",
+ "label": "passed",
+ "group": "success",
+ "tooltip": "passed",
+ "has_details": true,
+ "details_path": "/gitlab-org/gitlab-runner/pipelines/23211253#test",
+ "illustration": null,
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png"
+ },
+ "path": "/gitlab-org/gitlab-runner/pipelines/23211253#test",
+ "dropdown_path": "/gitlab-org/gitlab-runner/pipelines/23211253/stage.json?stage=test"
+ },
+ {
+ "name": "cleanup",
+ "title": "cleanup: skipped",
+ "groups": [
+ {
+ "name": "review-docs-cleanup",
+ "size": 1,
+ "status": {
+ "icon": "status_manual",
+ "text": "manual",
+ "label": "manual stop action",
+ "group": "manual",
+ "tooltip": "manual action",
+ "has_details": true,
+ "details_path": "/gitlab-org/gitlab-runner/-/jobs/72469034",
+ "illustration": {
+ "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg",
+ "size": "svg-394",
+ "title": "This job requires a manual action",
+ "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments"
+ },
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png",
+ "action": {
+ "icon": "stop",
+ "title": "Stop",
+ "path": "/gitlab-org/gitlab-runner/-/jobs/72469034/play",
+ "method": "post",
+ "button_title": "Stop this environment"
+ }
+ },
+ "jobs": [
+ {
+ "id": 72469034,
+ "name": "review-docs-cleanup",
+ "started": null,
+ "archived": false,
+ "build_path": "/gitlab-org/gitlab-runner/-/jobs/72469034",
+ "play_path": "/gitlab-org/gitlab-runner/-/jobs/72469034/play",
+ "playable": true,
+ "scheduled": false,
+ "created_at": "2018-06-05T11:31:30.760Z",
+ "updated_at": "2018-06-05T11:31:56.037Z",
+ "status": {
+ "icon": "status_manual",
+ "text": "manual",
+ "label": "manual stop action",
+ "group": "manual",
+ "tooltip": "manual action",
+ "has_details": true,
+ "details_path": "/gitlab-org/gitlab-runner/-/jobs/72469034",
+ "illustration": {
+ "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg",
+ "size": "svg-394",
+ "title": "This job requires a manual action",
+ "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments"
+ },
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png",
+ "action": {
+ "icon": "stop",
+ "title": "Stop",
+ "path": "/gitlab-org/gitlab-runner/-/jobs/72469034/play",
+ "method": "post",
+ "button_title": "Stop this environment"
+ }
+ }
+ }
+ ]
+ }
+ ],
+ "status": {
+ "icon": "status_skipped",
+ "text": "skipped",
+ "label": "skipped",
+ "group": "skipped",
+ "tooltip": "skipped",
+ "has_details": true,
+ "details_path": "/gitlab-org/gitlab-runner/pipelines/23211253#cleanup",
+ "illustration": null,
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png"
+ },
+ "path": "/gitlab-org/gitlab-runner/pipelines/23211253#cleanup",
+ "dropdown_path": "/gitlab-org/gitlab-runner/pipelines/23211253/stage.json?stage=cleanup"
+ }
+ ],
+ "artifacts": [],
+ "manual_actions": [
+ {
+ "name": "review-docs-cleanup",
+ "path": "/gitlab-org/gitlab-runner/-/jobs/72469034/play",
+ "playable": true,
+ "scheduled": false
+ },
+ {
+ "name": "review-docs-deploy",
+ "path": "/gitlab-org/gitlab-runner/-/jobs/72469032/play",
+ "playable": true,
+ "scheduled": false
+ }
+ ],
+ "scheduled_actions": []
+ },
+ "ref": {
+ "name": "docs/add-development-guide-to-readme",
+ "path": "/gitlab-org/gitlab-runner/commits/docs/add-development-guide-to-readme",
+ "tag": false,
+ "branch": true
+ },
+ "commit": {
+ "id": "8083eb0a920572214d0dccedd7981f05d535ad46",
+ "short_id": "8083eb0a",
+ "title": "Add link to development guide in readme",
+ "created_at": "2018-06-05T11:30:48.000Z",
+ "parent_ids": ["1d7cf79b5a1a2121b9474ac20d61c1b8f621289d"],
+ "message": "Add link to development guide in readme\n\nCloses https://gitlab.com/gitlab-org/gitlab-runner/issues/3122\n",
+ "author_name": "Achilleas Pipinellis",
+ "author_email": "axil@gitlab.com",
+ "authored_date": "2018-06-05T11:30:48.000Z",
+ "committer_name": "Achilleas Pipinellis",
+ "committer_email": "axil@gitlab.com",
+ "committed_date": "2018-06-05T11:30:48.000Z",
+ "author": {
+ "id": 3585,
+ "name": "Achilleas Pipinellis",
+ "username": "axil",
+ "state": "active",
+ "avatar_url": "https://assets.gitlab-static.net/uploads/-/system/user/avatar/3585/avatar.png",
+ "web_url": "https://gitlab.com/axil",
+ "status_tooltip_html": null,
+ "path": "/axil"
+ },
+ "author_gravatar_url": "https://secure.gravatar.com/avatar/1d37af00eec153a8333a4ce18e9aea41?s=80\u0026d=identicon",
+ "commit_url": "https://gitlab.com/gitlab-org/gitlab-runner/commit/8083eb0a920572214d0dccedd7981f05d535ad46",
+ "commit_path": "/gitlab-org/gitlab-runner/commit/8083eb0a920572214d0dccedd7981f05d535ad46"
+ },
+ "triggered_by": null,
+ "triggered": [
+ {
+ "id": 34993051,
+ "user": {
+ "id": 376774,
+ "name": "Alessio Caiazza",
+ "username": "nolith",
+ "state": "active",
+ "avatar_url": "https://assets.gitlab-static.net/uploads/-/system/user/avatar/376774/avatar.png",
+ "web_url": "https://gitlab.com/nolith",
+ "status_tooltip_html": null,
+ "path": "/nolith"
+ },
+ "active": false,
+ "coverage": null,
+ "source": "pipeline",
+ "path": "/gitlab-com/gitlab-docs/pipelines/34993051",
+ "details": {
+ "status": {
+ "icon": "status_failed",
+ "text": "failed",
+ "label": "failed",
+ "group": "failed",
+ "tooltip": "failed",
+ "has_details": true,
+ "details_path": "/gitlab-com/gitlab-docs/pipelines/34993051",
+ "illustration": null,
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_failed-41304d7f7e3828808b0c26771f0309e55296819a9beea3ea9fbf6689d9857c12.png"
+ }
+ },
+ "project": {
+ "id": 1794617,
+ "name": "GitLab Docs",
+ "full_path": "/gitlab-com/gitlab-docs",
+ "full_name": "GitLab.com / GitLab Docs"
+ }
+ }
+ ]
+}
diff --git a/spec/javascripts/pipelines/stores/pipeline_with_triggered_by.json b/spec/javascripts/pipelines/stores/pipeline_with_triggered_by.json
new file mode 100644
index 00000000000..7aeea6f3ebb
--- /dev/null
+++ b/spec/javascripts/pipelines/stores/pipeline_with_triggered_by.json
@@ -0,0 +1,379 @@
+{
+ "id": 23211253,
+ "user": {
+ "id": 3585,
+ "name": "Achilleas Pipinellis",
+ "username": "axil",
+ "state": "active",
+ "avatar_url": "https://assets.gitlab-static.net/uploads/-/system/user/avatar/3585/avatar.png",
+ "web_url": "https://gitlab.com/axil",
+ "status_tooltip_html": "\u003cspan class=\"user-status-emoji has-tooltip\" title=\"\" data-html=\"true\" data-placement=\"top\"\u003e\u003cgl-emoji title=\"trumpet\" data-name=\"trumpet\" data-unicode-version=\"6.0\"\u003e🎺\u003c/gl-emoji\u003e\u003c/span\u003e",
+ "path": "/axil"
+ },
+ "active": false,
+ "coverage": null,
+ "source": "push",
+ "created_at": "2018-06-05T11:31:30.452Z",
+ "updated_at": "2018-10-31T16:35:31.305Z",
+ "path": "/gitlab-org/gitlab-runner/pipelines/23211253",
+ "flags": {
+ "latest": false,
+ "stuck": false,
+ "auto_devops": false,
+ "yaml_errors": false,
+ "retryable": false,
+ "cancelable": false,
+ "failure_reason": false
+ },
+ "details": {
+ "status": {
+ "icon": "status_success",
+ "text": "passed",
+ "label": "passed",
+ "group": "success",
+ "tooltip": "passed",
+ "has_details": true,
+ "details_path": "/gitlab-org/gitlab-runner/pipelines/23211253",
+ "illustration": null,
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png"
+ },
+ "duration": 53,
+ "finished_at": "2018-10-31T16:35:31.299Z",
+ "stages": [
+ {
+ "name": "prebuild",
+ "title": "prebuild: passed",
+ "groups": [
+ {
+ "name": "review-docs-deploy",
+ "size": 1,
+ "status": {
+ "icon": "status_success",
+ "text": "passed",
+ "label": "manual play action",
+ "group": "success",
+ "tooltip": "passed",
+ "has_details": true,
+ "details_path": "/gitlab-org/gitlab-runner/-/jobs/72469032",
+ "illustration": {
+ "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg",
+ "size": "svg-394",
+ "title": "This job requires a manual action",
+ "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments"
+ },
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png",
+ "action": {
+ "icon": "play",
+ "title": "Play",
+ "path": "/gitlab-org/gitlab-runner/-/jobs/72469032/play",
+ "method": "post",
+ "button_title": "Trigger this manual action"
+ }
+ },
+ "jobs": [
+ {
+ "id": 72469032,
+ "name": "review-docs-deploy",
+ "started": "2018-10-31T16:34:58.778Z",
+ "archived": false,
+ "build_path": "/gitlab-org/gitlab-runner/-/jobs/72469032",
+ "retry_path": "/gitlab-org/gitlab-runner/-/jobs/72469032/retry",
+ "play_path": "/gitlab-org/gitlab-runner/-/jobs/72469032/play",
+ "playable": true,
+ "scheduled": false,
+ "created_at": "2018-06-05T11:31:30.495Z",
+ "updated_at": "2018-10-31T16:35:31.251Z",
+ "status": {
+ "icon": "status_success",
+ "text": "passed",
+ "label": "manual play action",
+ "group": "success",
+ "tooltip": "passed",
+ "has_details": true,
+ "details_path": "/gitlab-org/gitlab-runner/-/jobs/72469032",
+ "illustration": {
+ "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg",
+ "size": "svg-394",
+ "title": "This job requires a manual action",
+ "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments"
+ },
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png",
+ "action": {
+ "icon": "play",
+ "title": "Play",
+ "path": "/gitlab-org/gitlab-runner/-/jobs/72469032/play",
+ "method": "post",
+ "button_title": "Trigger this manual action"
+ }
+ }
+ }
+ ]
+ }
+ ],
+ "status": {
+ "icon": "status_success",
+ "text": "passed",
+ "label": "passed",
+ "group": "success",
+ "tooltip": "passed",
+ "has_details": true,
+ "details_path": "/gitlab-org/gitlab-runner/pipelines/23211253#prebuild",
+ "illustration": null,
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png"
+ },
+ "path": "/gitlab-org/gitlab-runner/pipelines/23211253#prebuild",
+ "dropdown_path": "/gitlab-org/gitlab-runner/pipelines/23211253/stage.json?stage=prebuild"
+ },
+ {
+ "name": "test",
+ "title": "test: passed",
+ "groups": [
+ {
+ "name": "docs check links",
+ "size": 1,
+ "status": {
+ "icon": "status_success",
+ "text": "passed",
+ "label": "passed",
+ "group": "success",
+ "tooltip": "passed",
+ "has_details": true,
+ "details_path": "/gitlab-org/gitlab-runner/-/jobs/72469033",
+ "illustration": {
+ "image": "https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
+ "size": "svg-430",
+ "title": "This job does not have a trace."
+ },
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png",
+ "action": {
+ "icon": "retry",
+ "title": "Retry",
+ "path": "/gitlab-org/gitlab-runner/-/jobs/72469033/retry",
+ "method": "post",
+ "button_title": "Retry this job"
+ }
+ },
+ "jobs": [
+ {
+ "id": 72469033,
+ "name": "docs check links",
+ "started": "2018-06-05T11:31:33.240Z",
+ "archived": false,
+ "build_path": "/gitlab-org/gitlab-runner/-/jobs/72469033",
+ "retry_path": "/gitlab-org/gitlab-runner/-/jobs/72469033/retry",
+ "playable": false,
+ "scheduled": false,
+ "created_at": "2018-06-05T11:31:30.627Z",
+ "updated_at": "2018-06-05T11:31:54.363Z",
+ "status": {
+ "icon": "status_success",
+ "text": "passed",
+ "label": "passed",
+ "group": "success",
+ "tooltip": "passed",
+ "has_details": true,
+ "details_path": "/gitlab-org/gitlab-runner/-/jobs/72469033",
+ "illustration": {
+ "image": "https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
+ "size": "svg-430",
+ "title": "This job does not have a trace."
+ },
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png",
+ "action": {
+ "icon": "retry",
+ "title": "Retry",
+ "path": "/gitlab-org/gitlab-runner/-/jobs/72469033/retry",
+ "method": "post",
+ "button_title": "Retry this job"
+ }
+ }
+ }
+ ]
+ }
+ ],
+ "status": {
+ "icon": "status_success",
+ "text": "passed",
+ "label": "passed",
+ "group": "success",
+ "tooltip": "passed",
+ "has_details": true,
+ "details_path": "/gitlab-org/gitlab-runner/pipelines/23211253#test",
+ "illustration": null,
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png"
+ },
+ "path": "/gitlab-org/gitlab-runner/pipelines/23211253#test",
+ "dropdown_path": "/gitlab-org/gitlab-runner/pipelines/23211253/stage.json?stage=test"
+ },
+ {
+ "name": "cleanup",
+ "title": "cleanup: skipped",
+ "groups": [
+ {
+ "name": "review-docs-cleanup",
+ "size": 1,
+ "status": {
+ "icon": "status_manual",
+ "text": "manual",
+ "label": "manual stop action",
+ "group": "manual",
+ "tooltip": "manual action",
+ "has_details": true,
+ "details_path": "/gitlab-org/gitlab-runner/-/jobs/72469034",
+ "illustration": {
+ "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg",
+ "size": "svg-394",
+ "title": "This job requires a manual action",
+ "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments"
+ },
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png",
+ "action": {
+ "icon": "stop",
+ "title": "Stop",
+ "path": "/gitlab-org/gitlab-runner/-/jobs/72469034/play",
+ "method": "post",
+ "button_title": "Stop this environment"
+ }
+ },
+ "jobs": [
+ {
+ "id": 72469034,
+ "name": "review-docs-cleanup",
+ "started": null,
+ "archived": false,
+ "build_path": "/gitlab-org/gitlab-runner/-/jobs/72469034",
+ "play_path": "/gitlab-org/gitlab-runner/-/jobs/72469034/play",
+ "playable": true,
+ "scheduled": false,
+ "created_at": "2018-06-05T11:31:30.760Z",
+ "updated_at": "2018-06-05T11:31:56.037Z",
+ "status": {
+ "icon": "status_manual",
+ "text": "manual",
+ "label": "manual stop action",
+ "group": "manual",
+ "tooltip": "manual action",
+ "has_details": true,
+ "details_path": "/gitlab-org/gitlab-runner/-/jobs/72469034",
+ "illustration": {
+ "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg",
+ "size": "svg-394",
+ "title": "This job requires a manual action",
+ "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments"
+ },
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png",
+ "action": {
+ "icon": "stop",
+ "title": "Stop",
+ "path": "/gitlab-org/gitlab-runner/-/jobs/72469034/play",
+ "method": "post",
+ "button_title": "Stop this environment"
+ }
+ }
+ }
+ ]
+ }
+ ],
+ "status": {
+ "icon": "status_skipped",
+ "text": "skipped",
+ "label": "skipped",
+ "group": "skipped",
+ "tooltip": "skipped",
+ "has_details": true,
+ "details_path": "/gitlab-org/gitlab-runner/pipelines/23211253#cleanup",
+ "illustration": null,
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png"
+ },
+ "path": "/gitlab-org/gitlab-runner/pipelines/23211253#cleanup",
+ "dropdown_path": "/gitlab-org/gitlab-runner/pipelines/23211253/stage.json?stage=cleanup"
+ }
+ ],
+ "artifacts": [],
+ "manual_actions": [
+ {
+ "name": "review-docs-cleanup",
+ "path": "/gitlab-org/gitlab-runner/-/jobs/72469034/play",
+ "playable": true,
+ "scheduled": false
+ },
+ {
+ "name": "review-docs-deploy",
+ "path": "/gitlab-org/gitlab-runner/-/jobs/72469032/play",
+ "playable": true,
+ "scheduled": false
+ }
+ ],
+ "scheduled_actions": []
+ },
+ "ref": {
+ "name": "docs/add-development-guide-to-readme",
+ "path": "/gitlab-org/gitlab-runner/commits/docs/add-development-guide-to-readme",
+ "tag": false,
+ "branch": true
+ },
+ "commit": {
+ "id": "8083eb0a920572214d0dccedd7981f05d535ad46",
+ "short_id": "8083eb0a",
+ "title": "Add link to development guide in readme",
+ "created_at": "2018-06-05T11:30:48.000Z",
+ "parent_ids": ["1d7cf79b5a1a2121b9474ac20d61c1b8f621289d"],
+ "message": "Add link to development guide in readme\n\nCloses https://gitlab.com/gitlab-org/gitlab-runner/issues/3122\n",
+ "author_name": "Achilleas Pipinellis",
+ "author_email": "axil@gitlab.com",
+ "authored_date": "2018-06-05T11:30:48.000Z",
+ "committer_name": "Achilleas Pipinellis",
+ "committer_email": "axil@gitlab.com",
+ "committed_date": "2018-06-05T11:30:48.000Z",
+ "author": {
+ "id": 3585,
+ "name": "Achilleas Pipinellis",
+ "username": "axil",
+ "state": "active",
+ "avatar_url": "https://assets.gitlab-static.net/uploads/-/system/user/avatar/3585/avatar.png",
+ "web_url": "https://gitlab.com/axil",
+ "status_tooltip_html": null,
+ "path": "/axil"
+ },
+ "author_gravatar_url": "https://secure.gravatar.com/avatar/1d37af00eec153a8333a4ce18e9aea41?s=80\u0026d=identicon",
+ "commit_url": "https://gitlab.com/gitlab-org/gitlab-runner/commit/8083eb0a920572214d0dccedd7981f05d535ad46",
+ "commit_path": "/gitlab-org/gitlab-runner/commit/8083eb0a920572214d0dccedd7981f05d535ad46"
+ },
+ "triggered_by": {
+ "id": 34993051,
+ "user": {
+ "id": 376774,
+ "name": "Alessio Caiazza",
+ "username": "nolith",
+ "state": "active",
+ "avatar_url": "https://assets.gitlab-static.net/uploads/-/system/user/avatar/376774/avatar.png",
+ "web_url": "https://gitlab.com/nolith",
+ "status_tooltip_html": null,
+ "path": "/nolith"
+ },
+ "active": false,
+ "coverage": null,
+ "source": "pipeline",
+ "path": "/gitlab-com/gitlab-docs/pipelines/34993051",
+ "details": {
+ "status": {
+ "icon": "status_failed",
+ "text": "failed",
+ "label": "failed",
+ "group": "failed",
+ "tooltip": "failed",
+ "has_details": true,
+ "details_path": "/gitlab-com/gitlab-docs/pipelines/34993051",
+ "illustration": null,
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_failed-41304d7f7e3828808b0c26771f0309e55296819a9beea3ea9fbf6689d9857c12.png"
+ }
+ },
+ "project": {
+ "id": 1794617,
+ "name": "GitLab Docs",
+ "full_path": "/gitlab-com/gitlab-docs",
+ "full_name": "GitLab.com / GitLab Docs"
+ }
+ },
+ "triggered": []
+}
diff --git a/spec/javascripts/pipelines/stores/pipeline_with_triggered_triggered_by.json b/spec/javascripts/pipelines/stores/pipeline_with_triggered_triggered_by.json
new file mode 100644
index 00000000000..2402cbae6c8
--- /dev/null
+++ b/spec/javascripts/pipelines/stores/pipeline_with_triggered_triggered_by.json
@@ -0,0 +1,452 @@
+{
+ "id": 23211253,
+ "user": {
+ "id": 3585,
+ "name": "Achilleas Pipinellis",
+ "username": "axil",
+ "state": "active",
+ "avatar_url": "https://assets.gitlab-static.net/uploads/-/system/user/avatar/3585/avatar.png",
+ "web_url": "https://gitlab.com/axil",
+ "status_tooltip_html": "\u003cspan class=\"user-status-emoji has-tooltip\" title=\"\" data-html=\"true\" data-placement=\"top\"\u003e\u003cgl-emoji title=\"trumpet\" data-name=\"trumpet\" data-unicode-version=\"6.0\"\u003e🎺\u003c/gl-emoji\u003e\u003c/span\u003e",
+ "path": "/axil"
+ },
+ "active": false,
+ "coverage": null,
+ "source": "push",
+ "created_at": "2018-06-05T11:31:30.452Z",
+ "updated_at": "2018-10-31T16:35:31.305Z",
+ "path": "/gitlab-org/gitlab-runner/pipelines/23211253",
+ "flags": {
+ "latest": false,
+ "stuck": false,
+ "auto_devops": false,
+ "yaml_errors": false,
+ "retryable": false,
+ "cancelable": false,
+ "failure_reason": false
+ },
+ "details": {
+ "status": {
+ "icon": "status_success",
+ "text": "passed",
+ "label": "passed",
+ "group": "success",
+ "tooltip": "passed",
+ "has_details": true,
+ "details_path": "/gitlab-org/gitlab-runner/pipelines/23211253",
+ "illustration": null,
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png"
+ },
+ "duration": 53,
+ "finished_at": "2018-10-31T16:35:31.299Z",
+ "stages": [
+ {
+ "name": "prebuild",
+ "title": "prebuild: passed",
+ "groups": [
+ {
+ "name": "review-docs-deploy",
+ "size": 1,
+ "status": {
+ "icon": "status_success",
+ "text": "passed",
+ "label": "manual play action",
+ "group": "success",
+ "tooltip": "passed",
+ "has_details": true,
+ "details_path": "/gitlab-org/gitlab-runner/-/jobs/72469032",
+ "illustration": {
+ "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg",
+ "size": "svg-394",
+ "title": "This job requires a manual action",
+ "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments"
+ },
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png",
+ "action": {
+ "icon": "play",
+ "title": "Play",
+ "path": "/gitlab-org/gitlab-runner/-/jobs/72469032/play",
+ "method": "post",
+ "button_title": "Trigger this manual action"
+ }
+ },
+ "jobs": [
+ {
+ "id": 72469032,
+ "name": "review-docs-deploy",
+ "started": "2018-10-31T16:34:58.778Z",
+ "archived": false,
+ "build_path": "/gitlab-org/gitlab-runner/-/jobs/72469032",
+ "retry_path": "/gitlab-org/gitlab-runner/-/jobs/72469032/retry",
+ "play_path": "/gitlab-org/gitlab-runner/-/jobs/72469032/play",
+ "playable": true,
+ "scheduled": false,
+ "created_at": "2018-06-05T11:31:30.495Z",
+ "updated_at": "2018-10-31T16:35:31.251Z",
+ "status": {
+ "icon": "status_success",
+ "text": "passed",
+ "label": "manual play action",
+ "group": "success",
+ "tooltip": "passed",
+ "has_details": true,
+ "details_path": "/gitlab-org/gitlab-runner/-/jobs/72469032",
+ "illustration": {
+ "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg",
+ "size": "svg-394",
+ "title": "This job requires a manual action",
+ "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments"
+ },
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png",
+ "action": {
+ "icon": "play",
+ "title": "Play",
+ "path": "/gitlab-org/gitlab-runner/-/jobs/72469032/play",
+ "method": "post",
+ "button_title": "Trigger this manual action"
+ }
+ }
+ }
+ ]
+ }
+ ],
+ "status": {
+ "icon": "status_success",
+ "text": "passed",
+ "label": "passed",
+ "group": "success",
+ "tooltip": "passed",
+ "has_details": true,
+ "details_path": "/gitlab-org/gitlab-runner/pipelines/23211253#prebuild",
+ "illustration": null,
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png"
+ },
+ "path": "/gitlab-org/gitlab-runner/pipelines/23211253#prebuild",
+ "dropdown_path": "/gitlab-org/gitlab-runner/pipelines/23211253/stage.json?stage=prebuild"
+ },
+ {
+ "name": "test",
+ "title": "test: passed",
+ "groups": [
+ {
+ "name": "docs check links",
+ "size": 1,
+ "status": {
+ "icon": "status_success",
+ "text": "passed",
+ "label": "passed",
+ "group": "success",
+ "tooltip": "passed",
+ "has_details": true,
+ "details_path": "/gitlab-org/gitlab-runner/-/jobs/72469033",
+ "illustration": {
+ "image": "https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
+ "size": "svg-430",
+ "title": "This job does not have a trace."
+ },
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png",
+ "action": {
+ "icon": "retry",
+ "title": "Retry",
+ "path": "/gitlab-org/gitlab-runner/-/jobs/72469033/retry",
+ "method": "post",
+ "button_title": "Retry this job"
+ }
+ },
+ "jobs": [
+ {
+ "id": 72469033,
+ "name": "docs check links",
+ "started": "2018-06-05T11:31:33.240Z",
+ "archived": false,
+ "build_path": "/gitlab-org/gitlab-runner/-/jobs/72469033",
+ "retry_path": "/gitlab-org/gitlab-runner/-/jobs/72469033/retry",
+ "playable": false,
+ "scheduled": false,
+ "created_at": "2018-06-05T11:31:30.627Z",
+ "updated_at": "2018-06-05T11:31:54.363Z",
+ "status": {
+ "icon": "status_success",
+ "text": "passed",
+ "label": "passed",
+ "group": "success",
+ "tooltip": "passed",
+ "has_details": true,
+ "details_path": "/gitlab-org/gitlab-runner/-/jobs/72469033",
+ "illustration": {
+ "image": "https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
+ "size": "svg-430",
+ "title": "This job does not have a trace."
+ },
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png",
+ "action": {
+ "icon": "retry",
+ "title": "Retry",
+ "path": "/gitlab-org/gitlab-runner/-/jobs/72469033/retry",
+ "method": "post",
+ "button_title": "Retry this job"
+ }
+ }
+ }
+ ]
+ }
+ ],
+ "status": {
+ "icon": "status_success",
+ "text": "passed",
+ "label": "passed",
+ "group": "success",
+ "tooltip": "passed",
+ "has_details": true,
+ "details_path": "/gitlab-org/gitlab-runner/pipelines/23211253#test",
+ "illustration": null,
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png"
+ },
+ "path": "/gitlab-org/gitlab-runner/pipelines/23211253#test",
+ "dropdown_path": "/gitlab-org/gitlab-runner/pipelines/23211253/stage.json?stage=test"
+ },
+ {
+ "name": "cleanup",
+ "title": "cleanup: skipped",
+ "groups": [
+ {
+ "name": "review-docs-cleanup",
+ "size": 1,
+ "status": {
+ "icon": "status_manual",
+ "text": "manual",
+ "label": "manual stop action",
+ "group": "manual",
+ "tooltip": "manual action",
+ "has_details": true,
+ "details_path": "/gitlab-org/gitlab-runner/-/jobs/72469034",
+ "illustration": {
+ "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg",
+ "size": "svg-394",
+ "title": "This job requires a manual action",
+ "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments"
+ },
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png",
+ "action": {
+ "icon": "stop",
+ "title": "Stop",
+ "path": "/gitlab-org/gitlab-runner/-/jobs/72469034/play",
+ "method": "post",
+ "button_title": "Stop this environment"
+ }
+ },
+ "jobs": [
+ {
+ "id": 72469034,
+ "name": "review-docs-cleanup",
+ "started": null,
+ "archived": false,
+ "build_path": "/gitlab-org/gitlab-runner/-/jobs/72469034",
+ "play_path": "/gitlab-org/gitlab-runner/-/jobs/72469034/play",
+ "playable": true,
+ "scheduled": false,
+ "created_at": "2018-06-05T11:31:30.760Z",
+ "updated_at": "2018-06-05T11:31:56.037Z",
+ "status": {
+ "icon": "status_manual",
+ "text": "manual",
+ "label": "manual stop action",
+ "group": "manual",
+ "tooltip": "manual action",
+ "has_details": true,
+ "details_path": "/gitlab-org/gitlab-runner/-/jobs/72469034",
+ "illustration": {
+ "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg",
+ "size": "svg-394",
+ "title": "This job requires a manual action",
+ "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments"
+ },
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png",
+ "action": {
+ "icon": "stop",
+ "title": "Stop",
+ "path": "/gitlab-org/gitlab-runner/-/jobs/72469034/play",
+ "method": "post",
+ "button_title": "Stop this environment"
+ }
+ }
+ }
+ ]
+ }
+ ],
+ "status": {
+ "icon": "status_skipped",
+ "text": "skipped",
+ "label": "skipped",
+ "group": "skipped",
+ "tooltip": "skipped",
+ "has_details": true,
+ "details_path": "/gitlab-org/gitlab-runner/pipelines/23211253#cleanup",
+ "illustration": null,
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png"
+ },
+ "path": "/gitlab-org/gitlab-runner/pipelines/23211253#cleanup",
+ "dropdown_path": "/gitlab-org/gitlab-runner/pipelines/23211253/stage.json?stage=cleanup"
+ }
+ ],
+ "artifacts": [],
+ "manual_actions": [
+ {
+ "name": "review-docs-cleanup",
+ "path": "/gitlab-org/gitlab-runner/-/jobs/72469034/play",
+ "playable": true,
+ "scheduled": false
+ },
+ {
+ "name": "review-docs-deploy",
+ "path": "/gitlab-org/gitlab-runner/-/jobs/72469032/play",
+ "playable": true,
+ "scheduled": false
+ }
+ ],
+ "scheduled_actions": []
+ },
+ "ref": {
+ "name": "docs/add-development-guide-to-readme",
+ "path": "/gitlab-org/gitlab-runner/commits/docs/add-development-guide-to-readme",
+ "tag": false,
+ "branch": true
+ },
+ "commit": {
+ "id": "8083eb0a920572214d0dccedd7981f05d535ad46",
+ "short_id": "8083eb0a",
+ "title": "Add link to development guide in readme",
+ "created_at": "2018-06-05T11:30:48.000Z",
+ "parent_ids": ["1d7cf79b5a1a2121b9474ac20d61c1b8f621289d"],
+ "message": "Add link to development guide in readme\n\nCloses https://gitlab.com/gitlab-org/gitlab-runner/issues/3122\n",
+ "author_name": "Achilleas Pipinellis",
+ "author_email": "axil@gitlab.com",
+ "authored_date": "2018-06-05T11:30:48.000Z",
+ "committer_name": "Achilleas Pipinellis",
+ "committer_email": "axil@gitlab.com",
+ "committed_date": "2018-06-05T11:30:48.000Z",
+ "author": {
+ "id": 3585,
+ "name": "Achilleas Pipinellis",
+ "username": "axil",
+ "state": "active",
+ "avatar_url": "https://assets.gitlab-static.net/uploads/-/system/user/avatar/3585/avatar.png",
+ "web_url": "https://gitlab.com/axil",
+ "status_tooltip_html": null,
+ "path": "/axil"
+ },
+ "author_gravatar_url": "https://secure.gravatar.com/avatar/1d37af00eec153a8333a4ce18e9aea41?s=80\u0026d=identicon",
+ "commit_url": "https://gitlab.com/gitlab-org/gitlab-runner/commit/8083eb0a920572214d0dccedd7981f05d535ad46",
+ "commit_path": "/gitlab-org/gitlab-runner/commit/8083eb0a920572214d0dccedd7981f05d535ad46"
+ },
+ "triggered_by": {
+ "id": 34993051,
+ "user": {
+ "id": 376774,
+ "name": "Alessio Caiazza",
+ "username": "nolith",
+ "state": "active",
+ "avatar_url": "https://assets.gitlab-static.net/uploads/-/system/user/avatar/376774/avatar.png",
+ "web_url": "https://gitlab.com/nolith",
+ "status_tooltip_html": null,
+ "path": "/nolith"
+ },
+ "active": false,
+ "coverage": null,
+ "source": "pipeline",
+ "path": "/gitlab-com/gitlab-docs/pipelines/34993051",
+ "details": {
+ "status": {
+ "icon": "status_failed",
+ "text": "failed",
+ "label": "failed",
+ "group": "failed",
+ "tooltip": "failed",
+ "has_details": true,
+ "details_path": "/gitlab-com/gitlab-docs/pipelines/34993051",
+ "illustration": null,
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_failed-41304d7f7e3828808b0c26771f0309e55296819a9beea3ea9fbf6689d9857c12.png"
+ }
+ },
+ "project": {
+ "id": 1794617,
+ "name": "GitLab Docs",
+ "full_path": "/gitlab-com/gitlab-docs",
+ "full_name": "GitLab.com / GitLab Docs"
+ }
+ },
+ "triggered": [
+ {
+ "id": 349233051,
+ "user": {
+ "id": 376774,
+ "name": "Alessio Caiazza",
+ "username": "nolith",
+ "state": "active",
+ "avatar_url": "https://assets.gitlab-static.net/uploads/-/system/user/avatar/376774/avatar.png",
+ "web_url": "https://gitlab.com/nolith",
+ "status_tooltip_html": null,
+ "path": "/nolith"
+ },
+ "active": false,
+ "coverage": null,
+ "source": "pipeline",
+ "path": "/gitlab-com/gitlab-docs/pipelines/34993051",
+ "details": {
+ "status": {
+ "icon": "status_failed",
+ "text": "failed",
+ "label": "failed",
+ "group": "failed",
+ "tooltip": "failed",
+ "has_details": true,
+ "details_path": "/gitlab-com/gitlab-docs/pipelines/349233051",
+ "illustration": null,
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_failed-41304d7f7e3828808b0c26771f0309e55296819a9beea3ea9fbf6689d9857c12.png"
+ }
+ },
+ "project": {
+ "id": 1794617,
+ "name": "GitLab Docs",
+ "full_path": "/gitlab-com/gitlab-docs",
+ "full_name": "GitLab.com / GitLab Docs"
+ }
+ },
+ {
+ "id": 34993023,
+ "user": {
+ "id": 376774,
+ "name": "Alessio Caiazza",
+ "username": "nolith",
+ "state": "active",
+ "avatar_url": "https://assets.gitlab-static.net/uploads/-/system/user/avatar/376774/avatar.png",
+ "web_url": "https://gitlab.com/nolith",
+ "status_tooltip_html": null,
+ "path": "/nolith"
+ },
+ "active": false,
+ "coverage": null,
+ "source": "pipeline",
+ "path": "/gitlab-com/gitlab-docs/pipelines/34993023",
+ "details": {
+ "status": {
+ "icon": "status_failed",
+ "text": "failed",
+ "label": "failed",
+ "group": "failed",
+ "tooltip": "failed",
+ "has_details": true,
+ "details_path": "/gitlab-com/gitlab-docs/pipelines/34993051",
+ "illustration": null,
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_failed-41304d7f7e3828808b0c26771f0309e55296819a9beea3ea9fbf6689d9857c12.png"
+ }
+ },
+ "project": {
+ "id": 1794617,
+ "name": "GitLab Docs",
+ "full_path": "/gitlab-com/gitlab-docs",
+ "full_name": "GitLab.com / GitLab Docs"
+ }
+ }
+ ]
+}
diff --git a/spec/javascripts/registry/components/app_spec.js b/spec/javascripts/registry/components/app_spec.js
deleted file mode 100644
index 5ea3f85a247..00000000000
--- a/spec/javascripts/registry/components/app_spec.js
+++ /dev/null
@@ -1,129 +0,0 @@
-import MockAdapter from 'axios-mock-adapter';
-import axios from '~/lib/utils/axios_utils';
-import Vue from 'vue';
-import registry from '~/registry/components/app.vue';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
-import { TEST_HOST } from 'spec/test_constants';
-import { reposServerResponse } from '../mock_data';
-
-describe('Registry List', () => {
- const Component = Vue.extend(registry);
- const props = {
- endpoint: `${TEST_HOST}/foo`,
- helpPagePath: 'foo',
- noContainersImage: 'foo',
- containersErrorImage: 'foo',
- repositoryUrl: 'foo',
- };
- let vm;
- let mock;
-
- beforeEach(() => {
- mock = new MockAdapter(axios);
- });
-
- afterEach(() => {
- mock.restore();
- vm.$destroy();
- });
-
- describe('with data', () => {
- beforeEach(() => {
- mock.onGet(`${TEST_HOST}/foo`).replyOnce(200, reposServerResponse);
-
- vm = mountComponent(Component, { ...props });
- });
-
- it('should render a list of repos', done => {
- setTimeout(() => {
- expect(vm.$store.state.repos.length).toEqual(reposServerResponse.length);
-
- Vue.nextTick(() => {
- expect(vm.$el.querySelectorAll('.container-image').length).toEqual(
- reposServerResponse.length,
- );
- done();
- });
- }, 0);
- });
-
- describe('delete repository', () => {
- it('should be possible to delete a repo', done => {
- setTimeout(() => {
- Vue.nextTick(() => {
- expect(vm.$el.querySelector('.container-image-head .js-remove-repo')).toBeDefined();
- done();
- });
- }, 0);
- });
- });
-
- describe('toggle repository', () => {
- it('should open the container', done => {
- setTimeout(() => {
- Vue.nextTick(() => {
- vm.$el.querySelector('.js-toggle-repo').click();
- Vue.nextTick(() => {
- expect(
- vm.$el.querySelector('.js-toggle-repo use').getAttribute('xlink:href'),
- ).toContain('angle-up');
- done();
- });
- });
- }, 0);
- });
- });
- });
-
- describe('without data', () => {
- beforeEach(() => {
- mock.onGet(`${TEST_HOST}/foo`).replyOnce(200, []);
-
- vm = mountComponent(Component, { ...props });
- });
-
- it('should render empty message', done => {
- setTimeout(() => {
- expect(vm.$el.querySelector('.js-no-container-images-text').textContent).toEqual(
- 'With the Container Registry, every project can have its own space to store its Docker images. More Information',
- );
- done();
- }, 0);
- });
- });
-
- describe('while loading data', () => {
- beforeEach(() => {
- mock.onGet(`${TEST_HOST}/foo`).replyOnce(200, []);
-
- vm = mountComponent(Component, { ...props });
- });
-
- it('should render a loading spinner', done => {
- Vue.nextTick(() => {
- expect(vm.$el.querySelector('.gl-spinner')).not.toBe(null);
- done();
- });
- });
- });
-
- describe('invalid characters in path', () => {
- beforeEach(() => {
- mock.onGet(`${TEST_HOST}/foo`).replyOnce(200, []);
-
- vm = mountComponent(Component, {
- ...props,
- characterError: true,
- });
- });
-
- it('should render invalid characters error message', done => {
- setTimeout(() => {
- expect(vm.$el.querySelector('p')).not.toContain(
- 'We are having trouble connecting to Docker, which could be due to an issue with your project name or path. More information',
- );
- done();
- });
- });
- });
-});
diff --git a/spec/javascripts/registry/components/collapsible_container_spec.js b/spec/javascripts/registry/components/collapsible_container_spec.js
deleted file mode 100644
index 2a5d8dd11da..00000000000
--- a/spec/javascripts/registry/components/collapsible_container_spec.js
+++ /dev/null
@@ -1,87 +0,0 @@
-import MockAdapter from 'axios-mock-adapter';
-import axios from '~/lib/utils/axios_utils';
-import Vue from 'vue';
-import collapsibleComponent from '~/registry/components/collapsible_container.vue';
-import store from '~/registry/stores';
-import * as types from '~/registry/stores/mutation_types';
-
-import { repoPropsData, registryServerResponse, reposServerResponse } from '../mock_data';
-
-describe('collapsible registry container', () => {
- let vm;
- let mock;
- const Component = Vue.extend(collapsibleComponent);
-
- const findDeleteBtn = () => vm.$el.querySelector('.js-remove-repo');
-
- beforeEach(() => {
- mock = new MockAdapter(axios);
-
- mock.onGet(repoPropsData.tagsPath).replyOnce(200, registryServerResponse, {});
-
- store.commit(types.SET_REPOS_LIST, reposServerResponse);
-
- vm = new Component({
- store,
- propsData: {
- repo: repoPropsData,
- },
- }).$mount();
- });
-
- afterEach(() => {
- mock.restore();
- vm.$destroy();
- });
-
- describe('toggle', () => {
- it('should be closed by default', () => {
- expect(vm.$el.querySelector('.container-image-tags')).toBe(null);
- expect(vm.iconName).toEqual('angle-right');
- });
-
- it('should be open when user clicks on closed repo', done => {
- vm.$el.querySelector('.js-toggle-repo').click();
-
- Vue.nextTick(() => {
- expect(vm.$el.querySelector('.container-image-tags')).not.toBeNull();
- expect(vm.iconName).toEqual('angle-up');
-
- done();
- });
- });
-
- it('should be closed when the user clicks on an opened repo', done => {
- vm.$el.querySelector('.js-toggle-repo').click();
-
- Vue.nextTick(() => {
- vm.$el.querySelector('.js-toggle-repo').click();
- setTimeout(() => {
- Vue.nextTick(() => {
- expect(vm.$el.querySelector('.container-image-tags')).toBe(null);
- expect(vm.iconName).toEqual('angle-right');
- done();
- });
- });
- });
- });
- });
-
- describe('delete repo', () => {
- it('should be possible to delete a repo', () => {
- expect(findDeleteBtn()).not.toBeNull();
- });
-
- it('should call deleteItem when confirming deletion', done => {
- findDeleteBtn().click();
- spyOn(vm, 'deleteItem').and.returnValue(Promise.resolve());
-
- Vue.nextTick(() => {
- document.querySelector(`#${vm.modalId} .btn-danger`).click();
-
- expect(vm.deleteItem).toHaveBeenCalledWith(vm.repo);
- done();
- });
- });
- });
-});
diff --git a/spec/javascripts/registry/components/table_registry_spec.js b/spec/javascripts/registry/components/table_registry_spec.js
deleted file mode 100644
index 9c7439206ef..00000000000
--- a/spec/javascripts/registry/components/table_registry_spec.js
+++ /dev/null
@@ -1,189 +0,0 @@
-import Vue from 'vue';
-import tableRegistry from '~/registry/components/table_registry.vue';
-import store from '~/registry/stores';
-import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
-import { repoPropsData } from '../mock_data';
-
-const [firstImage, secondImage] = repoPropsData.list;
-
-describe('table registry', () => {
- let vm;
- const Component = Vue.extend(tableRegistry);
- const bulkDeletePath = 'path';
-
- const findDeleteBtn = () => vm.$el.querySelector('.js-delete-registry');
- const findDeleteBtnRow = () => vm.$el.querySelector('.js-delete-registry-row');
- const findSelectAllCheckbox = () => vm.$el.querySelector('.js-select-all-checkbox > input');
- const findAllRowCheckboxes = () =>
- Array.from(vm.$el.querySelectorAll('.js-select-checkbox input'));
- const confirmationModal = (child = '') => document.querySelector(`#${vm.modalId} ${child}`);
-
- const createComponent = () => {
- vm = mountComponentWithStore(Component, {
- store,
- props: {
- repo: repoPropsData,
- },
- });
- };
-
- const selectAllCheckboxes = () => vm.selectAll();
- const deselectAllCheckboxes = () => vm.deselectAll();
-
- beforeEach(() => {
- createComponent();
- });
-
- afterEach(() => {
- vm.$destroy();
- });
-
- describe('rendering', () => {
- it('should render a table with the registry list', () => {
- expect(vm.$el.querySelectorAll('table tbody tr').length).toEqual(repoPropsData.list.length);
- });
-
- it('should render registry tag', () => {
- const textRendered = vm.$el
- .querySelector('.table tbody tr')
- .textContent.trim()
- // replace additional whitespace characters (e.g. new lines) with a single empty space
- .replace(/\s\s+/g, ' ');
-
- expect(textRendered).toContain(repoPropsData.list[0].tag);
- expect(textRendered).toContain(repoPropsData.list[0].shortRevision);
- expect(textRendered).toContain(repoPropsData.list[0].layers);
- expect(textRendered).toContain(repoPropsData.list[0].size);
- });
- });
-
- describe('multi select', () => {
- it('should support multiselect and selecting a row should enable delete button', done => {
- findSelectAllCheckbox().click();
- selectAllCheckboxes();
-
- expect(findSelectAllCheckbox().checked).toBe(true);
-
- Vue.nextTick(() => {
- expect(findDeleteBtn().disabled).toBe(false);
- done();
- });
- });
-
- it('selecting all checkbox should select all rows and enable delete button', done => {
- selectAllCheckboxes();
-
- Vue.nextTick(() => {
- const checkedValues = findAllRowCheckboxes().filter(x => x.checked);
-
- expect(checkedValues.length).toBe(repoPropsData.list.length);
- done();
- });
- });
-
- it('deselecting select all checkbox should deselect all rows and disable delete button', done => {
- selectAllCheckboxes();
- deselectAllCheckboxes();
-
- Vue.nextTick(() => {
- const checkedValues = findAllRowCheckboxes().filter(x => x.checked);
-
- expect(checkedValues.length).toBe(0);
- done();
- });
- });
-
- it('should delete multiple items when multiple items are selected', done => {
- selectAllCheckboxes();
-
- Vue.nextTick(() => {
- expect(vm.itemsToBeDeleted).toEqual([0, 1]);
- expect(findDeleteBtn().disabled).toBe(false);
-
- findDeleteBtn().click();
- spyOn(vm, 'multiDeleteItems').and.returnValue(Promise.resolve());
-
- Vue.nextTick(() => {
- const modal = confirmationModal();
- confirmationModal('.btn-danger').click();
-
- expect(modal).toExist();
-
- Vue.nextTick(() => {
- expect(vm.itemsToBeDeleted).toEqual([]);
- expect(vm.multiDeleteItems).toHaveBeenCalledWith({
- path: bulkDeletePath,
- items: [firstImage.tag, secondImage.tag],
- });
- done();
- });
- });
- });
- });
- });
-
- describe('delete registry', () => {
- beforeEach(() => {
- vm.itemsToBeDeleted = [0];
- });
-
- it('should be possible to delete a registry', done => {
- Vue.nextTick(() => {
- expect(vm.itemsToBeDeleted).toEqual([0]);
- expect(findDeleteBtn()).toBeDefined();
- expect(findDeleteBtn().disabled).toBe(false);
- expect(findDeleteBtnRow()).toBeDefined();
- done();
- });
- });
-
- it('should call deleteItems and reset itemsToBeDeleted when confirming deletion', done => {
- Vue.nextTick(() => {
- expect(vm.itemsToBeDeleted).toEqual([0]);
- expect(findDeleteBtn().disabled).toBe(false);
- findDeleteBtn().click();
- spyOn(vm, 'multiDeleteItems').and.returnValue(Promise.resolve());
-
- Vue.nextTick(() => {
- confirmationModal('.btn-danger').click();
-
- expect(vm.itemsToBeDeleted).toEqual([]);
- expect(vm.multiDeleteItems).toHaveBeenCalledWith({
- path: bulkDeletePath,
- items: [firstImage.tag],
- });
- done();
- });
- });
- });
- });
-
- describe('pagination', () => {
- it('should be possible to change the page', () => {
- expect(vm.$el.querySelector('.gl-pagination')).toBeDefined();
- });
- });
-
- describe('modal content', () => {
- it('should show the singular title and image name when deleting a single image', done => {
- findDeleteBtnRow().click();
-
- Vue.nextTick(() => {
- expect(vm.modalTitle).toBe('Remove image');
- expect(vm.modalDescription).toContain(firstImage.tag);
- done();
- });
- });
-
- it('should show the plural title and image count when deleting more than one image', done => {
- selectAllCheckboxes();
- vm.setModalDescription();
-
- Vue.nextTick(() => {
- expect(vm.modalTitle).toBe('Remove images');
- expect(vm.modalDescription).toContain('<b>2</b> images');
- done();
- });
- });
- });
-});
diff --git a/spec/javascripts/registry/stores/actions_spec.js b/spec/javascripts/registry/stores/actions_spec.js
deleted file mode 100644
index 0613ec8e0f1..00000000000
--- a/spec/javascripts/registry/stores/actions_spec.js
+++ /dev/null
@@ -1,132 +0,0 @@
-import MockAdapter from 'axios-mock-adapter';
-import axios from '~/lib/utils/axios_utils';
-import * as actions from '~/registry/stores/actions';
-import * as types from '~/registry/stores/mutation_types';
-import state from '~/registry/stores/state';
-import { TEST_HOST } from 'spec/test_constants';
-import testAction from '../../helpers/vuex_action_helper';
-import {
- reposServerResponse,
- registryServerResponse,
- parsedReposServerResponse,
-} from '../mock_data';
-
-describe('Actions Registry Store', () => {
- let mockedState;
- let mock;
-
- beforeEach(() => {
- mockedState = state();
- mockedState.endpoint = `${TEST_HOST}/endpoint.json`;
- mock = new MockAdapter(axios);
- });
-
- afterEach(() => {
- mock.restore();
- });
-
- describe('server requests', () => {
- describe('fetchRepos', () => {
- beforeEach(() => {
- mock.onGet(`${TEST_HOST}/endpoint.json`).replyOnce(200, reposServerResponse, {});
- });
-
- it('should set receveived repos', done => {
- testAction(
- actions.fetchRepos,
- null,
- mockedState,
- [
- { type: types.TOGGLE_MAIN_LOADING },
- { type: types.TOGGLE_MAIN_LOADING },
- { type: types.SET_REPOS_LIST, payload: reposServerResponse },
- ],
- [],
- done,
- );
- });
- });
-
- describe('fetchList', () => {
- let repo;
- beforeEach(() => {
- mockedState.repos = parsedReposServerResponse;
- [, repo] = mockedState.repos;
-
- mock.onGet(repo.tagsPath).replyOnce(200, registryServerResponse, {});
- });
-
- it('should set received list', done => {
- testAction(
- actions.fetchList,
- { repo },
- mockedState,
- [
- { type: types.TOGGLE_REGISTRY_LIST_LOADING, payload: repo },
- { type: types.TOGGLE_REGISTRY_LIST_LOADING, payload: repo },
- {
- type: types.SET_REGISTRY_LIST,
- payload: {
- repo,
- resp: registryServerResponse,
- headers: jasmine.anything(),
- },
- },
- ],
- [],
- done,
- );
- });
- });
- });
-
- describe('setMainEndpoint', () => {
- it('should commit set main endpoint', done => {
- testAction(
- actions.setMainEndpoint,
- 'endpoint',
- mockedState,
- [{ type: types.SET_MAIN_ENDPOINT, payload: 'endpoint' }],
- [],
- done,
- );
- });
- });
-
- describe('toggleLoading', () => {
- it('should commit toggle main loading', done => {
- testAction(
- actions.toggleLoading,
- null,
- mockedState,
- [{ type: types.TOGGLE_MAIN_LOADING }],
- [],
- done,
- );
- });
- });
-
- describe('deleteItem', () => {
- it('should perform DELETE request on destroyPath', done => {
- const destroyPath = `${TEST_HOST}/mygroup/myproject/container_registry/1.json`;
- let deleted = false;
- mock.onDelete(destroyPath).replyOnce(() => {
- deleted = true;
- return [200];
- });
- testAction(
- actions.deleteItem,
- {
- destroyPath,
- },
- mockedState,
- )
- .then(() => {
- expect(mock.history.delete.length).toBe(1);
- expect(deleted).toBe(true);
- done();
- })
- .catch(done.fail);
- });
- });
-});
diff --git a/spec/javascripts/releases/components/release_block_spec.js b/spec/javascripts/releases/components/release_block_spec.js
deleted file mode 100644
index 11a385fa64d..00000000000
--- a/spec/javascripts/releases/components/release_block_spec.js
+++ /dev/null
@@ -1,170 +0,0 @@
-import Vue from 'vue';
-import component from '~/releases/components/release_block.vue';
-import timeagoMixin from '~/vue_shared/mixins/timeago';
-
-import mountComponent from '../../helpers/vue_mount_component_helper';
-
-describe('Release block', () => {
- const Component = Vue.extend(component);
-
- const release = {
- name: 'Bionic Beaver',
- tag_name: '18.04',
- description: '## changelog\n\n* line 1\n* line2',
- description_html: '<div><h2>changelog</h2><ul><li>line1</li<li>line 2</li></ul></div>',
- author_name: 'Release bot',
- author_email: 'release-bot@example.com',
- released_at: '2012-05-28T05:00:00-07:00',
- author: {
- avatar_url: 'uploads/-/system/user/avatar/johndoe/avatar.png',
- id: 482476,
- name: 'John Doe',
- path: '/johndoe',
- state: 'active',
- status_tooltip_html: null,
- username: 'johndoe',
- web_url: 'https://gitlab.com/johndoe',
- },
- commit: {
- id: '2695effb5807a22ff3d138d593fd856244e155e7',
- short_id: '2695effb',
- title: 'Initial commit',
- created_at: '2017-07-26T11:08:53.000+02:00',
- parent_ids: ['2a4b78934375d7f53875269ffd4f45fd83a84ebe'],
- message: 'Initial commit',
- author_name: 'John Smith',
- author_email: 'john@example.com',
- authored_date: '2012-05-28T04:42:42-07:00',
- committer_name: 'Jack Smith',
- committer_email: 'jack@example.com',
- committed_date: '2012-05-28T04:42:42-07:00',
- },
- assets: {
- count: 6,
- sources: [
- {
- format: 'zip',
- url:
- 'https://gitlab.com/gitlab-org/gitlab-foss/-/archive/v11.3.12/gitlab-ce-v11.3.12.zip',
- },
- {
- format: 'tar.gz',
- url:
- 'https://gitlab.com/gitlab-org/gitlab-foss/-/archive/v11.3.12/gitlab-ce-v11.3.12.tar.gz',
- },
- {
- format: 'tar.bz2',
- url:
- 'https://gitlab.com/gitlab-org/gitlab-foss/-/archive/v11.3.12/gitlab-ce-v11.3.12.tar.bz2',
- },
- {
- format: 'tar',
- url:
- 'https://gitlab.com/gitlab-org/gitlab-foss/-/archive/v11.3.12/gitlab-ce-v11.3.12.tar',
- },
- ],
- links: [
- {
- name: 'release-18.04.dmg',
- url: 'https://my-external-hosting.example.com/scrambled-url/',
- external: true,
- },
- {
- name: 'binary-linux-amd64',
- url:
- 'https://gitlab.com/gitlab-org/gitlab-foss/-/jobs/artifacts/v11.6.0-rc4/download?job=rspec-mysql+41%2F50',
- external: false,
- },
- ],
- },
- };
- let vm;
-
- const factory = props => mountComponent(Component, { release: props });
-
- beforeEach(() => {
- vm = factory(release);
- });
-
- afterEach(() => {
- vm.$destroy();
- });
-
- it("renders the block with an id equal to the release's tag name", () => {
- expect(vm.$el.id).toBe('18.04');
- });
-
- it('renders release name', () => {
- expect(vm.$el.textContent).toContain(release.name);
- });
-
- it('renders commit sha', () => {
- expect(vm.$el.textContent).toContain(release.commit.short_id);
- });
-
- it('renders tag name', () => {
- expect(vm.$el.textContent).toContain(release.tag_name);
- });
-
- it('renders release date', () => {
- expect(vm.$el.textContent).toContain(timeagoMixin.methods.timeFormated(release.released_at));
- });
-
- it('renders number of assets provided', () => {
- expect(vm.$el.querySelector('.js-assets-count').textContent).toContain(release.assets.count);
- });
-
- it('renders dropdown with the sources', () => {
- expect(vm.$el.querySelectorAll('.js-sources-dropdown li').length).toEqual(
- release.assets.sources.length,
- );
-
- expect(vm.$el.querySelector('.js-sources-dropdown li a').getAttribute('href')).toEqual(
- release.assets.sources[0].url,
- );
-
- expect(vm.$el.querySelector('.js-sources-dropdown li a').textContent).toContain(
- release.assets.sources[0].format,
- );
- });
-
- it('renders list with the links provided', () => {
- expect(vm.$el.querySelectorAll('.js-assets-list li').length).toEqual(
- release.assets.links.length,
- );
-
- expect(vm.$el.querySelector('.js-assets-list li a').getAttribute('href')).toEqual(
- release.assets.links[0].url,
- );
-
- expect(vm.$el.querySelector('.js-assets-list li a').textContent).toContain(
- release.assets.links[0].name,
- );
- });
-
- it('renders author avatar', () => {
- expect(vm.$el.querySelector('.user-avatar-link')).not.toBeNull();
- });
-
- describe('external label', () => {
- it('renders external label when link is external', () => {
- expect(vm.$el.querySelector('.js-assets-list li a').textContent).toContain('external source');
- });
-
- it('does not render external label when link is not external', () => {
- expect(vm.$el.querySelector('.js-assets-list li:nth-child(2) a').textContent).not.toContain(
- 'external source',
- );
- });
- });
-
- describe('with upcoming_release flag', () => {
- beforeEach(() => {
- vm = factory(Object.assign({}, release, { upcoming_release: true }));
- });
-
- it('renders upcoming release badge', () => {
- expect(vm.$el.textContent).toContain('Upcoming Release');
- });
- });
-});
diff --git a/spec/javascripts/releases/components/app_spec.js b/spec/javascripts/releases/list/components/app_spec.js
index f30c7685e34..471c442e497 100644
--- a/spec/javascripts/releases/components/app_spec.js
+++ b/spec/javascripts/releases/list/components/app_spec.js
@@ -1,10 +1,10 @@
import Vue from 'vue';
-import app from '~/releases/components/app.vue';
-import createStore from '~/releases/store';
+import app from '~/releases/list/components/app.vue';
+import createStore from '~/releases/list/store';
import api from '~/api';
import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import { resetStore } from '../store/helpers';
-import { releases } from '../mock_data';
+import { releases } from '../../mock_data';
describe('Releases App ', () => {
const Component = Vue.extend(app);
diff --git a/spec/javascripts/releases/store/actions_spec.js b/spec/javascripts/releases/list/store/actions_spec.js
index 6eb8e681be9..8e78a631a5f 100644
--- a/spec/javascripts/releases/store/actions_spec.js
+++ b/spec/javascripts/releases/list/store/actions_spec.js
@@ -3,12 +3,12 @@ import {
fetchReleases,
receiveReleasesSuccess,
receiveReleasesError,
-} from '~/releases/store/actions';
-import state from '~/releases/store/state';
-import * as types from '~/releases/store/mutation_types';
+} from '~/releases/list/store/actions';
+import state from '~/releases/list/store/state';
+import * as types from '~/releases/list/store/mutation_types';
import api from '~/api';
import testAction from 'spec/helpers/vuex_action_helper';
-import { releases } from '../mock_data';
+import { releases } from '../../mock_data';
describe('Releases State actions', () => {
let mockedState;
diff --git a/spec/javascripts/releases/store/helpers.js b/spec/javascripts/releases/list/store/helpers.js
index e962b254377..fbc89ec2148 100644
--- a/spec/javascripts/releases/store/helpers.js
+++ b/spec/javascripts/releases/list/store/helpers.js
@@ -1,4 +1,4 @@
-import state from '~/releases/store/state';
+import state from '~/releases/list/store/state';
// eslint-disable-next-line import/prefer-default-export
export const resetStore = store => {
diff --git a/spec/javascripts/releases/store/mutations_spec.js b/spec/javascripts/releases/list/store/mutations_spec.js
index 72b98529fe9..d2577891495 100644
--- a/spec/javascripts/releases/store/mutations_spec.js
+++ b/spec/javascripts/releases/list/store/mutations_spec.js
@@ -1,7 +1,7 @@
-import state from '~/releases/store/state';
-import mutations from '~/releases/store/mutations';
-import * as types from '~/releases/store/mutation_types';
-import { releases } from '../mock_data';
+import state from '~/releases/list/store/state';
+import mutations from '~/releases/list/store/mutations';
+import * as types from '~/releases/list/store/mutation_types';
+import { releases } from '../../mock_data';
describe('Releases Store Mutations', () => {
let stateCopy;
diff --git a/spec/javascripts/reports/components/grouped_test_reports_app_spec.js b/spec/javascripts/reports/components/grouped_test_reports_app_spec.js
index 1f1e626ed33..1b006cdbd4e 100644
--- a/spec/javascripts/reports/components/grouped_test_reports_app_spec.js
+++ b/spec/javascripts/reports/components/grouped_test_reports_app_spec.js
@@ -83,11 +83,11 @@ describe('Grouped Test Reports App', () => {
setTimeout(() => {
expect(vm.$el.querySelector('.gl-spinner')).toBeNull();
expect(vm.$el.querySelector('.js-code-text').textContent.trim()).toEqual(
- 'Test summary contained 2 failed test results out of 11 total tests',
+ 'Test summary contained 2 failed/error test results out of 11 total tests',
);
expect(vm.$el.textContent).toContain(
- 'rspec:pg found 2 failed test results out of 8 total tests',
+ 'rspec:pg found 2 failed/error test results out of 8 total tests',
);
expect(vm.$el.textContent).toContain('New');
@@ -111,16 +111,16 @@ describe('Grouped Test Reports App', () => {
setTimeout(() => {
expect(vm.$el.querySelector('.gl-spinner')).toBeNull();
expect(vm.$el.querySelector('.js-code-text').textContent.trim()).toEqual(
- 'Test summary contained 2 failed test results and 2 fixed test results out of 11 total tests',
+ 'Test summary contained 2 failed/error test results and 2 fixed test results out of 11 total tests',
);
expect(vm.$el.textContent).toContain(
- 'rspec:pg found 1 failed test result and 2 fixed test results out of 8 total tests',
+ 'rspec:pg found 1 failed/error test result and 2 fixed test results out of 8 total tests',
);
expect(vm.$el.textContent).toContain('New');
expect(vm.$el.textContent).toContain(
- ' java ant found 1 failed test result out of 3 total tests',
+ ' java ant found 1 failed/error test result out of 3 total tests',
);
done();
}, 0);
diff --git a/spec/javascripts/sidebar/assignee_title_spec.js b/spec/javascripts/sidebar/assignee_title_spec.js
index 7fff7c075d9..6c65a55ff29 100644
--- a/spec/javascripts/sidebar/assignee_title_spec.js
+++ b/spec/javascripts/sidebar/assignee_title_spec.js
@@ -1,13 +1,12 @@
import Vue from 'vue';
import AssigneeTitle from '~/sidebar/components/assignees/assignee_title.vue';
+import { mockTracking, triggerEvent } from 'spec/helpers/tracking_helper';
describe('AssigneeTitle component', () => {
let component;
let AssigneeTitleComponent;
- let statsSpy;
beforeEach(() => {
- statsSpy = spyOnDependency(AssigneeTitle, 'trackEvent');
AssigneeTitleComponent = Vue.extend(AssigneeTitle);
});
@@ -105,15 +104,20 @@ describe('AssigneeTitle component', () => {
expect(component.$el.querySelector('.edit-link')).not.toBeNull();
});
- it('calls trackEvent when edit is clicked', () => {
+ it('tracks the event when edit is clicked', () => {
component = new AssigneeTitleComponent({
propsData: {
numberOfAssignees: 0,
editable: true,
},
}).$mount();
- component.$el.querySelector('.js-sidebar-dropdown-toggle').click();
- expect(statsSpy).toHaveBeenCalled();
+ const spy = mockTracking('_category_', component.$el, spyOn);
+ triggerEvent('.js-sidebar-dropdown-toggle');
+
+ expect(spy).toHaveBeenCalledWith('_category_', 'click_edit_button', {
+ label: 'right_sidebar',
+ property: 'assignee',
+ });
});
});
diff --git a/spec/javascripts/sidebar/components/time_tracking/time_tracker_spec.js b/spec/javascripts/sidebar/components/time_tracking/time_tracker_spec.js
index 2e1863cff86..ab28190ae64 100644
--- a/spec/javascripts/sidebar/components/time_tracking/time_tracker_spec.js
+++ b/spec/javascripts/sidebar/components/time_tracking/time_tracker_spec.js
@@ -83,8 +83,8 @@ describe('Issuable Time Tracker', () => {
initTimeTrackingComponent({
timeEstimate: 100000, // 1d 3h
timeSpent: 5000, // 1h 23m
- timeEstimateHumanReadable: '',
- timeSpentHumanReadable: '',
+ timeEstimateHumanReadable: '1d 3h',
+ timeSpentHumanReadable: '1h 23m',
});
});
@@ -98,6 +98,16 @@ describe('Issuable Time Tracker', () => {
});
});
+ it('should show full times when the sidebar is collapsed', done => {
+ Vue.nextTick(() => {
+ const timeTrackingText = vm.$el.querySelector('.time-tracking-collapsed-summary span')
+ .innerText;
+
+ expect(timeTrackingText).toBe('1h 23m / 1d 3h');
+ done();
+ });
+ });
+
describe('Remaining meter', () => {
it('should display the remaining meter with the correct width', done => {
Vue.nextTick(() => {
diff --git a/spec/javascripts/sidebar/confidential_issue_sidebar_spec.js b/spec/javascripts/sidebar/confidential_issue_sidebar_spec.js
deleted file mode 100644
index ea9e5677bc5..00000000000
--- a/spec/javascripts/sidebar/confidential_issue_sidebar_spec.js
+++ /dev/null
@@ -1,78 +0,0 @@
-import Vue from 'vue';
-import confidentialIssueSidebar from '~/sidebar/components/confidential/confidential_issue_sidebar.vue';
-
-describe('Confidential Issue Sidebar Block', () => {
- let vm1;
- let vm2;
- let statsSpy;
-
- beforeEach(() => {
- statsSpy = spyOnDependency(confidentialIssueSidebar, 'trackEvent');
- const Component = Vue.extend(confidentialIssueSidebar);
- const service = {
- update: () => Promise.resolve(true),
- };
-
- vm1 = new Component({
- propsData: {
- isConfidential: true,
- isEditable: true,
- service,
- },
- }).$mount();
-
- vm2 = new Component({
- propsData: {
- isConfidential: false,
- isEditable: false,
- service,
- },
- }).$mount();
- });
-
- it('shows if confidential and/or editable', () => {
- expect(vm1.$el.innerHTML.includes('Edit')).toBe(true);
-
- expect(vm1.$el.innerHTML.includes('This issue is confidential')).toBe(true);
-
- expect(vm2.$el.innerHTML.includes('Not confidential')).toBe(true);
- });
-
- it('displays the edit form when editable', done => {
- expect(vm1.edit).toBe(false);
-
- vm1.$el.querySelector('.confidential-edit').click();
-
- expect(vm1.edit).toBe(true);
-
- setTimeout(() => {
- expect(vm1.$el.innerHTML.includes('You are going to turn off the confidentiality.')).toBe(
- true,
- );
-
- done();
- });
- });
-
- it('displays the edit form when opened from collapsed state', done => {
- expect(vm1.edit).toBe(false);
-
- vm1.$el.querySelector('.sidebar-collapsed-icon').click();
-
- expect(vm1.edit).toBe(true);
-
- setTimeout(() => {
- expect(vm1.$el.innerHTML.includes('You are going to turn off the confidentiality.')).toBe(
- true,
- );
-
- done();
- });
- });
-
- it('calls trackEvent when "Edit" is clicked', () => {
- vm1.$el.querySelector('.confidential-edit').click();
-
- expect(statsSpy).toHaveBeenCalled();
- });
-});
diff --git a/spec/javascripts/sidebar/lock/lock_issue_sidebar_spec.js b/spec/javascripts/sidebar/lock/lock_issue_sidebar_spec.js
index 2d930428230..decccbb8964 100644
--- a/spec/javascripts/sidebar/lock/lock_issue_sidebar_spec.js
+++ b/spec/javascripts/sidebar/lock/lock_issue_sidebar_spec.js
@@ -1,13 +1,12 @@
import Vue from 'vue';
import lockIssueSidebar from '~/sidebar/components/lock/lock_issue_sidebar.vue';
+import { mockTracking, triggerEvent } from 'spec/helpers/tracking_helper';
describe('LockIssueSidebar', () => {
let vm1;
let vm2;
- let statsSpy;
beforeEach(() => {
- statsSpy = spyOnDependency(lockIssueSidebar, 'trackEvent');
const Component = Vue.extend(lockIssueSidebar);
const mediator = {
@@ -61,10 +60,14 @@ describe('LockIssueSidebar', () => {
});
});
- it('calls trackEvent when "Edit" is clicked', () => {
- vm1.$el.querySelector('.lock-edit').click();
+ it('tracks an event when "Edit" is clicked', () => {
+ const spy = mockTracking('_category_', vm1.$el, spyOn);
+ triggerEvent('.lock-edit');
- expect(statsSpy).toHaveBeenCalled();
+ expect(spy).toHaveBeenCalledWith('_category_', 'click_edit_button', {
+ label: 'right_sidebar',
+ property: 'lock_issue',
+ });
});
it('displays the edit form when opened from collapsed state', done => {
diff --git a/spec/javascripts/sidebar/subscriptions_spec.js b/spec/javascripts/sidebar/subscriptions_spec.js
index 2efa13f3fe8..a97608d6b8a 100644
--- a/spec/javascripts/sidebar/subscriptions_spec.js
+++ b/spec/javascripts/sidebar/subscriptions_spec.js
@@ -2,14 +2,13 @@ import Vue from 'vue';
import subscriptions from '~/sidebar/components/subscriptions/subscriptions.vue';
import eventHub from '~/sidebar/event_hub';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
+import { mockTracking } from 'spec/helpers/tracking_helper';
describe('Subscriptions', function() {
let vm;
let Subscriptions;
- let statsSpy;
beforeEach(() => {
- statsSpy = spyOnDependency(subscriptions, 'trackEvent');
Subscriptions = Vue.extend(subscriptions);
});
@@ -53,6 +52,7 @@ describe('Subscriptions', function() {
vm = mountComponent(Subscriptions, { subscribed: true });
spyOn(eventHub, '$emit');
spyOn(vm, '$emit');
+ spyOn(vm, 'track');
vm.toggleSubscription();
@@ -60,11 +60,12 @@ describe('Subscriptions', function() {
expect(vm.$emit).toHaveBeenCalledWith('toggleSubscription', jasmine.any(Object));
});
- it('calls trackEvent when toggled', () => {
+ it('tracks the event when toggled', () => {
vm = mountComponent(Subscriptions, { subscribed: true });
+ const spy = mockTracking('_category_', vm.$el, spyOn);
vm.toggleSubscription();
- expect(statsSpy).toHaveBeenCalled();
+ expect(spy).toHaveBeenCalled();
});
it('onClickCollapsedIcon method emits `toggleSidebar` event on component', () => {
diff --git a/spec/javascripts/sidebar/todo_spec.js b/spec/javascripts/sidebar/todo_spec.js
deleted file mode 100644
index e7abd19c865..00000000000
--- a/spec/javascripts/sidebar/todo_spec.js
+++ /dev/null
@@ -1,171 +0,0 @@
-import Vue from 'vue';
-
-import SidebarTodos from '~/sidebar/components/todo_toggle/todo.vue';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
-
-const createComponent = ({
- issuableId = 1,
- issuableType = 'epic',
- isTodo,
- isActionActive,
- collapsed,
-}) => {
- const Component = Vue.extend(SidebarTodos);
-
- return mountComponent(Component, {
- issuableId,
- issuableType,
- isTodo,
- isActionActive,
- collapsed,
- });
-};
-
-describe('SidebarTodo', () => {
- let vm;
-
- beforeEach(() => {
- vm = createComponent({});
- });
-
- afterEach(() => {
- vm.$destroy();
- });
-
- describe('computed', () => {
- describe('buttonClasses', () => {
- it('returns todo button classes for when `collapsed` prop is `false`', () => {
- expect(vm.buttonClasses).toBe('btn btn-default btn-todo issuable-header-btn float-right');
- });
-
- it('returns todo button classes for when `collapsed` prop is `true`', done => {
- vm.collapsed = true;
- Vue.nextTick()
- .then(() => {
- expect(vm.buttonClasses).toBe(
- 'btn-blank btn-todo sidebar-collapsed-icon dont-change-state',
- );
- })
- .then(done)
- .catch(done.fail);
- });
- });
-
- describe('buttonLabel', () => {
- it('returns todo button text for marking todo as done when `isTodo` prop is `true`', () => {
- expect(vm.buttonLabel).toBe('Mark as done');
- });
-
- it('returns todo button text for add todo when `isTodo` prop is `false`', done => {
- vm.isTodo = false;
- Vue.nextTick()
- .then(() => {
- expect(vm.buttonLabel).toBe('Add a To Do');
- })
- .then(done)
- .catch(done.fail);
- });
- });
-
- describe('collapsedButtonIconClasses', () => {
- it('returns collapsed button icon class when `isTodo` prop is `true`', () => {
- expect(vm.collapsedButtonIconClasses).toBe('todo-undone');
- });
-
- it('returns empty string when `isTodo` prop is `false`', done => {
- vm.isTodo = false;
- Vue.nextTick()
- .then(() => {
- expect(vm.collapsedButtonIconClasses).toBe('');
- })
- .then(done)
- .catch(done.fail);
- });
- });
-
- describe('collapsedButtonIcon', () => {
- it('returns button icon name when `isTodo` prop is `true`', () => {
- expect(vm.collapsedButtonIcon).toBe('todo-done');
- });
-
- it('returns button icon name when `isTodo` prop is `false`', done => {
- vm.isTodo = false;
- Vue.nextTick()
- .then(() => {
- expect(vm.collapsedButtonIcon).toBe('todo-add');
- })
- .then(done)
- .catch(done.fail);
- });
- });
- });
-
- describe('methods', () => {
- describe('handleButtonClick', () => {
- it('emits `toggleTodo` event on component', () => {
- spyOn(vm, '$emit');
- vm.handleButtonClick();
-
- expect(vm.$emit).toHaveBeenCalledWith('toggleTodo');
- });
- });
- });
-
- describe('template', () => {
- it('renders component container element', () => {
- const dataAttributes = {
- issuableId: '1',
- issuableType: 'epic',
- originalTitle: '',
- placement: 'left',
- container: 'body',
- boundary: 'viewport',
- };
-
- expect(vm.$el.nodeName).toBe('BUTTON');
-
- const elDataAttrs = vm.$el.dataset;
- Object.keys(elDataAttrs).forEach(attr => {
- expect(elDataAttrs[attr]).toBe(dataAttributes[attr]);
- });
- });
-
- it('check button label computed property', () => {
- expect(vm.buttonLabel).toEqual('Mark as done');
- });
-
- it('renders button label element when `collapsed` prop is `false`', () => {
- const buttonLabelEl = vm.$el.querySelector('span.issuable-todo-inner');
-
- expect(buttonLabelEl).not.toBeNull();
- expect(buttonLabelEl.innerText.trim()).toBe('Mark as done');
- });
-
- it('renders button icon when `collapsed` prop is `true`', done => {
- vm.collapsed = true;
- Vue.nextTick()
- .then(() => {
- const buttonIconEl = vm.$el.querySelector('svg');
-
- expect(buttonIconEl).not.toBeNull();
- expect(buttonIconEl.querySelector('use').getAttribute('xlink:href')).toContain(
- 'todo-done',
- );
- })
- .then(done)
- .catch(done.fail);
- });
-
- it('renders loading icon when `isActionActive` prop is true', done => {
- vm.isActionActive = true;
- Vue.nextTick()
- .then(() => {
- const loadingEl = vm.$el.querySelector('span.loading-container');
-
- expect(loadingEl).not.toBeNull();
- })
- .then(done)
- .catch(done.fail);
- });
- });
-});
diff --git a/spec/javascripts/test_bundle.js b/spec/javascripts/test_bundle.js
index c0a999cfaa6..cb6b158f01c 100644
--- a/spec/javascripts/test_bundle.js
+++ b/spec/javascripts/test_bundle.js
@@ -7,7 +7,6 @@ import 'core-js/features/set-immediate';
import 'vendor/jasmine-jquery';
import '~/commons';
import Vue from 'vue';
-import VueResource from 'vue-resource';
import Translate from '~/vue_shared/translate';
import jasmineDiff from 'jasmine-diff';
import { config as testUtilsConfig } from '@vue/test-utils';
@@ -46,7 +45,6 @@ Vue.config.errorHandler = function(err) {
fail(err);
};
-Vue.use(VueResource);
Vue.use(Translate);
// enable test fixtures
@@ -72,7 +70,7 @@ window.gl = window.gl || {};
window.gl.TEST_HOST = TEST_HOST;
window.gon = window.gon || {};
window.gon.test_env = true;
-window.gon.ee = process.env.IS_GITLAB_EE;
+window.gon.ee = process.env.IS_EE;
gon.relative_url_root = '';
let hasUnhandledPromiseRejections = false;
@@ -102,13 +100,6 @@ afterEach(__rewire_reset_all__); // eslint-disable-line
// to run our unit tests.
beforeEach(done => done());
-const builtinVueHttpInterceptors = Vue.http.interceptors.slice();
-
-beforeEach(() => {
- // restore interceptors so we have no remaining ones from previous tests
- Vue.http.interceptors = builtinVueHttpInterceptors.slice();
-});
-
let longRunningTestTimeoutHandle;
beforeEach(done => {
@@ -127,7 +118,7 @@ const axiosDefaultAdapter = getDefaultAdapter();
// render all of our tests
const testContexts = [require.context('spec', true, /_spec$/)];
-if (process.env.IS_GITLAB_EE) {
+if (process.env.IS_EE) {
testContexts.push(require.context('ee_spec', true, /_spec$/));
}
@@ -216,7 +207,7 @@ if (process.env.BABEL_ENV === 'coverage') {
describe('Uncovered files', function() {
const sourceFilesContexts = [require.context('~', true, /\.(js|vue)$/)];
- if (process.env.IS_GITLAB_EE) {
+ if (process.env.IS_EE) {
sourceFilesContexts.push(require.context('ee', true, /\.(js|vue)$/));
}
diff --git a/spec/javascripts/test_constants.js b/spec/javascripts/test_constants.js
index c97d47a6406..51c0716b99d 100644
--- a/spec/javascripts/test_constants.js
+++ b/spec/javascripts/test_constants.js
@@ -1,7 +1 @@
-export const FIXTURES_PATH = `/fixtures`;
-export const TEST_HOST = 'http://test.host';
-
-export const DUMMY_IMAGE_URL = `${FIXTURES_PATH}/static/images/one_white_pixel.png`;
-
-export const GREEN_BOX_IMAGE_URL = `${FIXTURES_PATH}/static/images/green_box.png`;
-export const RED_BOX_IMAGE_URL = `${FIXTURES_PATH}/static/images/red_box.png`;
+export * from '../frontend/helpers/test_constants';
diff --git a/spec/javascripts/todos_spec.js b/spec/javascripts/todos_spec.js
index 802f54f6a7e..dc3c547c632 100644
--- a/spec/javascripts/todos_spec.js
+++ b/spec/javascripts/todos_spec.js
@@ -1,18 +1,31 @@
import $ from 'jquery';
+import MockAdapter from 'axios-mock-adapter';
import Todos from '~/pages/dashboard/todos/index/todos';
import '~/lib/utils/common_utils';
+import '~/gl_dropdown';
+import axios from '~/lib/utils/axios_utils';
+import { addDelimiter } from '~/lib/utils/text_utility';
+
+const TEST_COUNT_BIG = 2000;
+const TEST_DONE_COUNT_BIG = 7300;
describe('Todos', () => {
preloadFixtures('todos/todos.html');
let todoItem;
+ let mock;
beforeEach(() => {
loadFixtures('todos/todos.html');
todoItem = document.querySelector('.todos-list .todo');
+ mock = new MockAdapter(axios);
return new Todos();
});
+ afterEach(() => {
+ mock.restore();
+ });
+
describe('goToTodoUrl', () => {
it('opens the todo url', done => {
const todoLink = todoItem.dataset.url;
@@ -53,5 +66,43 @@ describe('Todos', () => {
expect(windowOpenSpy).not.toHaveBeenCalled();
});
});
+
+ describe('on done todo click', () => {
+ let onToggleSpy;
+
+ beforeEach(done => {
+ const el = document.querySelector('.js-done-todo');
+ const path = el.dataset.href;
+
+ // Arrange
+ mock
+ .onDelete(path)
+ .replyOnce(200, { count: TEST_COUNT_BIG, done_count: TEST_DONE_COUNT_BIG });
+ onToggleSpy = jasmine.createSpy('onToggle');
+ $(document).on('todo:toggle', onToggleSpy);
+
+ // Act
+ el.click();
+
+ // Wait for axios and HTML to udpate
+ setImmediate(done);
+ });
+
+ it('dispatches todo:toggle', () => {
+ expect(onToggleSpy).toHaveBeenCalledWith(jasmine.anything(), TEST_COUNT_BIG);
+ });
+
+ it('updates pending text', () => {
+ expect(document.querySelector('.todos-pending .badge').innerHTML).toEqual(
+ addDelimiter(TEST_COUNT_BIG),
+ );
+ });
+
+ it('updates done text', () => {
+ expect(document.querySelector('.todos-done .badge').innerHTML).toEqual(
+ addDelimiter(TEST_DONE_COUNT_BIG),
+ );
+ });
+ });
});
});
diff --git a/spec/javascripts/vue_mr_widget/components/deployment_spec.js b/spec/javascripts/vue_mr_widget/components/deployment_spec.js
index 42bf3b7df09..1949bee1406 100644
--- a/spec/javascripts/vue_mr_widget/components/deployment_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/deployment_spec.js
@@ -207,7 +207,7 @@ describe('Deployment component', () => {
it('renders the link to the review app without dropdown', () => {
expect(vm.$el.querySelector('.js-mr-wigdet-deployment-dropdown')).toBeNull();
- expect(vm.$el.querySelector('.js-deploy-url-feature-flag')).not.toBeNull();
+ expect(vm.$el.querySelector('.js-deploy-url')).not.toBeNull();
});
});
@@ -223,12 +223,12 @@ describe('Deployment component', () => {
it('renders the link to the review app without dropdown', () => {
expect(vm.$el.querySelector('.js-mr-wigdet-deployment-dropdown')).toBeNull();
- expect(vm.$el.querySelector('.js-deploy-url-feature-flag')).not.toBeNull();
+ expect(vm.$el.querySelector('.js-deploy-url')).not.toBeNull();
});
it('renders the link to the review app linked to to the first change', () => {
const expectedUrl = deploymentMockData.changes[0].external_url;
- const deployUrl = vm.$el.querySelector('.js-deploy-url-feature-flag');
+ const deployUrl = vm.$el.querySelector('.js-deploy-url');
expect(vm.$el.querySelector('.js-mr-wigdet-deployment-dropdown')).toBeNull();
expect(deployUrl).not.toBeNull();
diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_container_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_container_spec.js
index dfbc68c48b9..6cdf60f3535 100644
--- a/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_container_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_container_spec.js
@@ -1,6 +1,7 @@
import { mount, createLocalVue } from '@vue/test-utils';
import MrWidgetPipelineContainer from '~/vue_merge_request_widget/components/mr_widget_pipeline_container.vue';
import MrWidgetPipeline from '~/vue_merge_request_widget/components/mr_widget_pipeline.vue';
+import ArtifactsApp from '~/vue_merge_request_widget/components/artifacts_list_app.vue';
import { mockStore } from '../mock_data';
describe('MrWidgetPipelineContainer', () => {
@@ -87,4 +88,10 @@ describe('MrWidgetPipelineContainer', () => {
expect(deployments.wrappers.map(x => x.props())).toEqual(expectedProps);
});
});
+
+ describe('with artifacts path', () => {
+ it('renders the artifacts app', () => {
+ expect(wrapper.find(ArtifactsApp).isVisible()).toBe(true);
+ });
+ });
});
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
index 7b1d589dcf8..5844dad42ff 100644
--- 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
@@ -47,7 +47,7 @@ describe('Wip', () => {
it('should make a request to service and handle response', done => {
const vm = createComponent();
- spyOn(window, 'Flash').and.returnValue(true);
+ const flashSpy = spyOnDependency(WorkInProgress, 'createFlash').and.returnValue(true);
spyOn(eventHub, '$emit');
spyOn(vm.service, 'removeWIP').and.returnValue(
new Promise(resolve => {
@@ -61,10 +61,7 @@ describe('Wip', () => {
setTimeout(() => {
expect(vm.isMakingRequest).toBeTruthy();
expect(eventHub.$emit).toHaveBeenCalledWith('UpdateWidgetData', mrObj);
- expect(window.Flash).toHaveBeenCalledWith(
- 'The merge request can now be merged.',
- 'notice',
- );
+ expect(flashSpy).toHaveBeenCalledWith('The merge request can now be merged.', 'notice');
done();
}, 333);
});
diff --git a/spec/javascripts/vue_mr_widget/mock_data.js b/spec/javascripts/vue_mr_widget/mock_data.js
index 2f79806652b..089ec08fbf9 100644
--- a/spec/javascripts/vue_mr_widget/mock_data.js
+++ b/spec/javascripts/vue_mr_widget/mock_data.js
@@ -289,4 +289,5 @@ export const mockStore = {
troubleshootingDocsPath: 'troubleshooting-docs-path',
ciStatus: 'ci-status',
hasCI: true,
+ exposedArtifactsPath: 'exposed_artifacts.json',
};
diff --git a/spec/javascripts/vue_mr_widget/stores/artifacts_list/actions_spec.js b/spec/javascripts/vue_mr_widget/stores/artifacts_list/actions_spec.js
new file mode 100644
index 00000000000..4c4bebcb4cd
--- /dev/null
+++ b/spec/javascripts/vue_mr_widget/stores/artifacts_list/actions_spec.js
@@ -0,0 +1,165 @@
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
+import {
+ setEndpoint,
+ requestArtifacts,
+ clearEtagPoll,
+ stopPolling,
+ fetchArtifacts,
+ receiveArtifactsSuccess,
+ receiveArtifactsError,
+} from '~/vue_merge_request_widget/stores/artifacts_list/actions';
+import state from '~/vue_merge_request_widget/stores/artifacts_list/state';
+import * as types from '~/vue_merge_request_widget/stores/artifacts_list/mutation_types';
+import testAction from 'spec/helpers/vuex_action_helper';
+import { TEST_HOST } from 'spec/test_constants';
+
+describe('Artifacts App Store Actions', () => {
+ let mockedState;
+
+ beforeEach(() => {
+ mockedState = state();
+ });
+
+ describe('setEndpoint', () => {
+ it('should commit SET_ENDPOINT mutation', done => {
+ testAction(
+ setEndpoint,
+ 'endpoint.json',
+ mockedState,
+ [{ type: types.SET_ENDPOINT, payload: 'endpoint.json' }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('requestArtifacts', () => {
+ it('should commit REQUEST_ARTIFACTS mutation', done => {
+ testAction(
+ requestArtifacts,
+ null,
+ mockedState,
+ [{ type: types.REQUEST_ARTIFACTS }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('fetchArtifacts', () => {
+ let mock;
+
+ beforeEach(() => {
+ mockedState.endpoint = `${TEST_HOST}/endpoint.json`;
+ mock = new MockAdapter(axios);
+ });
+
+ afterEach(() => {
+ mock.restore();
+ stopPolling();
+ clearEtagPoll();
+ });
+
+ describe('success', () => {
+ it('dispatches requestArtifacts and receiveArtifactsSuccess ', done => {
+ mock.onGet(`${TEST_HOST}/endpoint.json`).replyOnce(200, [
+ {
+ text: 'result.txt',
+ url: 'asda',
+ job_name: 'generate-artifact',
+ job_path: 'asda',
+ },
+ ]);
+
+ testAction(
+ fetchArtifacts,
+ null,
+ mockedState,
+ [],
+ [
+ {
+ type: 'requestArtifacts',
+ },
+ {
+ payload: {
+ data: [
+ {
+ text: 'result.txt',
+ url: 'asda',
+ job_name: 'generate-artifact',
+ job_path: 'asda',
+ },
+ ],
+ status: 200,
+ },
+ type: 'receiveArtifactsSuccess',
+ },
+ ],
+ done,
+ );
+ });
+ });
+
+ describe('error', () => {
+ beforeEach(() => {
+ mock.onGet(`${TEST_HOST}/endpoint.json`).reply(500);
+ });
+
+ it('dispatches requestArtifacts and receiveArtifactsError ', done => {
+ testAction(
+ fetchArtifacts,
+ null,
+ mockedState,
+ [],
+ [
+ {
+ type: 'requestArtifacts',
+ },
+ {
+ type: 'receiveArtifactsError',
+ },
+ ],
+ done,
+ );
+ });
+ });
+ });
+
+ describe('receiveArtifactsSuccess', () => {
+ it('should commit RECEIVE_ARTIFACTS_SUCCESS mutation with 200', done => {
+ testAction(
+ receiveArtifactsSuccess,
+ { data: { summary: {} }, status: 200 },
+ mockedState,
+ [{ type: types.RECEIVE_ARTIFACTS_SUCCESS, payload: { summary: {} } }],
+ [],
+ done,
+ );
+ });
+
+ it('should not commit RECEIVE_ARTIFACTS_SUCCESS mutation with 204', done => {
+ testAction(
+ receiveArtifactsSuccess,
+ { data: { summary: {} }, status: 204 },
+ mockedState,
+ [],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('receiveArtifactsError', () => {
+ it('should commit RECEIVE_ARTIFACTS_ERROR mutation', done => {
+ testAction(
+ receiveArtifactsError,
+ null,
+ mockedState,
+ [{ type: types.RECEIVE_ARTIFACTS_ERROR }],
+ [],
+ done,
+ );
+ });
+ });
+});
diff --git a/spec/javascripts/vue_shared/components/clipboard_button_spec.js b/spec/javascripts/vue_shared/components/clipboard_button_spec.js
index fd17349d48f..29a76574b89 100644
--- a/spec/javascripts/vue_shared/components/clipboard_button_spec.js
+++ b/spec/javascripts/vue_shared/components/clipboard_button_spec.js
@@ -14,7 +14,7 @@ describe('clipboard button', () => {
beforeEach(() => {
vm = mountComponent(Component, {
text: 'copy me',
- title: 'Copy this value into Clipboard!',
+ title: 'Copy this value',
cssClass: 'btn-danger',
});
});
@@ -26,7 +26,7 @@ describe('clipboard button', () => {
});
it('should have a tooltip with default values', () => {
- expect(vm.$el.getAttribute('data-original-title')).toEqual('Copy this value into Clipboard!');
+ expect(vm.$el.getAttribute('data-original-title')).toEqual('Copy this value');
});
it('should render provided classname', () => {
@@ -39,7 +39,7 @@ describe('clipboard button', () => {
vm = mountComponent(Component, {
text: 'copy me',
gfm: '`path/to/file`',
- title: 'Copy this value into Clipboard!',
+ title: 'Copy this value',
cssClass: 'btn-danger',
});
diff --git a/spec/javascripts/vue_shared/components/gl_modal_spec.js b/spec/javascripts/vue_shared/components/deprecated_modal_2_spec.js
index 19af8b5d2f7..64fb984d9fc 100644
--- a/spec/javascripts/vue_shared/components/gl_modal_spec.js
+++ b/spec/javascripts/vue_shared/components/deprecated_modal_2_spec.js
@@ -1,11 +1,11 @@
import $ from 'jquery';
import Vue from 'vue';
-import GlModal from '~/vue_shared/components/gl_modal.vue';
+import DeprecatedModal2 from '~/vue_shared/components/deprecated_modal_2.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
-const modalComponent = Vue.extend(GlModal);
+const modalComponent = Vue.extend(DeprecatedModal2);
-describe('GlModal', () => {
+describe('DeprecatedModal2', () => {
let vm;
afterEach(() => {
@@ -153,17 +153,17 @@ describe('GlModal', () => {
let template;
if (slotName) {
template = `
- <gl-modal>
+ <deprecated-modal-2>
<template slot="${slotName}">${slotContent}</template>
- </gl-modal>
+ </deprecated-modal-2>
`;
} else {
- template = `<gl-modal>${slotContent}</gl-modal>`;
+ template = `<deprecated-modal-2>${slotContent}</deprecated-modal-2>`;
}
return Vue.extend({
components: {
- GlModal,
+ DeprecatedModal2,
},
template,
});
diff --git a/spec/javascripts/vue_shared/components/file_row_spec.js b/spec/javascripts/vue_shared/components/file_row_spec.js
index 6abcac5c0ff..7da69e3fa84 100644
--- a/spec/javascripts/vue_shared/components/file_row_spec.js
+++ b/spec/javascripts/vue_shared/components/file_row_spec.js
@@ -90,19 +90,6 @@ describe('File row component', () => {
expect(vm.$el.querySelector('.js-file-row-header')).not.toBe(null);
});
- it('is not rendered for `moved` entries in subfolders', () => {
- createComponent({
- file: {
- path: 't5',
- moved: true,
- tree: [],
- },
- level: 2,
- });
-
- expect(vm.$el.nodeType).not.toEqual(1);
- });
-
describe('new dropdown', () => {
beforeEach(() => {
createComponent({
diff --git a/spec/javascripts/vue_shared/components/icon_spec.js b/spec/javascripts/vue_shared/components/icon_spec.js
index 45eef2ad737..7390798afa8 100644
--- a/spec/javascripts/vue_shared/components/icon_spec.js
+++ b/spec/javascripts/vue_shared/components/icon_spec.js
@@ -12,8 +12,6 @@ describe('Sprite Icon Component', function() {
icon = mountComponent(IconComponent, {
name: 'commit',
size: 32,
- cssClasses: 'extraclasses',
- tabIndex: '0',
});
});
@@ -47,10 +45,8 @@ describe('Sprite Icon Component', function() {
it('should properly render img css', function() {
const { classList } = icon.$el;
const containsSizeClass = classList.contains('s32');
- const containsCustomClass = classList.contains('extraclasses');
expect(containsSizeClass).toBe(true);
- expect(containsCustomClass).toBe(true);
});
it('`name` validator should return false for non existing icons', () => {
@@ -60,9 +56,5 @@ describe('Sprite Icon Component', function() {
it('`name` validator should return false for existing icons', () => {
expect(Icon.props.name.validator('commit')).toBe(true);
});
-
- it('should contain `tabindex` attribute on svg element when `tabIndex` prop is defined', () => {
- expect(icon.$el.getAttribute('tabindex')).toBe('0');
- });
});
});
diff --git a/spec/javascripts/vue_shared/components/project_selector/project_selector_spec.js b/spec/javascripts/vue_shared/components/project_selector/project_selector_spec.js
index f8271866ca1..9c2deca585b 100644
--- a/spec/javascripts/vue_shared/components/project_selector/project_selector_spec.js
+++ b/spec/javascripts/vue_shared/components/project_selector/project_selector_spec.js
@@ -4,9 +4,11 @@ import ProjectSelector from '~/vue_shared/components/project_selector/project_se
import ProjectListItem from '~/vue_shared/components/project_selector/project_list_item.vue';
import { GlSearchBoxByType } from '@gitlab/ui';
-import { mount } from '@vue/test-utils';
+import { mount, createLocalVue } from '@vue/test-utils';
import { trimText } from 'spec/helpers/text_helper';
+const localVue = createLocalVue();
+
describe('ProjectSelector component', () => {
let wrapper;
let vm;
@@ -22,6 +24,7 @@ describe('ProjectSelector component', () => {
jasmine.clock().install();
wrapper = mount(Vue.extend(ProjectSelector), {
+ localVue,
propsData: {
projectSearchResults: searchResults,
selectedProjects: selected,
diff --git a/spec/javascripts/zen_mode_spec.js b/spec/javascripts/zen_mode_spec.js
index 8f662c71c7a..5dee11b3810 100644
--- a/spec/javascripts/zen_mode_spec.js
+++ b/spec/javascripts/zen_mode_spec.js
@@ -2,6 +2,7 @@ import $ from 'jquery';
import Dropzone from 'dropzone';
import Mousetrap from 'mousetrap';
import ZenMode from '~/zen_mode';
+import initNotes from '~/init_notes';
describe('ZenMode', () => {
let zen;
@@ -28,6 +29,7 @@ describe('ZenMode', () => {
beforeEach(() => {
loadFixtures(fixtureName);
+ initNotes();
dropzoneForElementSpy = spyOn(Dropzone, 'forElement').and.callFake(() => ({
enable: () => true,
diff --git a/spec/lib/api/helpers/graphql_helpers_spec.rb b/spec/lib/api/helpers/graphql_helpers_spec.rb
new file mode 100644
index 00000000000..c775ba6d5e8
--- /dev/null
+++ b/spec/lib/api/helpers/graphql_helpers_spec.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe API::Helpers::GraphqlHelpers do
+ describe 'run_graphql!' do
+ let(:query) { '{ metadata { version } }' }
+
+ let(:graphql_helper) do
+ Class.new do
+ include API::Helpers::GraphqlHelpers
+ end.new
+ end
+
+ context 'when transform function is provided' do
+ let(:result) { { 'data' => { 'metadata' => { 'version' => '1.0.0' } } } }
+
+ before do
+ allow(GitlabSchema).to receive(:execute).and_return(result)
+ end
+
+ it 'returns the expected result' do
+ expect(
+ graphql_helper.run_graphql!(
+ query: query,
+ transform: ->(result) { result.dig('data', 'metadata') }
+ )
+ ).to eq({ 'version' => '1.0.0' })
+ end
+ end
+
+ context 'when a transform function is not provided' do
+ let(:result) { double('result') }
+
+ before do
+ allow(GitlabSchema).to receive(:execute).and_return(result)
+ end
+
+ it 'returns the expected result' do
+ expect(graphql_helper.run_graphql!(query: query)).to eq(result)
+ end
+ end
+ end
+end
diff --git a/spec/lib/backup/files_spec.rb b/spec/lib/backup/files_spec.rb
index e903eada62d..b75f3bafeef 100644
--- a/spec/lib/backup/files_spec.rb
+++ b/spec/lib/backup/files_spec.rb
@@ -24,6 +24,7 @@ describe Backup::Files do
describe '#restore' do
subject { described_class.new('registry', '/var/gitlab-registry') }
+
let(:timestamp) { Time.utc(2017, 3, 22) }
around do |example|
diff --git a/spec/lib/backup/manager_spec.rb b/spec/lib/backup/manager_spec.rb
index fee7ffc60ee..35594cd2fb8 100644
--- a/spec/lib/backup/manager_spec.rb
+++ b/spec/lib/backup/manager_spec.rb
@@ -21,6 +21,49 @@ describe Backup::Manager do
$progress = @old_progress # rubocop:disable Style/GlobalVars
end
+ describe '#pack' do
+ let(:backup_contents) { ['backup_contents'] }
+ let(:tar_system_options) { { out: [tar_file, 'w', Gitlab.config.backup.archive_permissions] } }
+ let(:tar_cmdline) { ['tar', '-cf', '-', *backup_contents, tar_system_options] }
+
+ let(:backup_information) do
+ {
+ backup_created_at: Time.zone.parse('2019-01-01'),
+ gitlab_version: '12.3'
+ }
+ end
+
+ before do
+ allow(ActiveRecord::Base.connection).to receive(:reconnect!)
+ allow(Kernel).to receive(:system).and_return(true)
+
+ allow(subject).to receive(:backup_contents).and_return(backup_contents)
+ allow(subject).to receive(:backup_information).and_return(backup_information)
+ allow(subject).to receive(:upload)
+ end
+
+ context 'when BACKUP is not set' do
+ let(:tar_file) { '1546300800_2019_01_01_12.3_gitlab_backup.tar' }
+
+ it 'uses the default tar file name' do
+ subject.pack
+
+ expect(Kernel).to have_received(:system).with(*tar_cmdline)
+ end
+ end
+
+ context 'when BACKUP is set' do
+ let(:tar_file) { 'custom_gitlab_backup.tar' }
+
+ it 'uses the given value as tar file name' do
+ stub_env('BACKUP', '/ignored/path/custom')
+ subject.pack
+
+ expect(Kernel).to have_received(:system).with(*tar_cmdline)
+ end
+ end
+ end
+
describe '#remove_old' do
let(:files) do
[
@@ -238,7 +281,7 @@ describe Backup::Manager do
allow(Kernel).to receive(:system).and_return(true)
allow(YAML).to receive(:load_file).and_return(gitlab_version: Gitlab::VERSION)
- stub_env('BACKUP', '1451606400_2016_01_01_1.2.3')
+ stub_env('BACKUP', '/ignored/path/1451606400_2016_01_01_1.2.3')
end
it 'unpacks the file' do
diff --git a/spec/lib/backup/repository_spec.rb b/spec/lib/backup/repository_spec.rb
index e1d46c25338..bf827fb3914 100644
--- a/spec/lib/backup/repository_spec.rb
+++ b/spec/lib/backup/repository_spec.rb
@@ -10,7 +10,6 @@ describe Backup::Repository do
before do
allow(progress).to receive(:puts)
allow(progress).to receive(:print)
- allow(FileUtils).to receive(:mkdir_p).and_return(true)
allow(FileUtils).to receive(:mv).and_return(true)
allow_any_instance_of(described_class).to receive(:progress).and_return(progress)
@@ -84,30 +83,6 @@ describe Backup::Repository do
end
end
- describe '#prepare_directories', :seed_helper do
- before do
- allow(FileUtils).to receive(:mkdir_p).and_call_original
- allow(FileUtils).to receive(:mv).and_call_original
- end
-
- after(:all) do
- ensure_seeds
- end
-
- it' removes all repositories' do
- # Sanity check: there should be something for us to delete
- expect(list_repositories).to include(File.join(SEED_STORAGE_PATH, TEST_REPO_PATH))
-
- subject.prepare_directories
-
- expect(list_repositories).to be_empty
- end
-
- def list_repositories
- Dir[File.join(SEED_STORAGE_PATH, '*.git')]
- end
- end
-
describe '#empty_repo?' do
context 'for a wiki' do
let(:wiki) { create(:project_wiki) }
diff --git a/spec/lib/banzai/filter/audio_link_filter_spec.rb b/spec/lib/banzai/filter/audio_link_filter_spec.rb
new file mode 100644
index 00000000000..a8459137169
--- /dev/null
+++ b/spec/lib/banzai/filter/audio_link_filter_spec.rb
@@ -0,0 +1,120 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Banzai::Filter::AudioLinkFilter do
+ def filter(doc, contexts = {})
+ contexts.reverse_merge!({
+ project: project
+ })
+
+ described_class.call(doc, contexts)
+ end
+
+ def link_to_image(path)
+ return '<img/>' if path.nil?
+
+ %(<img src="#{path}"/>)
+ end
+
+ let(:project) { create(:project, :repository) }
+
+ shared_examples 'an audio element' do
+ let(:image) { link_to_image(src) }
+
+ it 'replaces the image tag with an audio tag' do
+ container = filter(image).children.first
+
+ expect(container.name).to eq 'div'
+ expect(container['class']).to eq 'audio-container'
+
+ audio, paragraph = container.children
+
+ expect(audio.name).to eq 'audio'
+ expect(audio['src']).to eq src
+
+ expect(paragraph.name).to eq 'p'
+
+ link = paragraph.children.first
+
+ expect(link.name).to eq 'a'
+ expect(link['href']).to eq src
+ expect(link['target']).to eq '_blank'
+ end
+ end
+
+ shared_examples 'an unchanged element' do |ext|
+ it 'leaves the document unchanged' do
+ element = filter(link_to_image(src)).children.first
+
+ expect(element.name).to eq 'img'
+ expect(element['src']).to eq src
+ end
+ end
+
+ context 'when the element src has an audio extension' do
+ Gitlab::FileTypeDetection::SAFE_AUDIO_EXT.each do |ext|
+ it_behaves_like 'an audio element' do
+ let(:src) { "/path/audio.#{ext}" }
+ end
+
+ it_behaves_like 'an audio element' do
+ let(:src) { "/path/audio.#{ext.upcase}" }
+ end
+ end
+ end
+
+ context 'when the element has no src attribute' do
+ let(:src) { nil }
+
+ it_behaves_like 'an unchanged element'
+ end
+
+ context 'when the element src is an image' do
+ let(:src) { '/path/my_image.jpg' }
+
+ it_behaves_like 'an unchanged element'
+ end
+
+ context 'when the element src has an invalid file extension' do
+ let(:src) { '/path/my_audio.somewav' }
+
+ it_behaves_like 'an unchanged element'
+ end
+
+ context 'when data-canonical-src is empty' do
+ let(:image) { %(<img src="#{src}" data-canonical-src=""/>) }
+
+ context 'and src is audio' do
+ let(:src) { '/path/audio.wav' }
+
+ it_behaves_like 'an audio element'
+ end
+
+ context 'and src is an image' do
+ let(:src) { '/path/my_image.jpg' }
+
+ it_behaves_like 'an unchanged element'
+ end
+ end
+
+ context 'when data-canonical-src is set' do
+ it 'uses the correct src' do
+ proxy_src = 'https://assets.example.com/6d8b63'
+ canonical_src = 'http://example.com/test.wav'
+ image = %(<img src="#{proxy_src}" data-canonical-src="#{canonical_src}"/>)
+ container = filter(image).children.first
+
+ expect(container['class']).to eq 'audio-container'
+
+ audio, paragraph = container.children
+
+ expect(audio['src']).to eq proxy_src
+ expect(audio['data-canonical-src']).to eq canonical_src
+
+ link = paragraph.children.first
+
+ expect(link['href']).to eq proxy_src
+ end
+ end
+end
diff --git a/spec/lib/banzai/filter/project_reference_filter_spec.rb b/spec/lib/banzai/filter/project_reference_filter_spec.rb
index 927d226c400..d0b4542d503 100644
--- a/spec/lib/banzai/filter/project_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/project_reference_filter_spec.rb
@@ -15,6 +15,7 @@ describe Banzai::Filter::ProjectReferenceFilter do
let(:project) { create(:project, :public) }
subject { project }
+
let(:subject_name) { "project" }
let(:reference) { get_reference(project) }
diff --git a/spec/lib/banzai/filter/relative_link_filter_spec.rb b/spec/lib/banzai/filter/relative_link_filter_spec.rb
index f8b3748c663..046c346a7ac 100644
--- a/spec/lib/banzai/filter/relative_link_filter_spec.rb
+++ b/spec/lib/banzai/filter/relative_link_filter_spec.rb
@@ -3,6 +3,9 @@
require 'spec_helper'
describe Banzai::Filter::RelativeLinkFilter do
+ include GitHelpers
+ include RepoHelpers
+
def filter(doc, contexts = {})
contexts.reverse_merge!({
commit: commit,
@@ -26,6 +29,10 @@ describe Banzai::Filter::RelativeLinkFilter do
%(<video src="#{path}"></video>)
end
+ def audio(path)
+ %(<audio src="#{path}"></audio>)
+ end
+
def link(path)
%(<a href="#{path}">#{path}</a>)
end
@@ -34,6 +41,12 @@ describe Banzai::Filter::RelativeLinkFilter do
%(<div>#{element}</div>)
end
+ def allow_gitaly_n_plus_1
+ Gitlab::GitalyClient.allow_n_plus_1_calls do
+ yield
+ end
+ end
+
let(:project) { create(:project, :repository, :public) }
let(:user) { create(:user) }
let(:group) { nil }
@@ -44,6 +57,19 @@ describe Banzai::Filter::RelativeLinkFilter do
let(:requested_path) { '/' }
let(:only_path) { true }
+ it 'does not trigger a gitaly n+1', :request_store do
+ raw_doc = ""
+
+ allow_gitaly_n_plus_1 do
+ 30.times do |i|
+ create_file_in_repo(project, ref, ref, "new_file_#{i}", "x" )
+ raw_doc += link("new_file_#{i}")
+ end
+ end
+
+ expect { filter(raw_doc) }.to change { Gitlab::GitalyClient.get_request_count }.by(2)
+ end
+
shared_examples :preserve_unchanged do
it 'does not modify any relative URL in anchor' do
doc = filter(link('README.md'))
@@ -60,6 +86,12 @@ describe Banzai::Filter::RelativeLinkFilter do
expect(doc.at_css('video')['src']).to eq 'files/videos/intro.mp4'
end
+
+ it 'does not modify any relative URL in audio' do
+ doc = filter(audio('files/audio/sample.wav'), commit: project.commit('audio'), ref: 'audio')
+
+ expect(doc.at_css('audio')['src']).to eq 'files/audio/sample.wav'
+ end
end
context 'with a project_wiki' do
@@ -196,6 +228,13 @@ describe Banzai::Filter::RelativeLinkFilter do
.to eq "/#{project_path}/raw/video/files/videos/intro.mp4"
end
+ it 'rebuilds relative URL for audio in the repo' do
+ doc = filter(audio('files/audio/sample.wav'), commit: project.commit('audio'), ref: 'audio')
+
+ expect(doc.at_css('audio')['src'])
+ .to eq "/#{project_path}/raw/audio/files/audio/sample.wav"
+ end
+
it 'does not modify relative URL with an anchor only' do
doc = filter(link('#section-1'))
expect(doc.at_css('a')['href']).to eq '#section-1'
@@ -206,6 +245,12 @@ describe Banzai::Filter::RelativeLinkFilter do
expect(doc.at_css('a')['href']).to eq 'http://example.com'
end
+ it 'does not call gitaly' do
+ filter(link('http://example.com'))
+
+ expect(described_class).not_to receive(:get_blob_types)
+ end
+
it 'supports Unicode filenames' do
path = 'files/images/한글.png'
escaped = Addressable::URI.escape(path)
@@ -244,7 +289,8 @@ describe Banzai::Filter::RelativeLinkFilter do
end
context 'when ref name contains special chars' do
- let(:ref) {'mark#\'@],+;-._/#@!$&()+down'}
+ let(:ref) { 'mark#\'@],+;-._/#@!$&()+down' }
+ let(:path) { 'files/images/logo-black.png' }
it 'correctly escapes the ref' do
# Addressable won't escape the '#', so we do this manually
@@ -252,8 +298,9 @@ describe Banzai::Filter::RelativeLinkFilter do
# Stub this method so the branch doesn't actually need to be in the repo
allow_any_instance_of(described_class).to receive(:uri_type).and_return(:raw)
+ allow_any_instance_of(described_class).to receive(:get_uri_types).and_return({ path: :tree })
- doc = filter(link('files/images/logo-black.png'))
+ doc = filter(link(path))
expect(doc.at_css('a')['href'])
.to eq "/#{project_path}/raw/#{ref_escaped}/files/images/logo-black.png"
diff --git a/spec/lib/banzai/filter/table_of_contents_filter_spec.rb b/spec/lib/banzai/filter/table_of_contents_filter_spec.rb
index 5ca3c722e3e..05ef77c811a 100644
--- a/spec/lib/banzai/filter/table_of_contents_filter_spec.rb
+++ b/spec/lib/banzai/filter/table_of_contents_filter_spec.rb
@@ -82,7 +82,9 @@ describe Banzai::Filter::TableOfContentsFilter do
it 'supports Unicode' do
doc = filter(header(1, '한글'))
expect(doc.css('h1 a').first.attr('id')).to eq 'user-content-한글'
- expect(doc.css('h1 a').first.attr('href')).to eq '#한글'
+ # check that we encode the href to avoid issues with the
+ # ExternalLinkFilter (see https://gitlab.com/gitlab-org/gitlab/issues/26210)
+ expect(doc.css('h1 a').first.attr('href')).to eq "##{CGI.escape('한글')}"
end
end
end
diff --git a/spec/lib/banzai/filter/user_reference_filter_spec.rb b/spec/lib/banzai/filter/user_reference_filter_spec.rb
index 6bc87d245f5..a09aeb7d7f6 100644
--- a/spec/lib/banzai/filter/user_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/user_reference_filter_spec.rb
@@ -12,6 +12,7 @@ describe Banzai::Filter::UserReferenceFilter do
let(:project) { create(:project, :public) }
let(:user) { create(:user) }
subject { user }
+
let(:subject_name) { "user" }
let(:reference) { get_reference(user) }
diff --git a/spec/lib/banzai/filter/video_link_filter_spec.rb b/spec/lib/banzai/filter/video_link_filter_spec.rb
index cd932f502f3..a395b021f32 100644
--- a/spec/lib/banzai/filter/video_link_filter_spec.rb
+++ b/spec/lib/banzai/filter/video_link_filter_spec.rb
@@ -12,52 +12,99 @@ describe Banzai::Filter::VideoLinkFilter do
end
def link_to_image(path)
- %(<img src="#{path}" />)
+ return '<img/>' if path.nil?
+
+ %(<img src="#{path}"/>)
end
let(:project) { create(:project, :repository) }
- context 'when the element src has a video extension' do
- UploaderHelper::VIDEO_EXT.each do |ext|
- it "replaces the image tag 'path/video.#{ext}' with a video tag" do
- container = filter(link_to_image("/path/video.#{ext}")).children.first
+ shared_examples 'a video element' do
+ let(:image) { link_to_image(src) }
- expect(container.name).to eq 'div'
- expect(container['class']).to eq 'video-container'
+ it 'replaces the image tag with a video tag' do
+ container = filter(image).children.first
- video, paragraph = container.children
+ expect(container.name).to eq 'div'
+ expect(container['class']).to eq 'video-container'
- expect(video.name).to eq 'video'
- expect(video['src']).to eq "/path/video.#{ext}"
+ video, paragraph = container.children
- expect(paragraph.name).to eq 'p'
+ expect(video.name).to eq 'video'
+ expect(video['src']).to eq src
+ expect(video['width']).to eq "100%"
- link = paragraph.children.first
+ expect(paragraph.name).to eq 'p'
- expect(link.name).to eq 'a'
- expect(link['href']).to eq "/path/video.#{ext}"
- expect(link['target']).to eq '_blank'
- end
+ link = paragraph.children.first
+
+ expect(link.name).to eq 'a'
+ expect(link['href']).to eq src
+ expect(link['target']).to eq '_blank'
end
end
- context 'when the element src is an image' do
+ shared_examples 'an unchanged element' do |ext|
it 'leaves the document unchanged' do
- element = filter(link_to_image('/path/my_image.jpg')).children.first
+ element = filter(link_to_image(src)).children.first
expect(element.name).to eq 'img'
- expect(element['src']).to eq '/path/my_image.jpg'
+ expect(element['src']).to eq src
end
end
- context 'when asset proxy is enabled' do
- it 'uses the correct src' do
- stub_asset_proxy_setting(enabled: true)
+ context 'when the element src has a video extension' do
+ Gitlab::FileTypeDetection::SAFE_VIDEO_EXT.each do |ext|
+ it_behaves_like 'a video element' do
+ let(:src) { "/path/video.#{ext}" }
+ end
+
+ it_behaves_like 'a video element' do
+ let(:src) { "/path/video.#{ext.upcase}" }
+ end
+ end
+ end
+
+ context 'when the element has no src attribute' do
+ let(:src) { nil }
+
+ it_behaves_like 'an unchanged element'
+ end
+
+ context 'when the element src is an image' do
+ let(:src) { '/path/my_image.jpg' }
+
+ it_behaves_like 'an unchanged element'
+ end
+
+ context 'when the element src has an invalid file extension' do
+ let(:src) { '/path/my_video.somemp4' }
+
+ it_behaves_like 'an unchanged element'
+ end
+
+ context 'when data-canonical-src is empty' do
+ let(:image) { %(<img src="#{src}" data-canonical-src=""/>) }
+ context 'and src is a video' do
+ let(:src) { '/path/video.mp4' }
+
+ it_behaves_like 'a video element'
+ end
+
+ context 'and src is an image' do
+ let(:src) { '/path/my_image.jpg' }
+
+ it_behaves_like 'an unchanged element'
+ end
+ end
+
+ context 'when data-canonical-src is set' do
+ it 'uses the correct src' do
proxy_src = 'https://assets.example.com/6d8b63'
canonical_src = 'http://example.com/test.mp4'
- image = %(<img src="#{proxy_src}" data-canonical-src="#{canonical_src}" />)
- container = filter(image, asset_proxy_enabled: true).children.first
+ image = %(<img src="#{proxy_src}" data-canonical-src="#{canonical_src}"/>)
+ container = filter(image).children.first
expect(container['class']).to eq 'video-container'
diff --git a/spec/lib/banzai/filter/wiki_link_filter_spec.rb b/spec/lib/banzai/filter/wiki_link_filter_spec.rb
index d2d539a62fc..4587bd85939 100644
--- a/spec/lib/banzai/filter/wiki_link_filter_spec.rb
+++ b/spec/lib/banzai/filter/wiki_link_filter_spec.rb
@@ -60,6 +60,14 @@ describe Banzai::Filter::WikiLinkFilter do
expect(filtered_link.attribute('src').value).to eq("#{wiki.wiki_base_path}/#{repository_upload_folder}/a.mp4")
end
end
+
+ context 'with "audio" html tag' do
+ it 'rewrites links' do
+ filtered_link = filter("<audio src='#{repository_upload_folder}/a.wav'></audio>", project_wiki: wiki).children[0]
+
+ expect(filtered_link.attribute('src').value).to eq("#{wiki.wiki_base_path}/#{repository_upload_folder}/a.wav")
+ end
+ end
end
describe "invalid links" do
diff --git a/spec/lib/banzai/pipeline/wiki_pipeline_spec.rb b/spec/lib/banzai/pipeline/wiki_pipeline_spec.rb
index 4a485fbc2bd..26f2b0b0acf 100644
--- a/spec/lib/banzai/pipeline/wiki_pipeline_spec.rb
+++ b/spec/lib/banzai/pipeline/wiki_pipeline_spec.rb
@@ -260,11 +260,11 @@ describe Banzai::Pipeline::WikiPipeline do
end
end
- describe 'videos' do
- let(:namespace) { create(:namespace, name: "wiki_link_ns") }
- let(:project) { create(:project, :public, name: "wiki_link_project", namespace: namespace) }
- let(:project_wiki) { ProjectWiki.new(project, double(:user)) }
- let(:page) { build(:wiki_page, wiki: project_wiki, page: OpenStruct.new(url_path: 'nested/twice/start-page')) }
+ describe 'videos and audio' do
+ let_it_be(:namespace) { create(:namespace, name: "wiki_link_ns") }
+ let_it_be(:project) { create(:project, :public, name: "wiki_link_project", namespace: namespace) }
+ let_it_be(:project_wiki) { ProjectWiki.new(project, double(:user)) }
+ let_it_be(:page) { build(:wiki_page, wiki: project_wiki, page: OpenStruct.new(url_path: 'nested/twice/start-page')) }
it 'generates video html structure' do
markdown = "![video_file](video_file_name.mp4)"
@@ -279,5 +279,19 @@ describe Banzai::Pipeline::WikiPipeline do
expect(output).to include('<video src="/wiki_link_ns/wiki_link_project/wikis/nested/twice/video%20file%20name.mp4"')
end
+
+ it 'generates audio html structure' do
+ markdown = "![audio_file](audio_file_name.wav)"
+ output = described_class.to_html(markdown, project: project, project_wiki: project_wiki, page_slug: page.slug)
+
+ expect(output).to include('<audio src="/wiki_link_ns/wiki_link_project/wikis/nested/twice/audio_file_name.wav"')
+ end
+
+ it 'rewrites and replaces audio links names with white spaces to %20' do
+ markdown = "![audio file](audio file name.wav)"
+ output = described_class.to_html(markdown, project: project, project_wiki: project_wiki, page_slug: page.slug)
+
+ expect(output).to include('<audio src="/wiki_link_ns/wiki_link_project/wikis/nested/twice/audio%20file%20name.wav"')
+ end
end
end
diff --git a/spec/lib/banzai/reference_parser/commit_parser_spec.rb b/spec/lib/banzai/reference_parser/commit_parser_spec.rb
index b44ae67e430..eac1cf16a8f 100644
--- a/spec/lib/banzai/reference_parser/commit_parser_spec.rb
+++ b/spec/lib/banzai/reference_parser/commit_parser_spec.rb
@@ -8,6 +8,7 @@ describe Banzai::ReferenceParser::CommitParser do
let(:project) { create(:project, :public) }
let(:user) { create(:user) }
subject { described_class.new(Banzai::RenderContext.new(project, user)) }
+
let(:link) { empty_html_link }
describe '#nodes_visible_to_user' do
diff --git a/spec/lib/banzai/reference_parser/commit_range_parser_spec.rb b/spec/lib/banzai/reference_parser/commit_range_parser_spec.rb
index da853233018..78b337466aa 100644
--- a/spec/lib/banzai/reference_parser/commit_range_parser_spec.rb
+++ b/spec/lib/banzai/reference_parser/commit_range_parser_spec.rb
@@ -8,6 +8,7 @@ describe Banzai::ReferenceParser::CommitRangeParser do
let(:project) { create(:project, :public) }
let(:user) { create(:user) }
subject { described_class.new(Banzai::RenderContext.new(project, user)) }
+
let(:link) { empty_html_link }
describe '#nodes_visible_to_user' do
diff --git a/spec/lib/banzai/reference_parser/external_issue_parser_spec.rb b/spec/lib/banzai/reference_parser/external_issue_parser_spec.rb
index 0f29a95bdcc..9343d52e44b 100644
--- a/spec/lib/banzai/reference_parser/external_issue_parser_spec.rb
+++ b/spec/lib/banzai/reference_parser/external_issue_parser_spec.rb
@@ -8,6 +8,7 @@ describe Banzai::ReferenceParser::ExternalIssueParser do
let(:project) { create(:project, :public) }
let(:user) { create(:user) }
subject { described_class.new(Banzai::RenderContext.new(project, user)) }
+
let(:link) { empty_html_link }
describe '#nodes_visible_to_user' do
diff --git a/spec/lib/banzai/reference_parser/label_parser_spec.rb b/spec/lib/banzai/reference_parser/label_parser_spec.rb
index cf8adb57ffc..8b66a891e69 100644
--- a/spec/lib/banzai/reference_parser/label_parser_spec.rb
+++ b/spec/lib/banzai/reference_parser/label_parser_spec.rb
@@ -9,6 +9,7 @@ describe Banzai::ReferenceParser::LabelParser do
let(:user) { create(:user) }
let(:label) { create(:label, project: project) }
subject { described_class.new(Banzai::RenderContext.new(project, user)) }
+
let(:link) { empty_html_link }
describe '#nodes_visible_to_user' do
diff --git a/spec/lib/banzai/reference_parser/mentioned_user_parser_spec.rb b/spec/lib/banzai/reference_parser/mentioned_user_parser_spec.rb
new file mode 100644
index 00000000000..1be279375bd
--- /dev/null
+++ b/spec/lib/banzai/reference_parser/mentioned_user_parser_spec.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Banzai::ReferenceParser::MentionedUserParser do
+ include ReferenceParserHelpers
+
+ let(:group) { create(:group, :private) }
+ let(:user) { create(:user) }
+ let(:new_user) { create(:user) }
+ let(:project) { create(:project, group: group, creator: user) }
+ let(:link) { empty_html_link }
+
+ subject { described_class.new(Banzai::RenderContext.new(project, new_user)) }
+
+ describe '#gather_references' do
+ context 'when the link has a data-group attribute' do
+ context 'using an existing group ID' do
+ before do
+ link['data-group'] = project.group.id.to_s
+ group.add_developer(new_user)
+ end
+
+ it 'returns empty list of users' do
+ expect(subject.gather_references([link])).to eq([])
+ end
+ end
+ end
+
+ context 'when the link has a data-project attribute' do
+ context 'using an existing project ID' do
+ before do
+ link['data-project'] = project.id.to_s
+ project.add_developer(new_user)
+ end
+
+ it 'returns empty list of users' do
+ expect(subject.gather_references([link])).to eq([])
+ end
+ end
+ end
+
+ context 'when the link has a data-user attribute' do
+ it 'returns an Array of users' do
+ link['data-user'] = user.id.to_s
+
+ expect(subject.referenced_by([link])).to eq([user])
+ end
+ end
+ end
+end
diff --git a/spec/lib/banzai/reference_parser/mentioned_users_by_group_parser_spec.rb b/spec/lib/banzai/reference_parser/mentioned_users_by_group_parser_spec.rb
new file mode 100644
index 00000000000..99d607629eb
--- /dev/null
+++ b/spec/lib/banzai/reference_parser/mentioned_users_by_group_parser_spec.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Banzai::ReferenceParser::MentionedUsersByGroupParser do
+ include ReferenceParserHelpers
+
+ let(:group) { create(:group, :private) }
+ let(:user) { create(:user) }
+ let(:new_user) { create(:user) }
+ let(:project) { create(:project, group: group, creator: user) }
+ let(:link) { empty_html_link }
+
+ subject { described_class.new(Banzai::RenderContext.new(project, new_user)) }
+
+ describe '#gather_references' do
+ context 'when the link has a data-group attribute' do
+ context 'using an existing group ID where user does not have access' do
+ it 'returns empty array' do
+ link['data-group'] = project.group.id.to_s
+
+ expect(subject.gather_references([link])).to eq([])
+ end
+ end
+
+ context 'using an existing group ID' do
+ before do
+ link['data-group'] = project.group.id.to_s
+ group.add_developer(new_user)
+ end
+
+ it 'returns groups' do
+ expect(subject.gather_references([link])).to eq([group])
+ end
+ end
+
+ context 'using a non-existing group ID' do
+ it 'returns an empty Array' do
+ link['data-group'] = 'test-non-existing'
+
+ expect(subject.gather_references([link])).to eq([])
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/banzai/reference_parser/mentioned_users_by_project_parser_spec.rb b/spec/lib/banzai/reference_parser/mentioned_users_by_project_parser_spec.rb
new file mode 100644
index 00000000000..155f2189d9e
--- /dev/null
+++ b/spec/lib/banzai/reference_parser/mentioned_users_by_project_parser_spec.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Banzai::ReferenceParser::MentionedUsersByProjectParser do
+ include ReferenceParserHelpers
+
+ let(:group) { create(:group, :private) }
+ let(:user) { create(:user) }
+ let(:new_user) { create(:user) }
+ let(:project) { create(:project, group: group, creator: user) }
+ let(:link) { empty_html_link }
+
+ subject { described_class.new(Banzai::RenderContext.new(project, new_user)) }
+
+ describe '#gather_references' do
+ context 'when the link has a data-project attribute' do
+ context 'using an existing project ID where user does not have access' do
+ it 'returns empty Array' do
+ link['data-project'] = project.id.to_s
+
+ expect(subject.gather_references([link])).to eq([])
+ end
+ end
+
+ context 'using an existing project ID' do
+ before do
+ link['data-project'] = project.id.to_s
+ project.add_developer(new_user)
+ end
+
+ it 'returns an Array of referenced projects' do
+ expect(subject.gather_references([link])).to eq([project])
+ end
+ end
+
+ context 'using a non-existing project ID' do
+ it 'returns an empty Array' do
+ link['data-project'] = 'inexisting-project-id'
+
+ expect(subject.gather_references([link])).to eq([])
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/banzai/reference_parser/merge_request_parser_spec.rb b/spec/lib/banzai/reference_parser/merge_request_parser_spec.rb
index 1561dabcdbf..cb65893aea0 100644
--- a/spec/lib/banzai/reference_parser/merge_request_parser_spec.rb
+++ b/spec/lib/banzai/reference_parser/merge_request_parser_spec.rb
@@ -9,6 +9,7 @@ describe Banzai::ReferenceParser::MergeRequestParser do
let(:project) { create(:project, :public) }
let(:merge_request) { create(:merge_request, source_project: project) }
subject { described_class.new(Banzai::RenderContext.new(merge_request.target_project, user)) }
+
let(:link) { empty_html_link }
describe '#nodes_visible_to_user' do
diff --git a/spec/lib/banzai/reference_parser/milestone_parser_spec.rb b/spec/lib/banzai/reference_parser/milestone_parser_spec.rb
index 006f8e37690..25ba41dd8a0 100644
--- a/spec/lib/banzai/reference_parser/milestone_parser_spec.rb
+++ b/spec/lib/banzai/reference_parser/milestone_parser_spec.rb
@@ -9,6 +9,7 @@ describe Banzai::ReferenceParser::MilestoneParser do
let(:user) { create(:user) }
let(:milestone) { create(:milestone, project: project) }
subject { described_class.new(Banzai::RenderContext.new(project, user)) }
+
let(:link) { empty_html_link }
describe '#nodes_visible_to_user' do
diff --git a/spec/lib/banzai/reference_parser/project_parser_spec.rb b/spec/lib/banzai/reference_parser/project_parser_spec.rb
index e4936aa9e57..356dde1e9c2 100644
--- a/spec/lib/banzai/reference_parser/project_parser_spec.rb
+++ b/spec/lib/banzai/reference_parser/project_parser_spec.rb
@@ -8,6 +8,7 @@ describe Banzai::ReferenceParser::ProjectParser do
let(:project) { create(:project, :public) }
let(:user) { create(:user) }
subject { described_class.new(Banzai::RenderContext.new(project, user)) }
+
let(:link) { empty_html_link }
describe '#referenced_by' do
diff --git a/spec/lib/banzai/reference_parser/snippet_parser_spec.rb b/spec/lib/banzai/reference_parser/snippet_parser_spec.rb
index 528f79ed020..05dc1cb4d2d 100644
--- a/spec/lib/banzai/reference_parser/snippet_parser_spec.rb
+++ b/spec/lib/banzai/reference_parser/snippet_parser_spec.rb
@@ -12,6 +12,7 @@ describe Banzai::ReferenceParser::SnippetParser do
let(:project_member) { create(:user) }
subject { described_class.new(Banzai::RenderContext.new(project, user)) }
+
let(:link) { empty_html_link }
def visible_references(snippet_visibility, user = nil)
diff --git a/spec/lib/banzai/reference_parser/user_parser_spec.rb b/spec/lib/banzai/reference_parser/user_parser_spec.rb
index a5b4e59a3a1..931fb1e3953 100644
--- a/spec/lib/banzai/reference_parser/user_parser_spec.rb
+++ b/spec/lib/banzai/reference_parser/user_parser_spec.rb
@@ -9,6 +9,7 @@ describe Banzai::ReferenceParser::UserParser do
let(:user) { create(:user) }
let(:project) { create(:project, :public, group: group, creator: user) }
subject { described_class.new(Banzai::RenderContext.new(project, user)) }
+
let(:link) { empty_html_link }
describe '#referenced_by' do
diff --git a/spec/lib/container_registry/client_spec.rb b/spec/lib/container_registry/client_spec.rb
index 6c2b338bfcd..3782c30e88a 100644
--- a/spec/lib/container_registry/client_spec.rb
+++ b/spec/lib/container_registry/client_spec.rb
@@ -73,4 +73,69 @@ describe ContainerRegistry::Client do
expect(response).to eq('Successfully redirected')
end
end
+
+ def stub_upload(path, content, digest, status = 200)
+ stub_request(:post, "http://container-registry/v2/#{path}/blobs/uploads/")
+ .to_return(status: status, body: "", headers: { 'location' => 'http://container-registry/next_upload?id=someid' })
+
+ stub_request(:put, "http://container-registry/next_upload?digest=#{digest}&id=someid")
+ .with(body: content)
+ .to_return(status: status, body: "", headers: {})
+ end
+
+ describe '#upload_blob' do
+ subject { client.upload_blob('path', 'content', 'sha256:123') }
+
+ context 'with successful uploads' do
+ it 'starts the upload and posts the blob' do
+ stub_upload('path', 'content', 'sha256:123')
+
+ expect(subject).to be_success
+ end
+ end
+
+ context 'with a failed upload' do
+ before do
+ stub_upload('path', 'content', 'sha256:123', 400)
+ end
+
+ it 'returns nil' do
+ expect(subject).to be nil
+ end
+ end
+ end
+
+ describe '#generate_empty_manifest' do
+ subject { client.generate_empty_manifest('path') }
+
+ let(:result_manifest) do
+ {
+ schemaVersion: 2,
+ mediaType: 'application/vnd.docker.distribution.manifest.v2+json',
+ config: {
+ mediaType: 'application/vnd.docker.container.image.v1+json',
+ size: 21,
+ digest: 'sha256:4435000728ee66e6a80e55637fc22725c256b61de344a2ecdeaac6bdb36e8bc3'
+ }
+ }
+ end
+
+ it 'uploads a random image and returns the manifest' do
+ stub_upload('path', "{\n \"config\": {\n }\n}", 'sha256:4435000728ee66e6a80e55637fc22725c256b61de344a2ecdeaac6bdb36e8bc3')
+
+ expect(subject).to eq(result_manifest)
+ end
+ end
+
+ describe '#put_tag' do
+ subject { client.put_tag('path', 'tagA', { foo: :bar }) }
+
+ it 'uploads the manifest and returns the digest' do
+ stub_request(:put, "http://container-registry/v2/path/manifests/tagA")
+ .with(body: "{\n \"foo\": \"bar\"\n}")
+ .to_return(status: 200, body: "", headers: { 'docker-content-digest' => 'sha256:123' })
+
+ expect(subject).to eq 'sha256:123'
+ end
+ end
end
diff --git a/spec/lib/container_registry/tag_spec.rb b/spec/lib/container_registry/tag_spec.rb
index 110f006536b..3115dfe852f 100644
--- a/spec/lib/container_registry/tag_spec.rb
+++ b/spec/lib/container_registry/tag_spec.rb
@@ -179,7 +179,7 @@ describe ContainerRegistry::Tag do
end
end
- describe '#delete' do
+ describe '#unsafe_delete' do
before do
stub_request(:delete, 'http://registry.gitlab/v2/group/test/manifests/sha256:digest')
.with(headers: headers)
@@ -187,7 +187,7 @@ describe ContainerRegistry::Tag do
end
it 'correctly deletes the tag' do
- expect(tag.delete).to be_truthy
+ expect(tag.unsafe_delete).to be_truthy
end
end
end
diff --git a/spec/lib/event_filter_spec.rb b/spec/lib/event_filter_spec.rb
index d6eca8d85ff..21b8f726425 100644
--- a/spec/lib/event_filter_spec.rb
+++ b/spec/lib/event_filter_spec.rb
@@ -3,12 +3,6 @@
require 'spec_helper'
describe EventFilter do
- describe 'FILTERS' do
- it 'returns a definite list of filters' do
- expect(described_class::FILTERS).to eq(%w[all push merged issue comments team])
- end
- end
-
describe '#filter' do
it 'returns "all" if given filter is nil' do
expect(described_class.new(nil).filter).to eq(described_class::ALL)
diff --git a/spec/lib/gitlab/analytics/cycle_analytics/base_query_builder_spec.rb b/spec/lib/gitlab/analytics/cycle_analytics/base_query_builder_spec.rb
new file mode 100644
index 00000000000..0fc9d3c1e9e
--- /dev/null
+++ b/spec/lib/gitlab/analytics/cycle_analytics/base_query_builder_spec.rb
@@ -0,0 +1,62 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Analytics::CycleAnalytics::BaseQueryBuilder do
+ let_it_be(:project) { create(:project, :empty_repo) }
+ let_it_be(:mr1) { create(:merge_request, target_project: project, source_project: project, allow_broken: true, created_at: 3.months.ago) }
+ let_it_be(:mr2) { create(:merge_request, target_project: project, source_project: project, allow_broken: true, created_at: 1.month.ago) }
+ let(:params) { {} }
+ let(:records) do
+ stage = build(:cycle_analytics_project_stage, {
+ start_event_identifier: :merge_request_created,
+ end_event_identifier: :merge_request_merged,
+ project: project
+ })
+ described_class.new(stage: stage, params: params).build.to_a
+ end
+
+ before do
+ mr1.metrics.update!(merged_at: 1.month.ago)
+ mr2.metrics.update!(merged_at: Time.now)
+ end
+
+ around do |example|
+ Timecop.freeze { example.run }
+ end
+
+ describe 'date range parameters' do
+ context 'when filters by only the `from` parameter' do
+ before do
+ params[:from] = 4.months.ago
+ end
+
+ it { expect(records.size).to eq(2) }
+ end
+
+ context 'when filters by both `from` and `to` parameters' do
+ before do
+ params.merge!(from: 4.months.ago, to: 2.months.ago)
+ end
+
+ it { expect(records.size).to eq(1) }
+ end
+
+ context 'invalid date range is provided' do
+ before do
+ params.merge!(from: 1.month.ago, to: 10.months.ago)
+ end
+
+ it { expect(records.size).to eq(0) }
+ end
+ end
+
+ it 'scopes query within the target project' do
+ other_mr = create(:merge_request, source_project: create(:project), allow_broken: true, created_at: 2.months.ago)
+ other_mr.metrics.update!(merged_at: 1.month.ago)
+
+ params[:from] = 1.year.ago
+
+ expect(records.size).to eq(2)
+ end
+end
diff --git a/spec/lib/gitlab/analytics/cycle_analytics/records_fetcher_spec.rb b/spec/lib/gitlab/analytics/cycle_analytics/records_fetcher_spec.rb
new file mode 100644
index 00000000000..334cab0b799
--- /dev/null
+++ b/spec/lib/gitlab/analytics/cycle_analytics/records_fetcher_spec.rb
@@ -0,0 +1,131 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Analytics::CycleAnalytics::RecordsFetcher do
+ around do |example|
+ Timecop.freeze { example.run }
+ end
+
+ let_it_be(:project) { create(:project, :empty_repo) }
+ let_it_be(:user) { create(:user) }
+
+ subject do
+ Gitlab::Analytics::CycleAnalytics::DataCollector.new(
+ stage: stage,
+ params: {
+ from: 1.year.ago,
+ current_user: user
+ }
+ ).records_fetcher.serialized_records
+ end
+
+ describe '#serialized_records' do
+ shared_context 'when records are loaded by maintainer' do
+ before do
+ project.add_user(user, Gitlab::Access::MAINTAINER)
+ end
+
+ it 'returns all records' do
+ expect(subject.size).to eq(2)
+ end
+ end
+
+ describe 'for issue based stage' do
+ let_it_be(:issue1) { create(:issue, project: project) }
+ let_it_be(:issue2) { create(:issue, project: project, confidential: true) }
+ let(:stage) do
+ build(:cycle_analytics_project_stage, {
+ start_event_identifier: :plan_stage_start,
+ end_event_identifier: :issue_first_mentioned_in_commit,
+ project: project
+ })
+ end
+
+ before do
+ issue1.metrics.update(first_added_to_board_at: 3.days.ago, first_mentioned_in_commit_at: 2.days.ago)
+ issue2.metrics.update(first_added_to_board_at: 3.days.ago, first_mentioned_in_commit_at: 2.days.ago)
+ end
+
+ context 'when records are loaded by guest' do
+ before do
+ project.add_user(user, Gitlab::Access::GUEST)
+ end
+
+ it 'filters out confidential issues' do
+ expect(subject.size).to eq(1)
+ expect(subject.first[:iid].to_s).to eq(issue1.iid.to_s)
+ end
+ end
+
+ include_context 'when records are loaded by maintainer'
+ end
+
+ describe 'for merge request based stage' do
+ let(:mr1) { create(:merge_request, created_at: 5.days.ago, source_project: project, allow_broken: true) }
+ let(:mr2) { create(:merge_request, created_at: 4.days.ago, source_project: project, allow_broken: true) }
+ let(:stage) do
+ build(:cycle_analytics_project_stage, {
+ start_event_identifier: :merge_request_created,
+ end_event_identifier: :merge_request_merged,
+ project: project
+ })
+ end
+
+ before do
+ mr1.metrics.update(merged_at: 3.days.ago)
+ mr2.metrics.update(merged_at: 3.days.ago)
+ end
+
+ include_context 'when records are loaded by maintainer'
+ end
+
+ describe 'special case' do
+ let(:mr1) { create(:merge_request, source_project: project, allow_broken: true, created_at: 20.days.ago) }
+ let(:mr2) { create(:merge_request, source_project: project, allow_broken: true, created_at: 19.days.ago) }
+ let(:ci_build1) { create(:ci_build) }
+ let(:ci_build2) { create(:ci_build) }
+ let(:default_stages) { Gitlab::Analytics::CycleAnalytics::DefaultStages }
+ let(:stage) { build(:cycle_analytics_project_stage, default_stages.params_for_test_stage.merge(project: project)) }
+
+ before do
+ mr1.metrics.update!({
+ merged_at: 5.days.ago,
+ first_deployed_to_production_at: 1.day.ago,
+ latest_build_started_at: 5.days.ago,
+ latest_build_finished_at: 1.day.ago,
+ pipeline: ci_build1.pipeline
+ })
+ mr2.metrics.update!({
+ merged_at: 10.days.ago,
+ first_deployed_to_production_at: 5.days.ago,
+ latest_build_started_at: 9.days.ago,
+ latest_build_finished_at: 7.days.ago,
+ pipeline: ci_build2.pipeline
+ })
+ end
+
+ context 'returns build records' do
+ shared_examples 'orders build records by `latest_build_finished_at`' do
+ it 'orders by `latest_build_finished_at`' do
+ build_ids = subject.map { |item| item[:id] }
+
+ expect(build_ids).to eq([ci_build1.id, ci_build2.id])
+ end
+ end
+
+ context 'when requesting records for default test stage' do
+ include_examples 'orders build records by `latest_build_finished_at`'
+ end
+
+ context 'when requesting records for default staging stage' do
+ before do
+ stage.assign_attributes(default_stages.params_for_staging_stage)
+ end
+
+ include_examples 'orders build records by `latest_build_finished_at`'
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/analytics/cycle_analytics/stage_events/code_stage_start_spec.rb b/spec/lib/gitlab/analytics/cycle_analytics/stage_events/code_stage_start_spec.rb
new file mode 100644
index 00000000000..29c8d548754
--- /dev/null
+++ b/spec/lib/gitlab/analytics/cycle_analytics/stage_events/code_stage_start_spec.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Analytics::CycleAnalytics::StageEvents::CodeStageStart do
+ let(:subject) { described_class.new({}) }
+ let(:project) { create(:project) }
+
+ it_behaves_like 'cycle analytics event'
+
+ it 'needs connection with an issue via merge_requests_closing_issues table' do
+ issue = create(:issue, project: project)
+ merge_request = create(:merge_request, source_project: project)
+ create(:merge_requests_closing_issues, issue: issue, merge_request: merge_request)
+
+ other_merge_request = create(:merge_request, source_project: project, source_branch: 'a', target_branch: 'master')
+
+ records = subject.apply_query_customization(MergeRequest.all)
+ expect(records).to eq([merge_request])
+ expect(records).not_to include(other_merge_request)
+ end
+end
diff --git a/spec/lib/gitlab/analytics/cycle_analytics/stage_events/issue_created_spec.rb b/spec/lib/gitlab/analytics/cycle_analytics/stage_events/issue_created_spec.rb
new file mode 100644
index 00000000000..efdef91c5a2
--- /dev/null
+++ b/spec/lib/gitlab/analytics/cycle_analytics/stage_events/issue_created_spec.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Analytics::CycleAnalytics::StageEvents::IssueCreated do
+ it_behaves_like 'cycle analytics event'
+end
diff --git a/spec/lib/gitlab/analytics/cycle_analytics/stage_events/issue_first_mentioned_in_commit_spec.rb b/spec/lib/gitlab/analytics/cycle_analytics/stage_events/issue_first_mentioned_in_commit_spec.rb
new file mode 100644
index 00000000000..50883e1c1e2
--- /dev/null
+++ b/spec/lib/gitlab/analytics/cycle_analytics/stage_events/issue_first_mentioned_in_commit_spec.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Analytics::CycleAnalytics::StageEvents::IssueFirstMentionedInCommit do
+ it_behaves_like 'cycle analytics event'
+end
diff --git a/spec/lib/gitlab/analytics/cycle_analytics/stage_events/issue_stage_end_spec.rb b/spec/lib/gitlab/analytics/cycle_analytics/stage_events/issue_stage_end_spec.rb
new file mode 100644
index 00000000000..85062db370a
--- /dev/null
+++ b/spec/lib/gitlab/analytics/cycle_analytics/stage_events/issue_stage_end_spec.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Analytics::CycleAnalytics::StageEvents::IssueStageEnd do
+ it_behaves_like 'cycle analytics event'
+end
diff --git a/spec/lib/gitlab/analytics/cycle_analytics/stage_events/merge_request_created_spec.rb b/spec/lib/gitlab/analytics/cycle_analytics/stage_events/merge_request_created_spec.rb
new file mode 100644
index 00000000000..7858b810661
--- /dev/null
+++ b/spec/lib/gitlab/analytics/cycle_analytics/stage_events/merge_request_created_spec.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Analytics::CycleAnalytics::StageEvents::MergeRequestCreated do
+ it_behaves_like 'cycle analytics event'
+end
diff --git a/spec/lib/gitlab/analytics/cycle_analytics/stage_events/merge_request_first_deployed_to_production_spec.rb b/spec/lib/gitlab/analytics/cycle_analytics/stage_events/merge_request_first_deployed_to_production_spec.rb
new file mode 100644
index 00000000000..ba9d8be5a2c
--- /dev/null
+++ b/spec/lib/gitlab/analytics/cycle_analytics/stage_events/merge_request_first_deployed_to_production_spec.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Analytics::CycleAnalytics::StageEvents::MergeRequestFirstDeployedToProduction do
+ it_behaves_like 'cycle analytics event'
+end
diff --git a/spec/lib/gitlab/analytics/cycle_analytics/stage_events/merge_request_last_build_finished_spec.rb b/spec/lib/gitlab/analytics/cycle_analytics/stage_events/merge_request_last_build_finished_spec.rb
new file mode 100644
index 00000000000..8e83e10ef96
--- /dev/null
+++ b/spec/lib/gitlab/analytics/cycle_analytics/stage_events/merge_request_last_build_finished_spec.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Analytics::CycleAnalytics::StageEvents::MergeRequestLastBuildFinished do
+ it_behaves_like 'cycle analytics event'
+end
diff --git a/spec/lib/gitlab/analytics/cycle_analytics/stage_events/merge_request_last_build_started_spec.rb b/spec/lib/gitlab/analytics/cycle_analytics/stage_events/merge_request_last_build_started_spec.rb
new file mode 100644
index 00000000000..9f6b430a320
--- /dev/null
+++ b/spec/lib/gitlab/analytics/cycle_analytics/stage_events/merge_request_last_build_started_spec.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Analytics::CycleAnalytics::StageEvents::MergeRequestLastBuildStarted do
+ it_behaves_like 'cycle analytics event'
+end
diff --git a/spec/lib/gitlab/analytics/cycle_analytics/stage_events/merge_request_merged_spec.rb b/spec/lib/gitlab/analytics/cycle_analytics/stage_events/merge_request_merged_spec.rb
new file mode 100644
index 00000000000..ce2aa0a60db
--- /dev/null
+++ b/spec/lib/gitlab/analytics/cycle_analytics/stage_events/merge_request_merged_spec.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Analytics::CycleAnalytics::StageEvents::MergeRequestMerged do
+ it_behaves_like 'cycle analytics event'
+end
diff --git a/spec/lib/gitlab/analytics/cycle_analytics/stage_events/plan_stage_start_spec.rb b/spec/lib/gitlab/analytics/cycle_analytics/stage_events/plan_stage_start_spec.rb
new file mode 100644
index 00000000000..cb63139f0a8
--- /dev/null
+++ b/spec/lib/gitlab/analytics/cycle_analytics/stage_events/plan_stage_start_spec.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Analytics::CycleAnalytics::StageEvents::PlanStageStart do
+ let(:subject) { described_class.new({}) }
+ let(:project) { create(:project) }
+
+ it_behaves_like 'cycle analytics event'
+
+ it 'filters issues where first_associated_with_milestone_at or first_added_to_board_at is filled' do
+ issue1 = create(:issue, project: project)
+ issue1.metrics.update!(first_added_to_board_at: 1.month.ago, first_mentioned_in_commit_at: 2.months.ago)
+
+ issue2 = create(:issue, project: project)
+ issue2.metrics.update!(first_associated_with_milestone_at: 1.month.ago, first_mentioned_in_commit_at: 2.months.ago)
+
+ issue_without_metrics = create(:issue, project: project)
+
+ records = subject.apply_query_customization(Issue.all)
+ expect(records).to match_array([issue1, issue2])
+ expect(records).not_to include(issue_without_metrics)
+ end
+end
diff --git a/spec/lib/gitlab/analytics/cycle_analytics/stage_events/stage_event_spec.rb b/spec/lib/gitlab/analytics/cycle_analytics/stage_events/stage_event_spec.rb
index 29f4be76a65..b05faf5d813 100644
--- a/spec/lib/gitlab/analytics/cycle_analytics/stage_events/stage_event_spec.rb
+++ b/spec/lib/gitlab/analytics/cycle_analytics/stage_events/stage_event_spec.rb
@@ -3,8 +3,11 @@
require 'spec_helper'
describe Gitlab::Analytics::CycleAnalytics::StageEvents::StageEvent do
+ let(:instance) { described_class.new({}) }
+
it { expect(described_class).to respond_to(:name) }
it { expect(described_class).to respond_to(:identifier) }
-
- it { expect(described_class.new({})).to respond_to(:object_type) }
+ it { expect(instance).to respond_to(:object_type) }
+ it { expect(instance).to respond_to(:timestamp_projection) }
+ it { expect(instance).to respond_to(:apply_query_customization) }
end
diff --git a/spec/lib/gitlab/auth/current_user_mode_spec.rb b/spec/lib/gitlab/auth/current_user_mode_spec.rb
new file mode 100644
index 00000000000..b93d460cf48
--- /dev/null
+++ b/spec/lib/gitlab/auth/current_user_mode_spec.rb
@@ -0,0 +1,159 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Auth::CurrentUserMode, :do_not_mock_admin_mode do
+ include_context 'custom session'
+
+ let(:user) { build(:user) }
+
+ subject { described_class.new(user) }
+
+ before do
+ allow(ActiveSession).to receive(:list_sessions).with(user).and_return([session])
+ end
+
+ describe '#admin_mode?', :request_store do
+ context 'when the user is a regular user' do
+ it 'is false by default' do
+ expect(subject.admin_mode?).to be(false)
+ end
+
+ it 'cannot be enabled with a valid password' do
+ subject.enable_admin_mode!(password: user.password)
+
+ expect(subject.admin_mode?).to be(false)
+ end
+
+ it 'cannot be enabled with an invalid password' do
+ subject.enable_admin_mode!(password: nil)
+
+ expect(subject.admin_mode?).to be(false)
+ end
+
+ it 'cannot be enabled with empty params' do
+ subject.enable_admin_mode!
+
+ expect(subject.admin_mode?).to be(false)
+ end
+
+ it 'disable has no effect' do
+ subject.enable_admin_mode!
+ subject.disable_admin_mode!
+
+ expect(subject.admin_mode?).to be(false)
+ end
+
+ context 'skipping password validation' do
+ it 'cannot be enabled with a valid password' do
+ subject.enable_admin_mode!(password: user.password, skip_password_validation: true)
+
+ expect(subject.admin_mode?).to be(false)
+ end
+
+ it 'cannot be enabled with an invalid password' do
+ subject.enable_admin_mode!(skip_password_validation: true)
+
+ expect(subject.admin_mode?).to be(false)
+ end
+ end
+ end
+
+ context 'when the user is an admin' do
+ let(:user) { build(:user, :admin) }
+
+ it 'is false by default' do
+ expect(subject.admin_mode?).to be(false)
+ end
+
+ it 'cannot be enabled with an invalid password' do
+ subject.enable_admin_mode!(password: nil)
+
+ expect(subject.admin_mode?).to be(false)
+ end
+
+ it 'can be enabled with a valid password' do
+ subject.enable_admin_mode!(password: user.password)
+
+ expect(subject.admin_mode?).to be(true)
+ end
+
+ it 'can be disabled' do
+ subject.enable_admin_mode!(password: user.password)
+ subject.disable_admin_mode!
+
+ expect(subject.admin_mode?).to be(false)
+ end
+
+ it 'will expire in the future' do
+ subject.enable_admin_mode!(password: user.password)
+ expect(subject.admin_mode?).to be(true), 'admin mode is not active in the present'
+
+ Timecop.freeze(Gitlab::Auth::CurrentUserMode::MAX_ADMIN_MODE_TIME.from_now) do
+ # in the future this will be a new request, simulate by clearing the RequestStore
+ Gitlab::SafeRequestStore.clear!
+
+ expect(subject.admin_mode?).to be(false), 'admin mode did not expire in the future'
+ end
+ end
+
+ context 'skipping password validation' do
+ it 'can be enabled with a valid password' do
+ subject.enable_admin_mode!(password: user.password, skip_password_validation: true)
+
+ expect(subject.admin_mode?).to be(true)
+ end
+
+ it 'can be enabled with an invalid password' do
+ subject.enable_admin_mode!(skip_password_validation: true)
+
+ expect(subject.admin_mode?).to be(true)
+ end
+ end
+
+ context 'with two independent sessions' do
+ let(:another_session) { {} }
+ let(:another_subject) { described_class.new(user) }
+
+ before do
+ allow(ActiveSession).to receive(:list_sessions).with(user).and_return([session, another_session])
+ end
+
+ it 'can be enabled in one and seen in the other' do
+ Gitlab::Session.with_session(another_session) do
+ another_subject.enable_admin_mode!(password: user.password)
+ end
+
+ expect(subject.admin_mode?).to be(true)
+ end
+ end
+ end
+ end
+
+ describe '#enable_admin_mode!' do
+ let(:user) { build(:user, :admin) }
+
+ it 'creates a timestamp in the session' do
+ subject.enable_admin_mode!(password: user.password)
+
+ expect(session).to include(expected_session_entry(be_within(1.second).of Time.now))
+ end
+ end
+
+ describe '#disable_admin_mode!' do
+ let(:user) { build(:user, :admin) }
+
+ it 'sets the session timestamp to nil' do
+ subject.disable_admin_mode!
+
+ expect(session).to include(expected_session_entry(be_nil))
+ end
+ end
+
+ def expected_session_entry(value_matcher)
+ {
+ Gitlab::Auth::CurrentUserMode::SESSION_STORE_KEY => a_hash_including(
+ Gitlab::Auth::CurrentUserMode::ADMIN_MODE_START_TIME_KEY => value_matcher)
+ }
+ end
+end
diff --git a/spec/lib/gitlab/auth/user_access_denied_reason_spec.rb b/spec/lib/gitlab/auth/user_access_denied_reason_spec.rb
index 8ec19c454d8..7045105a2c7 100644
--- a/spec/lib/gitlab/auth/user_access_denied_reason_spec.rb
+++ b/spec/lib/gitlab/auth/user_access_denied_reason_spec.rb
@@ -33,5 +33,13 @@ describe Gitlab::Auth::UserAccessDeniedReason do
it { is_expected.to match /This action cannot be performed by internal users/ }
end
+
+ context 'when the user is deactivated' do
+ before do
+ user.deactivate!
+ end
+
+ it { is_expected.to eq "Your account has been deactivated by your administrator. Please log back in from a web browser to reactivate your account at #{Gitlab.config.gitlab.url}" }
+ end
end
end
diff --git a/spec/lib/gitlab/auth_spec.rb b/spec/lib/gitlab/auth_spec.rb
index 0365d63ea9c..dc4b0b5b1b6 100644
--- a/spec/lib/gitlab/auth_spec.rb
+++ b/spec/lib/gitlab/auth_spec.rb
@@ -4,6 +4,7 @@ require 'spec_helper'
describe Gitlab::Auth do
let(:gl_auth) { described_class }
+ set(:project) { create(:project) }
describe 'constants' do
it 'API_SCOPES contains all scopes for API access' do
@@ -90,13 +91,13 @@ describe Gitlab::Auth do
end
it 'recognises user-less build' do
- expect(subject).to eq(Gitlab::Auth::Result.new(nil, build.project, :ci, build_authentication_abilities))
+ expect(subject).to eq(Gitlab::Auth::Result.new(nil, build.project, :ci, described_class.build_authentication_abilities))
end
it 'recognises user token' do
build.update(user: create(:user))
- expect(subject).to eq(Gitlab::Auth::Result.new(build.user, build.project, :build, build_authentication_abilities))
+ expect(subject).to eq(Gitlab::Auth::Result.new(build.user, build.project, :build, described_class.build_authentication_abilities))
end
end
@@ -117,26 +118,25 @@ describe Gitlab::Auth do
end
it 'recognizes other ci services' do
- project = create(:project)
project.create_drone_ci_service(active: true)
project.drone_ci_service.update(token: 'token')
expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: 'drone-ci-token')
- expect(gl_auth.find_for_git_client('drone-ci-token', 'token', project: project, ip: 'ip')).to eq(Gitlab::Auth::Result.new(nil, project, :ci, build_authentication_abilities))
+ expect(gl_auth.find_for_git_client('drone-ci-token', 'token', project: project, ip: 'ip')).to eq(Gitlab::Auth::Result.new(nil, project, :ci, described_class.build_authentication_abilities))
end
it 'recognizes master passwords' do
user = create(:user, password: 'password')
expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: user.username)
- expect(gl_auth.find_for_git_client(user.username, 'password', project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(user, nil, :gitlab_or_ldap, full_authentication_abilities))
+ expect(gl_auth.find_for_git_client(user.username, 'password', project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(user, nil, :gitlab_or_ldap, described_class.full_authentication_abilities))
end
include_examples 'user login operation with unique ip limit' do
let(:user) { create(:user, password: 'password') }
def operation
- expect(gl_auth.find_for_git_client(user.username, 'password', project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(user, nil, :gitlab_or_ldap, full_authentication_abilities))
+ expect(gl_auth.find_for_git_client(user.username, 'password', project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(user, nil, :gitlab_or_ldap, described_class.full_authentication_abilities))
end
end
@@ -146,7 +146,7 @@ describe Gitlab::Auth do
token = Gitlab::LfsToken.new(user).token
expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: user.username)
- expect(gl_auth.find_for_git_client(user.username, token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(user, nil, :lfs_token, full_authentication_abilities))
+ expect(gl_auth.find_for_git_client(user.username, token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(user, nil, :lfs_token, described_class.read_write_project_authentication_abilities))
end
it 'recognizes deploy key lfs tokens' do
@@ -154,7 +154,7 @@ describe Gitlab::Auth do
token = Gitlab::LfsToken.new(key).token
expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: "lfs+deploy-key-#{key.id}")
- expect(gl_auth.find_for_git_client("lfs+deploy-key-#{key.id}", token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(key, nil, :lfs_deploy_token, read_only_authentication_abilities))
+ expect(gl_auth.find_for_git_client("lfs+deploy-key-#{key.id}", token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(key, nil, :lfs_deploy_token, described_class.read_only_authentication_abilities))
end
it 'does not try password auth before oauth' do
@@ -167,22 +167,20 @@ describe Gitlab::Auth do
end
it 'grants deploy key write permissions' do
- project = create(:project)
key = create(:deploy_key)
create(:deploy_keys_project, :write_access, deploy_key: key, project: project)
token = Gitlab::LfsToken.new(key).token
expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: "lfs+deploy-key-#{key.id}")
- expect(gl_auth.find_for_git_client("lfs+deploy-key-#{key.id}", token, project: project, ip: 'ip')).to eq(Gitlab::Auth::Result.new(key, nil, :lfs_deploy_token, read_write_authentication_abilities))
+ expect(gl_auth.find_for_git_client("lfs+deploy-key-#{key.id}", token, project: project, ip: 'ip')).to eq(Gitlab::Auth::Result.new(key, nil, :lfs_deploy_token, described_class.read_write_authentication_abilities))
end
it 'does not grant deploy key write permissions' do
- project = create(:project)
key = create(:deploy_key)
token = Gitlab::LfsToken.new(key).token
expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: "lfs+deploy-key-#{key.id}")
- expect(gl_auth.find_for_git_client("lfs+deploy-key-#{key.id}", token, project: project, ip: 'ip')).to eq(Gitlab::Auth::Result.new(key, nil, :lfs_deploy_token, read_only_authentication_abilities))
+ expect(gl_auth.find_for_git_client("lfs+deploy-key-#{key.id}", token, project: project, ip: 'ip')).to eq(Gitlab::Auth::Result.new(key, nil, :lfs_deploy_token, described_class.read_only_authentication_abilities))
end
end
@@ -193,7 +191,7 @@ describe Gitlab::Auth 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, full_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, described_class.full_authentication_abilities))
end
it 'fails for OAuth tokens with other scopes' do
@@ -214,7 +212,7 @@ describe Gitlab::Auth do
it 'succeeds for personal access tokens with the `api` scope' do
personal_access_token = create(:personal_access_token, scopes: ['api'])
- expect_results_with_abilities(personal_access_token, full_authentication_abilities)
+ expect_results_with_abilities(personal_access_token, described_class.full_authentication_abilities)
end
it 'succeeds for personal access tokens with the `read_repository` scope' do
@@ -244,7 +242,7 @@ describe Gitlab::Auth do
it 'succeeds if it is an impersonation token' do
impersonation_token = create(:personal_access_token, :impersonation, scopes: ['api'])
- expect_results_with_abilities(impersonation_token, full_authentication_abilities)
+ expect_results_with_abilities(impersonation_token, described_class.full_authentication_abilities)
end
it 'limits abilities based on scope' do
@@ -267,7 +265,7 @@ describe Gitlab::Auth do
)
expect(gl_auth.find_for_git_client(user.username, user.password, project: nil, ip: 'ip'))
- .to eq(Gitlab::Auth::Result.new(user, nil, :gitlab_or_ldap, full_authentication_abilities))
+ .to eq(Gitlab::Auth::Result.new(user, nil, :gitlab_or_ldap, described_class.full_authentication_abilities))
end
it 'fails through oauth authentication when the username is oauth2' do
@@ -278,7 +276,7 @@ describe Gitlab::Auth do
)
expect(gl_auth.find_for_git_client(user.username, user.password, project: nil, ip: 'ip'))
- .to eq(Gitlab::Auth::Result.new(user, nil, :gitlab_or_ldap, full_authentication_abilities))
+ .to eq(Gitlab::Auth::Result.new(user, nil, :gitlab_or_ldap, described_class.full_authentication_abilities))
end
end
@@ -296,7 +294,6 @@ describe Gitlab::Auth do
end
context 'while using deploy tokens' do
- let(:project) { create(:project) }
let(:auth_failure) { Gitlab::Auth::Result.new(nil, nil) }
context 'when deploy token and user have the same username' do
@@ -316,7 +313,7 @@ describe Gitlab::Auth do
end
it 'succeeds for the user' do
- auth_success = Gitlab::Auth::Result.new(user, nil, :gitlab_or_ldap, full_authentication_abilities)
+ auth_success = Gitlab::Auth::Result.new(user, nil, :gitlab_or_ldap, described_class.full_authentication_abilities)
expect(gl_auth.find_for_git_client(username, 'my-secret', project: project, ip: 'ip'))
.to eq(auth_success)
@@ -344,7 +341,7 @@ describe Gitlab::Auth do
end
context 'and belong to different projects' do
- let!(:read_registry) { create(:deploy_token, username: 'deployer', read_repository: false, projects: [create(:project)]) }
+ let!(:read_registry) { create(:deploy_token, username: 'deployer', read_repository: false, projects: [project]) }
let!(:read_repository) { create(:deploy_token, username: read_registry.username, read_registry: false, projects: [project]) }
it 'succeeds for the right token' do
@@ -523,6 +520,12 @@ describe Gitlab::Auth do
end
end
+ it 'finds the user in deactivated state' do
+ user.deactivate!
+
+ expect( gl_auth.find_with_user_password(username, password) ).to eql user
+ end
+
it "does not find user in blocked state" do
user.block
@@ -582,37 +585,6 @@ describe Gitlab::Auth do
private
- def build_authentication_abilities
- [
- :read_project,
- :build_download_code,
- :build_read_container_image,
- :build_create_container_image,
- :build_destroy_container_image
- ]
- end
-
- def read_only_authentication_abilities
- [
- :read_project,
- :download_code,
- :read_container_image
- ]
- end
-
- def read_write_authentication_abilities
- read_only_authentication_abilities + [
- :push_code,
- :create_container_image
- ]
- end
-
- def full_authentication_abilities
- read_write_authentication_abilities + [
- :admin_container_image
- ]
- end
-
def expect_results_with_abilities(personal_access_token, abilities, success = true)
expect(gl_auth).to receive(:rate_limit!).with('ip', success: success, login: '')
expect(gl_auth.find_for_git_client('', personal_access_token&.token, project: nil, ip: 'ip'))
diff --git a/spec/lib/gitlab/background_migration/legacy_upload_mover_spec.rb b/spec/lib/gitlab/background_migration/legacy_upload_mover_spec.rb
index 7d67dc0251d..c1eaf1d3433 100644
--- a/spec/lib/gitlab/background_migration/legacy_upload_mover_spec.rb
+++ b/spec/lib/gitlab/background_migration/legacy_upload_mover_spec.rb
@@ -32,7 +32,7 @@ describe Gitlab::BackgroundMigration::LegacyUploadMover do
if with_file
upload = create(:upload, :with_file, :attachment_upload, params)
- model.update(attachment: upload.build_uploader)
+ model.update(attachment: upload.retrieve_uploader)
model.attachment.upload
else
create(:upload, :attachment_upload, params)
diff --git a/spec/lib/gitlab/background_migration/legacy_uploads_migrator_spec.rb b/spec/lib/gitlab/background_migration/legacy_uploads_migrator_spec.rb
index ed8cbfeb11f..cabca3dbef9 100644
--- a/spec/lib/gitlab/background_migration/legacy_uploads_migrator_spec.rb
+++ b/spec/lib/gitlab/background_migration/legacy_uploads_migrator_spec.rb
@@ -24,7 +24,7 @@ describe Gitlab::BackgroundMigration::LegacyUploadsMigrator do
if with_file
upload = create(:upload, :with_file, :attachment_upload, params)
- model.update(attachment: upload.build_uploader)
+ model.update(attachment: upload.retrieve_uploader)
model.attachment.upload
else
create(:upload, :attachment_upload, params)
diff --git a/spec/lib/gitlab/background_migration/migrate_pages_metadata_spec.rb b/spec/lib/gitlab/background_migration/migrate_pages_metadata_spec.rb
new file mode 100644
index 00000000000..d94a312f605
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/migrate_pages_metadata_spec.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::BackgroundMigration::MigratePagesMetadata, :migration, schema: 20190919040324 do
+ let(:projects) { table(:projects) }
+
+ subject(:migrate_pages_metadata) { described_class.new }
+
+ describe '#perform_on_relation' do
+ let(:namespaces) { table(:namespaces) }
+ let(:builds) { table(:ci_builds) }
+ let(:pages_metadata) { table(:project_pages_metadata) }
+
+ it 'marks specified projects with successful pages deployment' do
+ namespace = namespaces.create!(name: 'gitlab', path: 'gitlab-org')
+ not_migrated_with_pages = projects.create!(namespace_id: namespace.id, name: 'Not Migrated With Pages')
+ builds.create!(project_id: not_migrated_with_pages.id, type: 'GenericCommitStatus', status: 'success', stage: 'deploy', name: 'pages:deploy')
+
+ migrated = projects.create!(namespace_id: namespace.id, name: 'Migrated')
+ pages_metadata.create!(project_id: migrated.id, deployed: true)
+
+ not_migrated_no_pages = projects.create!(namespace_id: namespace.id, name: 'Not Migrated No Pages')
+ project_not_in_relation_scope = projects.create!(namespace_id: namespace.id, name: 'Other')
+
+ projects_relation = projects.where(id: [not_migrated_with_pages, not_migrated_no_pages, migrated])
+
+ migrate_pages_metadata.perform_on_relation(projects_relation)
+
+ expect(pages_metadata.find_by_project_id(not_migrated_with_pages.id).deployed).to eq(true)
+ expect(pages_metadata.find_by_project_id(not_migrated_no_pages.id).deployed).to eq(false)
+ expect(pages_metadata.find_by_project_id(migrated.id).deployed).to eq(true)
+ expect(pages_metadata.find_by_project_id(project_not_in_relation_scope.id)).to be_nil
+ end
+ end
+
+ describe '#perform' do
+ it 'creates relation and delegates to #perform_on_relation' do
+ expect(migrate_pages_metadata).to receive(:perform_on_relation).with(projects.where(id: 3..5))
+
+ migrate_pages_metadata.perform(3, 5)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/badge/pipeline/template_spec.rb b/spec/lib/gitlab/badge/pipeline/template_spec.rb
index c0aaa3d73e1..da95c7219a4 100644
--- a/spec/lib/gitlab/badge/pipeline/template_spec.rb
+++ b/spec/lib/gitlab/badge/pipeline/template_spec.rb
@@ -67,7 +67,7 @@ describe Gitlab::Badge::Pipeline::Template do
end
it 'has expected color' do
- expect(template.value_color).to eq '#dfb317'
+ expect(template.value_color).to eq '#a7a7a7'
end
end
diff --git a/spec/lib/gitlab/bare_repository_import/importer_spec.rb b/spec/lib/gitlab/bare_repository_import/importer_spec.rb
index 0c1eedad7f4..2fb9f1a0a08 100644
--- a/spec/lib/gitlab/bare_repository_import/importer_spec.rb
+++ b/spec/lib/gitlab/bare_repository_import/importer_spec.rb
@@ -89,7 +89,7 @@ describe Gitlab::BareRepositoryImport::Importer, :seed_helper do
repo_path = "#{project.disk_path}.git"
hook_path = File.join(repo_path, 'hooks')
- expect(gitlab_shell.exists?(project.repository_storage, repo_path)).to be(true)
+ expect(gitlab_shell.repository_exists?(project.repository_storage, repo_path)).to be(true)
expect(gitlab_shell.exists?(project.repository_storage, hook_path)).to be(true)
end
@@ -145,8 +145,8 @@ describe Gitlab::BareRepositoryImport::Importer, :seed_helper do
project = Project.find_by_full_path("#{admin.full_path}/#{project_path}")
- expect(gitlab_shell.exists?(project.repository_storage, project.disk_path + '.git')).to be(true)
- expect(gitlab_shell.exists?(project.repository_storage, project.disk_path + '.wiki.git')).to be(true)
+ expect(gitlab_shell.repository_exists?(project.repository_storage, project.disk_path + '.git')).to be(true)
+ expect(gitlab_shell.repository_exists?(project.repository_storage, project.disk_path + '.wiki.git')).to be(true)
end
context 'with a repository already on disk' do
@@ -186,7 +186,7 @@ describe Gitlab::BareRepositoryImport::Importer, :seed_helper do
project = Project.find_by_full_path(project_path)
- expect(gitlab_shell.exists?(project.repository_storage, project.disk_path + '.wiki.git')).to be(true)
+ expect(gitlab_shell.repository_exists?(project.repository_storage, project.disk_path + '.wiki.git')).to be(true)
end
end
diff --git a/spec/lib/gitlab/bitbucket_import/importer_spec.rb b/spec/lib/gitlab/bitbucket_import/importer_spec.rb
index 3d0d3f91859..7f7a285c453 100644
--- a/spec/lib/gitlab/bitbucket_import/importer_spec.rb
+++ b/spec/lib/gitlab/bitbucket_import/importer_spec.rb
@@ -308,8 +308,8 @@ describe Gitlab::BitbucketImport::Importer do
importer.execute
- expect(project.issues.where(state: "closed").size).to eq(5)
- expect(project.issues.where(state: "opened").size).to eq(2)
+ expect(project.issues.where(state_id: Issue.available_states[:closed]).size).to eq(5)
+ expect(project.issues.where(state_id: Issue.available_states[:opened]).size).to eq(2)
end
describe 'wiki import' do
diff --git a/spec/lib/gitlab/ci/ansi2json/line_spec.rb b/spec/lib/gitlab/ci/ansi2json/line_spec.rb
new file mode 100644
index 00000000000..4b5c3f9489e
--- /dev/null
+++ b/spec/lib/gitlab/ci/ansi2json/line_spec.rb
@@ -0,0 +1,168 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Ci::Ansi2json::Line do
+ let(:offset) { 0 }
+ let(:style) { Gitlab::Ci::Ansi2json::Style.new }
+
+ subject { described_class.new(offset: offset, style: style) }
+
+ describe '#<<' do
+ it 'appends new data to the current segment' do
+ expect { subject << 'test 1' }.to change { subject.current_segment.text }
+ expect(subject.current_segment.text).to eq('test 1')
+
+ expect { subject << ', test 2' }.to change { subject.current_segment.text }
+ expect(subject.current_segment.text).to eq('test 1, test 2')
+ end
+ end
+
+ describe '#style' do
+ context 'when style is passed to the initializer' do
+ let(:style) { double }
+
+ it 'returns the same style' do
+ expect(subject.style).to eq(style)
+ end
+ end
+
+ context 'when style is not passed to the initializer' do
+ it 'returns the default style' do
+ expect(subject.style.set?).to be_falsey
+ end
+ end
+ end
+
+ describe '#update_style' do
+ let(:expected_style) do
+ Gitlab::Ci::Ansi2json::Style.new(
+ fg: 'term-fg-l-yellow',
+ bg: 'term-bg-blue',
+ mask: 1)
+ end
+
+ it 'sets the style' do
+ subject.update_style(%w[1 33 44])
+
+ expect(subject.style).to eq(expected_style)
+ end
+ end
+
+ describe '#add_section' do
+ it 'appends a new section to the list' do
+ subject.add_section('section_1')
+ subject.add_section('section_2')
+
+ expect(subject.sections).to eq(%w[section_1 section_2])
+ end
+ end
+
+ describe '#set_as_section_header' do
+ it 'change the section_header to true' do
+ expect { subject.set_as_section_header }
+ .to change { subject.section_header }
+ .to be_truthy
+ end
+ end
+
+ describe '#set_section_duration' do
+ it 'sets and formats the section_duration' do
+ subject.set_section_duration(75)
+
+ expect(subject.section_duration).to eq('01:15')
+ end
+ end
+
+ describe '#flush_current_segment!' do
+ context 'when current segment is not empty' do
+ before do
+ subject << 'some data'
+ end
+
+ it 'adds the segment to the list' do
+ expect { subject.flush_current_segment! }.to change { subject.segments.count }.by(1)
+
+ expect(subject.segments.map { |s| s[:text] }).to eq(['some data'])
+ end
+
+ it 'updates the current segment pointer propagating the style' do
+ previous_segment = subject.current_segment
+
+ subject.flush_current_segment!
+
+ expect(subject.current_segment).not_to eq(previous_segment)
+ expect(subject.current_segment.style).to eq(previous_segment.style)
+ end
+ end
+
+ context 'when current segment is empty' do
+ it 'does not add any segments to the list' do
+ expect { subject.flush_current_segment! }.not_to change { subject.segments.count }
+ end
+
+ it 'does not change the current segment' do
+ expect { subject.flush_current_segment! }.not_to change { subject.current_segment }
+ end
+ end
+ end
+
+ describe '#to_h' do
+ before do
+ subject << 'some data'
+ subject.update_style(['1'])
+ end
+
+ context 'when sections are present' do
+ before do
+ subject.add_section('section_1')
+ subject.add_section('section_2')
+ end
+
+ context 'when section header is set' do
+ before do
+ subject.set_as_section_header
+ end
+
+ it 'serializes the attributes set' do
+ result = {
+ offset: 0,
+ content: [{ text: 'some data', style: 'term-bold' }],
+ section: 'section_2',
+ section_header: true
+ }
+
+ expect(subject.to_h).to eq(result)
+ end
+ end
+
+ context 'when section duration is set' do
+ before do
+ subject.set_section_duration(75)
+ end
+
+ it 'serializes the attributes set' do
+ result = {
+ offset: 0,
+ content: [{ text: 'some data', style: 'term-bold' }],
+ section: 'section_2',
+ section_duration: '01:15'
+ }
+
+ expect(subject.to_h).to eq(result)
+ end
+ end
+ end
+
+ context 'when there are no sections' do
+ it 'serializes the attributes set' do
+ result = {
+ offset: 0,
+ content: [{ text: 'some data', style: 'term-bold' }]
+ }
+
+ expect(subject.to_h).to eq(result)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/ansi2json/parser_spec.rb b/spec/lib/gitlab/ci/ansi2json/parser_spec.rb
new file mode 100644
index 00000000000..e161e74c1ff
--- /dev/null
+++ b/spec/lib/gitlab/ci/ansi2json/parser_spec.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+# The rest of the specs for this class are covered in style_spec.rb
+describe Gitlab::Ci::Ansi2json::Parser do
+ subject { described_class }
+
+ describe 'bold?' do
+ it 'returns true if style mask matches bold format' do
+ expect(subject.bold?(0x01)).to be_truthy
+ end
+
+ it 'returns false if style mask does not match bold format' do
+ expect(subject.bold?(0x02)).to be_falsey
+ end
+ end
+
+ describe 'matching_formats' do
+ it 'returns matching formats given a style mask' do
+ expect(subject.matching_formats(0x01)).to eq(%w[term-bold])
+ expect(subject.matching_formats(0x03)).to eq(%w[term-bold term-italic])
+ expect(subject.matching_formats(0x07)).to eq(%w[term-bold term-italic term-underline])
+ end
+
+ it 'returns an empty array if no formats match the style mask' do
+ expect(subject.matching_formats(0)).to eq([])
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/ansi2json/style_spec.rb b/spec/lib/gitlab/ci/ansi2json/style_spec.rb
new file mode 100644
index 00000000000..88a0ca35859
--- /dev/null
+++ b/spec/lib/gitlab/ci/ansi2json/style_spec.rb
@@ -0,0 +1,166 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Ci::Ansi2json::Style do
+ describe '#set?' do
+ subject { described_class.new(params).set? }
+
+ context 'when fg color is set' do
+ let(:params) { { fg: 'term-fg-black' } }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when bg color is set' do
+ let(:params) { { bg: 'term-bg-black' } }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when mask is set' do
+ let(:params) { { mask: 0x01 } }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'nothing is set' do
+ let(:params) { {} }
+
+ it { is_expected.to be_falsey }
+ end
+ end
+
+ describe '#reset!' do
+ let(:style) { described_class.new(fg: 'term-fg-black', bg: 'term-bg-yellow', mask: 0x01) }
+
+ it 'set the style params to default' do
+ style.reset!
+
+ expect(style.fg).to be_nil
+ expect(style.bg).to be_nil
+ expect(style.mask).to be_zero
+ end
+ end
+
+ describe 'update formats to mimic terminals' do
+ subject { described_class.new(params) }
+
+ context 'when fg color present' do
+ let(:params) { { fg: 'term-fg-black', mask: mask } }
+
+ context 'when mask is set to bold' do
+ let(:mask) { 0x01 }
+
+ it 'changes the fg color to a lighter version' do
+ expect(subject.fg).to eq('term-fg-l-black')
+ end
+ end
+
+ context 'when mask set to another format' do
+ let(:mask) { 0x02 }
+
+ it 'does not change the fg color' do
+ expect(subject.fg).to eq('term-fg-black')
+ end
+ end
+
+ context 'when mask is not set' do
+ let(:mask) { 0 }
+
+ it 'does not change the fg color' do
+ expect(subject.fg).to eq('term-fg-black')
+ end
+ end
+ end
+ end
+
+ describe '#update' do
+ where(:initial_state, :ansi_commands, :result, :description) do
+ [
+ # add format
+ [[], %w[0], '', 'does not set any style'],
+ [[], %w[1], 'term-bold', 'enables format bold'],
+ [[], %w[3], 'term-italic', 'enables format italic'],
+ [[], %w[4], 'term-underline', 'enables format underline'],
+ [[], %w[8], 'term-conceal', 'enables format conceal'],
+ [[], %w[9], 'term-cross', 'enables format cross'],
+ # remove format
+ [%w[1], %w[21], '', 'disables format bold'],
+ [%w[1 3], %w[21], 'term-italic', 'disables format bold and leaves italic'],
+ [%w[1], %w[22], '', 'disables format bold using command 22'],
+ [%w[1 3], %w[22], 'term-italic', 'disables format bold and leaves italic using command 22'],
+ [%w[3], %w[23], '', 'disables format italic'],
+ [%w[1 3], %w[23], 'term-bold', 'disables format italic and leaves bold'],
+ [%w[4], %w[24], '', 'disables format underline'],
+ [%w[1 4], %w[24], 'term-bold', 'disables format underline and leaves bold'],
+ [%w[8], %w[28], '', 'disables format conceal'],
+ [%w[1 8], %w[28], 'term-bold', 'disables format conceal and leaves bold'],
+ [%w[9], %w[29], '', 'disables format cross'],
+ [%w[1 9], %w[29], 'term-bold', 'disables format cross and leaves bold'],
+ # set fg color
+ [[], %w[30], 'term-fg-black', 'sets fg color black'],
+ [[], %w[31], 'term-fg-red', 'sets fg color red'],
+ [[], %w[32], 'term-fg-green', 'sets fg color green'],
+ [[], %w[33], 'term-fg-yellow', 'sets fg color yellow'],
+ [[], %w[34], 'term-fg-blue', 'sets fg color blue'],
+ [[], %w[35], 'term-fg-magenta', 'sets fg color magenta'],
+ [[], %w[36], 'term-fg-cyan', 'sets fg color cyan'],
+ [[], %w[37], 'term-fg-white', 'sets fg color white'],
+ # sets xterm fg color
+ [[], %w[38 5 1], 'xterm-fg-1', 'sets xterm fg color 1'],
+ [[], %w[38 5 2], 'xterm-fg-2', 'sets xterm fg color 2'],
+ [[], %w[38 1], 'term-bold', 'ignores 38 command if not followed by 5 and sets format bold'],
+ # set bg color
+ [[], %w[40], 'term-bg-black', 'sets bg color black'],
+ [[], %w[41], 'term-bg-red', 'sets bg color red'],
+ [[], %w[42], 'term-bg-green', 'sets bg color green'],
+ [[], %w[43], 'term-bg-yellow', 'sets bg color yellow'],
+ [[], %w[44], 'term-bg-blue', 'sets bg color blue'],
+ [[], %w[45], 'term-bg-magenta', 'sets bg color magenta'],
+ [[], %w[46], 'term-bg-cyan', 'sets bg color cyan'],
+ [[], %w[47], 'term-bg-white', 'sets bg color white'],
+ # set xterm bg color
+ [[], %w[48 5 1], 'xterm-bg-1', 'sets xterm bg color 1'],
+ [[], %w[48 5 2], 'xterm-bg-2', 'sets xterm bg color 2'],
+ [[], %w[48 1], 'term-bold', 'ignores 48 command if not followed by 5 and sets format bold'],
+ # set light fg color
+ [[], %w[90], 'term-fg-l-black', 'sets fg color light black'],
+ [[], %w[91], 'term-fg-l-red', 'sets fg color light red'],
+ [[], %w[92], 'term-fg-l-green', 'sets fg color light green'],
+ [[], %w[93], 'term-fg-l-yellow', 'sets fg color light yellow'],
+ [[], %w[94], 'term-fg-l-blue', 'sets fg color light blue'],
+ [[], %w[95], 'term-fg-l-magenta', 'sets fg color light magenta'],
+ [[], %w[96], 'term-fg-l-cyan', 'sets fg color light cyan'],
+ [[], %w[97], 'term-fg-l-white', 'sets fg color light white'],
+ # set light bg color
+ [[], %w[100], 'term-bg-l-black', 'sets bg color light black'],
+ [[], %w[101], 'term-bg-l-red', 'sets bg color light red'],
+ [[], %w[102], 'term-bg-l-green', 'sets bg color light green'],
+ [[], %w[103], 'term-bg-l-yellow', 'sets bg color light yellow'],
+ [[], %w[104], 'term-bg-l-blue', 'sets bg color light blue'],
+ [[], %w[105], 'term-bg-l-magenta', 'sets bg color light magenta'],
+ [[], %w[106], 'term-bg-l-cyan', 'sets bg color light cyan'],
+ [[], %w[107], 'term-bg-l-white', 'sets bg color light white'],
+ # reset
+ [%w[1], %w[0], '', 'resets style from format bold'],
+ [%w[1 3], %w[0], '', 'resets style from format bold and italic'],
+ [%w[1 3 term-fg-l-red term-bg-yellow], %w[0], '', 'resets all formats and colors'],
+ # misc
+ [[], %w[1 30 42 3], 'term-fg-l-black term-bg-green term-bold term-italic', 'adds fg color, bg color and formats from no style'],
+ [%w[3 31], %w[23 1 43], 'term-fg-l-red term-bg-yellow term-bold', 'replaces format italic with bold and adds a yellow background']
+ ]
+ end
+
+ with_them do
+ it 'change the style' do
+ style = described_class.new
+ style.update(initial_state)
+
+ style.update(ansi_commands)
+
+ expect(style.to_s).to eq(result)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/ansi2json_spec.rb b/spec/lib/gitlab/ci/ansi2json_spec.rb
new file mode 100644
index 00000000000..3c6bc46436b
--- /dev/null
+++ b/spec/lib/gitlab/ci/ansi2json_spec.rb
@@ -0,0 +1,544 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Ci::Ansi2json do
+ subject { described_class }
+
+ describe 'lines' do
+ it 'prints non-ansi as-is' do
+ expect(convert_json('Hello')).to eq([
+ { offset: 0, content: [{ text: 'Hello' }] }
+ ])
+ end
+
+ it 'adds new line in a separate element' do
+ expect(convert_json("Hello\nworld")).to eq([
+ { offset: 0, content: [{ text: 'Hello' }] },
+ { offset: 6, content: [{ text: 'world' }] }
+ ])
+ end
+
+ it 'recognizes color changing ANSI sequences' do
+ expect(convert_json("\e[31mHello\e[0m")).to eq([
+ { offset: 0, content: [{ text: 'Hello', style: 'term-fg-red' }] }
+ ])
+ end
+
+ it 'recognizes color changing ANSI sequences across multiple lines' do
+ expect(convert_json("\e[31mHello\nWorld\e[0m")).to eq([
+ { offset: 0, content: [{ text: 'Hello', style: 'term-fg-red' }] },
+ { offset: 11, content: [{ text: 'World', style: 'term-fg-red' }] }
+ ])
+ end
+
+ it 'recognizes background and foreground colors' do
+ expect(convert_json("\e[31;44mHello")).to eq([
+ { offset: 0, content: [{ text: 'Hello', style: 'term-fg-red term-bg-blue' }] }
+ ])
+ end
+
+ it 'recognizes style changes within the same line' do
+ expect(convert_json("\e[31;44mHello\e[0m world")).to eq([
+ { offset: 0, content: [
+ { text: 'Hello', style: 'term-fg-red term-bg-blue' },
+ { text: ' world' }
+ ] }
+ ])
+ end
+
+ context 'with section markers' do
+ let(:section_name) { 'prepare-script' }
+ let(:section_duration) { 63.seconds }
+ let(:section_start_time) { Time.new(2019, 9, 17).utc }
+ let(:section_end_time) { section_start_time + section_duration }
+ let(:section_start) { "section_start:#{section_start_time.to_i}:#{section_name}\r\033[0K"}
+ let(:section_end) { "section_end:#{section_end_time.to_i}:#{section_name}\r\033[0K"}
+
+ it 'marks the first line of the section as header' do
+ expect(convert_json("Hello#{section_start}world!")).to eq([
+ {
+ offset: 0,
+ content: [{ text: 'Hello' }]
+ },
+ {
+ offset: 5,
+ content: [{ text: 'world!' }],
+ section: 'prepare-script',
+ section_header: true
+ }
+ ])
+ end
+
+ it 'does not marks the other lines of the section as header' do
+ expect(convert_json("outside section#{section_start}Hello\nworld!")).to eq([
+ {
+ offset: 0,
+ content: [{ text: 'outside section' }]
+ },
+ {
+ offset: 15,
+ content: [{ text: 'Hello' }],
+ section: 'prepare-script',
+ section_header: true
+ },
+ {
+ offset: 65,
+ content: [{ text: 'world!' }],
+ section: 'prepare-script'
+ }
+ ])
+ end
+
+ it 'marks the last line of the section as footer' do
+ expect(convert_json("#{section_start}Good\nmorning\nworld!#{section_end}")).to eq([
+ {
+ offset: 0,
+ content: [{ text: 'Good' }],
+ section: 'prepare-script',
+ section_header: true
+ },
+ {
+ offset: 49,
+ content: [{ text: 'morning' }],
+ section: 'prepare-script'
+ },
+ {
+ offset: 57,
+ content: [{ text: 'world!' }],
+ section: 'prepare-script'
+ },
+ {
+ offset: 63,
+ content: [],
+ section_duration: '01:03',
+ section: 'prepare-script'
+ },
+ {
+ offset: 63,
+ content: []
+ }
+ ])
+ end
+
+ it 'marks the first line as header and footer if is the only line in the section' do
+ expect(convert_json("#{section_start}Hello world!#{section_end}")).to eq([
+ {
+ offset: 0,
+ content: [{ text: 'Hello world!' }],
+ section: 'prepare-script',
+ section_header: true
+ },
+ {
+ offset: 56,
+ content: [],
+ section: 'prepare-script',
+ section_duration: '01:03'
+ },
+ {
+ offset: 56,
+ content: []
+ }
+ ])
+ end
+
+ it 'does not add sections attribute to lines after the section is closed' do
+ expect(convert_json("#{section_start}Hello#{section_end}world")).to eq([
+ {
+ offset: 0,
+ content: [{ text: 'Hello' }],
+ section: 'prepare-script',
+ section_header: true
+ },
+ {
+ offset: 49,
+ content: [],
+ section: 'prepare-script',
+ section_duration: '01:03'
+ },
+ {
+ offset: 49,
+ content: [{ text: 'world' }]
+ }
+ ])
+ end
+
+ it 'ignores section_end marker if no section_start exists' do
+ expect(convert_json("Hello #{section_end}world")).to eq([
+ {
+ offset: 0,
+ content: [{ text: 'Hello world' }]
+ }
+ ])
+ end
+
+ context 'when section name contains .-_ and capital letters' do
+ let(:section_name) { 'a.Legit-SeCtIoN_namE' }
+
+ it 'sanitizes the section name' do
+ expect(convert_json("Hello#{section_start}world!")).to eq([
+ {
+ offset: 0,
+ content: [{ text: 'Hello' }]
+ },
+ {
+ offset: 5,
+ content: [{ text: 'world!' }],
+ section: 'a-legit-section-name',
+ section_header: true
+ }
+ ])
+ end
+ end
+
+ context 'when section name includes $' do
+ let(:section_name) { 'my_$ection' }
+
+ it 'ignores the section' do
+ expect(convert_json("#{section_start}hello")).to eq([
+ {
+ offset: 0,
+ content: [{ text: "#{section_start.gsub("\033[0K", '')}hello" }]
+ }
+ ])
+ end
+ end
+
+ context 'when section name includes <' do
+ let(:section_name) { '<a_tag>' }
+
+ it 'ignores the section' do
+ expect(convert_json("#{section_start}hello")).to eq([
+ {
+ offset: 0,
+ content: [{ text: "#{section_start.gsub("\033[0K", '').gsub('<', '&lt;')}hello" }]
+ }
+ ])
+ end
+ end
+
+ it 'prevents XSS injection' do
+ trace = "#{section_start}section_end:1:2<script>alert('XSS Hack!');</script>#{section_end}"
+ expect(convert_json(trace)).to eq([
+ {
+ offset: 0,
+ content: [{ text: "section_end:1:2&lt;script>alert('XSS Hack!');&lt;/script>" }],
+ section: 'prepare-script',
+ section_header: true
+ },
+ {
+ offset: 95,
+ content: [],
+ section: 'prepare-script',
+ section_duration: '01:03'
+ },
+ {
+ offset: 95,
+ content: []
+ }
+ ])
+ end
+
+ context 'with nested section' do
+ let(:nested_section_name) { 'prepare-script-nested' }
+ let(:nested_section_duration) { 2.seconds }
+ let(:nested_section_start_time) { Time.new(2019, 9, 17).utc }
+ let(:nested_section_end_time) { nested_section_start_time + nested_section_duration }
+ let(:nested_section_start) { "section_start:#{nested_section_start_time.to_i}:#{nested_section_name}\r\033[0K"}
+ let(:nested_section_end) { "section_end:#{nested_section_end_time.to_i}:#{nested_section_name}\r\033[0K"}
+
+ it 'adds multiple sections to the lines inside the nested section' do
+ trace = "Hello#{section_start}foo#{nested_section_start}bar#{nested_section_end}baz#{section_end}world"
+
+ expect(convert_json(trace)).to eq([
+ {
+ offset: 0,
+ content: [{ text: 'Hello' }]
+ },
+ {
+ offset: 5,
+ content: [{ text: 'foo' }],
+ section: 'prepare-script',
+ section_header: true
+ },
+ {
+ offset: 52,
+ content: [{ text: 'bar' }],
+ section: 'prepare-script-nested',
+ section_header: true
+ },
+ {
+ offset: 106,
+ content: [],
+ section: 'prepare-script-nested',
+ section_duration: '00:02'
+ },
+ {
+ offset: 106,
+ content: [{ text: 'baz' }],
+ section: 'prepare-script'
+ },
+ {
+ offset: 158,
+ content: [],
+ section: 'prepare-script',
+ section_duration: '01:03'
+ },
+ {
+ offset: 158,
+ content: [{ text: 'world' }]
+ }
+ ])
+ end
+
+ it 'adds multiple sections to the lines inside the nested section and closes all sections together' do
+ trace = "Hello#{section_start}\e[91mfoo\e[0m#{nested_section_start}bar#{nested_section_end}#{section_end}"
+
+ expect(convert_json(trace)).to eq([
+ {
+ offset: 0,
+ content: [{ text: 'Hello' }]
+ },
+ {
+ offset: 5,
+ content: [{ text: 'foo', style: 'term-fg-l-red' }],
+ section: 'prepare-script',
+ section_header: true
+ },
+ {
+ offset: 61,
+ content: [{ text: 'bar' }],
+ section: 'prepare-script-nested',
+ section_header: true
+ },
+ {
+ offset: 115,
+ content: [],
+ section: 'prepare-script-nested',
+ section_duration: '00:02'
+ },
+ {
+ offset: 115,
+ content: [],
+ section: 'prepare-script',
+ section_duration: '01:03'
+ },
+ {
+ offset: 164,
+ content: []
+ }
+ ])
+ end
+ end
+ end
+
+ describe 'incremental updates' do
+ let(:pass1_stream) { StringIO.new(pre_text) }
+ let(:pass2_stream) { StringIO.new(pre_text + text) }
+ let(:pass1) { subject.convert(pass1_stream) }
+ let(:pass2) { subject.convert(pass2_stream, pass1.state) }
+
+ context 'with split word' do
+ let(:pre_text) { "\e[1mHello " }
+ let(:text) { "World" }
+
+ let(:lines) do
+ [
+ { offset: 0, content: [{ text: 'Hello World', style: 'term-bold' }] }
+ ]
+ end
+
+ it 'returns the full line' do
+ expect(pass2.lines).to eq(lines)
+ expect(pass2.append).to be_falsey
+ end
+ end
+
+ context 'with split word on second line' do
+ let(:pre_text) { "Good\nmorning " }
+ let(:text) { "World" }
+
+ let(:lines) do
+ [
+ { offset: 5, content: [{ text: 'morning World' }] }
+ ]
+ end
+
+ it 'returns all lines since last partially processed line' do
+ expect(pass2.lines).to eq(lines)
+ expect(pass2.append).to be_truthy
+ end
+ end
+
+ context 'with split sequence across multiple lines' do
+ let(:pre_text) { "\e[1mgood\nmorning\n" }
+ let(:text) { "\e[3mworld" }
+
+ let(:lines) do
+ [
+ { offset: 17, content: [{ text: 'world', style: 'term-bold term-italic' }] }
+ ]
+ end
+
+ it 'returns the full line' do
+ expect(pass2.lines).to eq(lines)
+ expect(pass2.append).to be_truthy
+ end
+ end
+
+ context 'with split partial sequence' do
+ let(:pre_text) { "hello\e" }
+ let(:text) { "[1m world" }
+
+ let(:lines) do
+ [
+ { offset: 0, content: [
+ { text: 'hello' },
+ { text: ' world', style: 'term-bold' }
+ ] }
+ ]
+ end
+
+ it 'returns the full line' do
+ expect(pass2.lines).to eq(lines)
+ expect(pass2.append).to be_falsey
+ end
+ end
+
+ context 'with split new line' do
+ let(:pre_text) { "hello\r" }
+ let(:text) { "\nworld" }
+
+ let(:lines) do
+ [
+ { offset: 0, content: [{ text: 'hello' }] },
+ { offset: 7, content: [{ text: 'world' }] }
+ ]
+ end
+
+ it 'returns the full line' do
+ expect(pass2.lines).to eq(lines)
+ expect(pass2.append).to be_falsey
+ end
+ end
+
+ context 'with split section' do
+ let(:section_name) { 'prepare-script' }
+ let(:section_duration) { 63.seconds }
+ let(:section_start_time) { Time.new(2019, 9, 17).utc }
+ let(:section_end_time) { section_start_time + section_duration }
+ let(:section_start) { "section_start:#{section_start_time.to_i}:#{section_name}\r\033[0K"}
+ let(:section_end) { "section_end:#{section_end_time.to_i}:#{section_name}\r\033[0K"}
+
+ context 'with split section body' do
+ let(:pre_text) { "#{section_start}this is a header\nand " }
+ let(:text) { "this\n is a body" }
+
+ let(:lines) do
+ [
+ {
+ offset: 61,
+ content: [{ text: 'and this' }],
+ section: 'prepare-script'
+ },
+ {
+ offset: 70,
+ content: [{ text: ' is a body' }],
+ section: 'prepare-script'
+ }
+ ]
+ end
+
+ it 'returns the full line' do
+ expect(pass2.lines).to eq(lines)
+ expect(pass2.append).to be_truthy
+ end
+ end
+
+ context 'with split section where header is also split' do
+ let(:pre_text) { "#{section_start}this is " }
+ let(:text) { "a header\nand body" }
+
+ let(:lines) do
+ [
+ {
+ offset: 0,
+ content: [{ text: 'this is a header' }],
+ section: 'prepare-script',
+ section_header: true
+ },
+ {
+ offset: 61,
+ content: [{ text: 'and body' }],
+ section: 'prepare-script'
+ }
+ ]
+ end
+
+ it 'returns the full line' do
+ expect(pass2.lines).to eq(lines)
+ expect(pass2.append).to be_falsey
+ end
+ end
+
+ context 'with split section end' do
+ let(:pre_text) { "#{section_start}this is a header\nthe" }
+ let(:text) { " body\nthe end#{section_end}" }
+
+ let(:lines) do
+ [
+ {
+ offset: 61,
+ content: [{ text: 'the body' }],
+ section: 'prepare-script'
+ },
+ {
+ offset: 70,
+ content: [{ text: 'the end' }],
+ section: 'prepare-script'
+ },
+ {
+ offset: 77,
+ content: [],
+ section: 'prepare-script',
+ section_duration: '01:03'
+ },
+ {
+ offset: 77,
+ content: []
+ }
+ ]
+ end
+
+ it 'returns the full line' do
+ expect(pass2.lines).to eq(lines)
+ expect(pass2.append).to be_truthy
+ end
+ end
+ end
+ end
+
+ describe 'trucates' do
+ let(:text) { "Hello World" }
+ let(:stream) { StringIO.new(text) }
+ let(:subject) { described_class.convert(stream) }
+
+ before do
+ stream.seek(3, IO::SEEK_SET)
+ end
+
+ it "returns truncated output" do
+ expect(subject.truncated).to be_truthy
+ end
+
+ it "does not append output" do
+ expect(subject.append).to be_falsey
+ end
+ end
+
+ def convert_json(data)
+ stream = StringIO.new(data)
+ subject.convert(stream).lines
+ 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 24d17eb0fb3..73c3cad88bc 100644
--- a/spec/lib/gitlab/ci/build/artifacts/metadata/entry_spec.rb
+++ b/spec/lib/gitlab/ci/build/artifacts/metadata/entry_spec.rb
@@ -34,27 +34,32 @@ describe Gitlab::Ci::Build::Artifacts::Metadata::Entry do
describe '#basename' do
subject { |example| path(example).basename }
+
it { is_expected.to eq 'absolute_path' }
end
end
describe 'path/dir_1/', path: 'path/dir_1/' do
subject { |example| path(example) }
+
it { is_expected.to have_parent }
it { is_expected.to be_directory }
describe '#basename' do
subject { |example| path(example).basename }
+
it { is_expected.to eq 'dir_1/' }
end
describe '#name' do
subject { |example| path(example).name }
+
it { is_expected.to eq 'dir_1' }
end
describe '#parent' do
subject { |example| path(example).parent }
+
it { is_expected.to eq entry('path/') }
end
@@ -102,21 +107,25 @@ describe Gitlab::Ci::Build::Artifacts::Metadata::Entry do
describe '#nodes' do
subject { |example| path(example).nodes }
+
it { is_expected.to eq 2 }
end
describe '#exists?' do
subject { |example| path(example).exists? }
+
it { is_expected.to be true }
end
describe '#empty?' do
subject { |example| path(example).empty? }
+
it { is_expected.to be false }
end
describe '#total_size' do
subject { |example| path(example).total_size }
+
it { is_expected.to eq(30) }
end
end
@@ -124,10 +133,12 @@ describe Gitlab::Ci::Build::Artifacts::Metadata::Entry do
describe 'empty path', path: '' do
subject { |example| path(example) }
+
it { is_expected.not_to have_parent }
describe '#children' do
subject { |example| path(example).children }
+
it { expect(subject.count).to eq 3 }
end
end
@@ -135,6 +146,7 @@ describe Gitlab::Ci::Build::Artifacts::Metadata::Entry do
describe 'path/dir_1/subdir/subfile', path: 'path/dir_1/subdir/subfile' do
describe '#nodes' do
subject { |example| path(example).nodes }
+
it { is_expected.to eq 4 }
end
@@ -153,11 +165,13 @@ describe Gitlab::Ci::Build::Artifacts::Metadata::Entry do
describe 'non-existent/', path: 'non-existent/' do
describe '#empty?' do
subject { |example| path(example).empty? }
+
it { is_expected.to be true }
end
describe '#exists?' do
subject { |example| path(example).exists? }
+
it { is_expected.to be false }
end
end
@@ -165,6 +179,7 @@ describe Gitlab::Ci::Build::Artifacts::Metadata::Entry do
describe 'another_directory/', path: 'another_directory/' do
describe '#empty?' do
subject { |example| path(example).empty? }
+
it { is_expected.to be true }
end
end
diff --git a/spec/lib/gitlab/ci/build/artifacts/metadata_spec.rb b/spec/lib/gitlab/ci/build/artifacts/metadata_spec.rb
index ff189c4701e..bfa65c66b33 100644
--- a/spec/lib/gitlab/ci/build/artifacts/metadata_spec.rb
+++ b/spec/lib/gitlab/ci/build/artifacts/metadata_spec.rb
@@ -76,21 +76,25 @@ describe Gitlab::Ci::Build::Artifacts::Metadata do
describe '#to_entry' do
subject { metadata('').to_entry }
+
it { is_expected.to be_an_instance_of(Gitlab::Ci::Build::Artifacts::Metadata::Entry) }
end
describe '#full_version' do
subject { metadata('').full_version }
+
it { is_expected.to eq 'GitLab Build Artifacts Metadata 0.0.1' }
end
describe '#version' do
subject { metadata('').version }
+
it { is_expected.to eq '0.0.1' }
end
describe '#errors' do
subject { metadata('').errors }
+
it { is_expected.to eq({}) }
end
end
diff --git a/spec/lib/gitlab/ci/build/rules/rule/clause/changes_spec.rb b/spec/lib/gitlab/ci/build/rules/rule/clause/changes_spec.rb
new file mode 100644
index 00000000000..076de3646b0
--- /dev/null
+++ b/spec/lib/gitlab/ci/build/rules/rule/clause/changes_spec.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Ci::Build::Rules::Rule::Clause::Changes do
+ describe '#satisfied_by?' do
+ it_behaves_like 'a glob matching rule' do
+ let(:pipeline) { build(:ci_pipeline) }
+
+ before do
+ allow(pipeline).to receive(:modified_paths).and_return(files.keys)
+ end
+
+ subject { described_class.new(globs).satisfied_by?(pipeline, nil) }
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/build/rules/rule/clause/exists_spec.rb b/spec/lib/gitlab/ci/build/rules/rule/clause/exists_spec.rb
new file mode 100644
index 00000000000..3605bac7dfc
--- /dev/null
+++ b/spec/lib/gitlab/ci/build/rules/rule/clause/exists_spec.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Ci::Build::Rules::Rule::Clause::Exists do
+ describe '#satisfied_by?' do
+ let(:pipeline) { build(:ci_pipeline, project: project, sha: project.repository.head_commit.sha) }
+
+ subject { described_class.new(globs).satisfied_by?(pipeline, nil) }
+
+ it_behaves_like 'a glob matching rule' do
+ let(:project) { create(:project, :custom_repo, files: files) }
+ end
+
+ context 'after pattern comparision limit is reached' do
+ let(:globs) { ['*definitely_not_a_matching_glob*'] }
+ let(:project) { create(:project, :repository) }
+
+ before do
+ stub_const('Gitlab::Ci::Build::Rules::Rule::Clause::Exists::MAX_PATTERN_COMPARISONS', 2)
+ expect(File).to receive(:fnmatch?).exactly(2).times.and_call_original
+ end
+
+ it { is_expected.to be_truthy }
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/config/edge_stages_injector_spec.rb b/spec/lib/gitlab/ci/config/edge_stages_injector_spec.rb
new file mode 100644
index 00000000000..042f9b591b6
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/edge_stages_injector_spec.rb
@@ -0,0 +1,112 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+
+describe Gitlab::Ci::Config::EdgeStagesInjector do
+ describe '#call' do
+ subject { described_class.new(config).to_hash }
+
+ context 'without stages' do
+ let(:config) do
+ {
+ test: { script: 'test' }
+ }
+ end
+
+ it { is_expected.to match config }
+ end
+
+ context 'with values' do
+ let(:config) do
+ {
+ stages: %w[stage1 stage2],
+ test: { script: 'test' }
+ }
+ end
+
+ let(:expected_stages) do
+ %w[.pre stage1 stage2 .post]
+ end
+
+ it { is_expected.to match(config.merge(stages: expected_stages)) }
+ end
+
+ context 'with bad values' do
+ let(:config) do
+ {
+ stages: 'stage1',
+ test: { script: 'test' }
+ }
+ end
+
+ it { is_expected.to match(config) }
+ end
+
+ context 'with collision values' do
+ let(:config) do
+ {
+ stages: %w[.post stage1 .pre .post stage2],
+ test: { script: 'test' }
+ }
+ end
+
+ let(:expected_stages) do
+ %w[.pre stage1 stage2 .post]
+ end
+
+ it { is_expected.to match(config.merge(stages: expected_stages)) }
+ end
+
+ context 'with types' do
+ let(:config) do
+ {
+ types: %w[stage1 stage2],
+ test: { script: 'test' }
+ }
+ end
+
+ let(:expected_config) do
+ {
+ types: %w[.pre stage1 stage2 .post],
+ test: { script: 'test' }
+ }
+ end
+
+ it { is_expected.to match expected_config }
+ end
+
+ context 'with types' do
+ let(:config) do
+ {
+ types: %w[.post stage1 .pre .post stage2],
+ test: { script: 'test' }
+ }
+ end
+
+ let(:expected_config) do
+ {
+ types: %w[.pre stage1 stage2 .post],
+ test: { script: 'test' }
+ }
+ end
+
+ it { is_expected.to match expected_config }
+ end
+ end
+
+ describe '.wrap_stages' do
+ subject { described_class.wrap_stages(stages) }
+
+ context 'with empty value' do
+ let(:stages) {}
+
+ it { is_expected.to eq %w[.pre .post] }
+ end
+
+ context 'with values' do
+ let(:stages) { %w[s1 .pre] }
+
+ it { is_expected.to eq %w[.pre s1 .post] }
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/config/entry/cache_spec.rb b/spec/lib/gitlab/ci/config/entry/cache_spec.rb
index 4cb63168ec7..9aab3664e1c 100644
--- a/spec/lib/gitlab/ci/config/entry/cache_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/cache_spec.rb
@@ -69,6 +69,7 @@ describe Gitlab::Ci::Config::Entry::Cache do
context 'when entry value is not correct' do
describe '#errors' do
subject { entry.errors }
+
context 'when is not a hash' do
let(:config) { 'ls' }
diff --git a/spec/lib/gitlab/ci/config/entry/coverage_spec.rb b/spec/lib/gitlab/ci/config/entry/coverage_spec.rb
index 48d0864cfca..877e3ec6216 100644
--- a/spec/lib/gitlab/ci/config/entry/coverage_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/coverage_spec.rb
@@ -11,11 +11,13 @@ describe Gitlab::Ci::Config::Entry::Coverage do
describe '#errors' do
subject { entry.errors }
+
it { is_expected.to include(/coverage config must be a regular expression/) }
end
describe '#valid?' do
subject { entry }
+
it { is_expected.not_to be_valid }
end
end
@@ -25,16 +27,19 @@ describe Gitlab::Ci::Config::Entry::Coverage do
describe '#value' do
subject { entry.value }
+
it { is_expected.to eq(config[1...-1]) }
end
describe '#errors' do
subject { entry.errors }
+
it { is_expected.to be_empty }
end
describe '#valid?' do
subject { entry }
+
it { is_expected.to be_valid }
end
end
@@ -44,11 +49,13 @@ describe Gitlab::Ci::Config::Entry::Coverage do
describe '#errors' do
subject { entry.errors }
+
it { is_expected.to include(/coverage config must be a regular expression/) }
end
describe '#valid?' do
subject { entry }
+
it { is_expected.not_to be_valid }
end
end
diff --git a/spec/lib/gitlab/ci/config/entry/root_spec.rb b/spec/lib/gitlab/ci/config/entry/root_spec.rb
index 968dbb9c7f2..7e1a80414d4 100644
--- a/spec/lib/gitlab/ci/config/entry/root_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/root_spec.rb
@@ -215,7 +215,7 @@ describe Gitlab::Ci::Config::Entry::Root do
describe '#stages_value' do
it 'returns an array of root stages' do
- expect(root.stages_value).to eq %w[build test deploy]
+ expect(root.stages_value).to eq %w[.pre build test deploy .post]
end
end
diff --git a/spec/lib/gitlab/ci/config/entry/rules/rule_spec.rb b/spec/lib/gitlab/ci/config/entry/rules/rule_spec.rb
index 18037a5612c..9d4f7153cd0 100644
--- a/spec/lib/gitlab/ci/config/entry/rules/rule_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/rules/rule_spec.rb
@@ -103,6 +103,52 @@ describe Gitlab::Ci::Config::Entry::Rules::Rule do
end
end
+ context 'when using a long list as an invalid changes: clause' do
+ let(:config) { { changes: ['app/'] * 51 } }
+
+ it { is_expected.not_to be_valid }
+
+ it 'returns errors' do
+ expect(subject.errors).to include(/changes is too long \(maximum is 50 characters\)/)
+ end
+ end
+
+ context 'when using a exists: clause' do
+ let(:config) { { exists: %w[app/ lib/ spec/ other/* paths/**/*.rb] } }
+
+ it { is_expected.to be_valid }
+ end
+
+ context 'when using a string as an invalid exists: clause' do
+ let(:config) { { exists: 'a regular string' } }
+
+ it { is_expected.not_to be_valid }
+
+ it 'reports an error about invalid policy' do
+ expect(subject.errors).to include(/should be an array of strings/)
+ end
+ end
+
+ context 'when using a list as an invalid exists: clause' do
+ let(:config) { { exists: [1, 2] } }
+
+ it { is_expected.not_to be_valid }
+
+ it 'returns errors' do
+ expect(subject.errors).to include(/exists should be an array of strings/)
+ end
+ end
+
+ context 'when using a long list as an invalid exists: clause' do
+ let(:config) { { exists: ['app/'] * 51 } }
+
+ it { is_expected.not_to be_valid }
+
+ it 'returns errors' do
+ expect(subject.errors).to include(/exists is too long \(maximum is 50 characters\)/)
+ end
+ end
+
context 'specifying a delayed job' do
let(:config) { { if: '$THIS || $THAT', when: 'delayed', start_in: '15 minutes' } }
@@ -198,6 +244,12 @@ describe Gitlab::Ci::Config::Entry::Rules::Rule do
expect(entry.value).to eq(config)
end
end
+
+ context 'when using a exists: clause' do
+ let(:config) { { exists: %w[app/ lib/ spec/ other/* paths/**/*.rb] } }
+
+ it { is_expected.to eq(config) }
+ end
end
describe '.default' do
diff --git a/spec/lib/gitlab/ci/config/entry/stages_spec.rb b/spec/lib/gitlab/ci/config/entry/stages_spec.rb
index 97970522104..3e6ff8eca28 100644
--- a/spec/lib/gitlab/ci/config/entry/stages_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/stages_spec.rb
@@ -42,7 +42,7 @@ describe Gitlab::Ci::Config::Entry::Stages do
describe '.default' do
it 'returns default stages' do
- expect(described_class.default).to eq %w[build test deploy]
+ expect(described_class.default).to eq %w[.pre build test deploy .post]
end
end
end
diff --git a/spec/lib/gitlab/ci/config/external/context_spec.rb b/spec/lib/gitlab/ci/config/external/context_spec.rb
new file mode 100644
index 00000000000..610646ca85a
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/external/context_spec.rb
@@ -0,0 +1,128 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+
+describe Gitlab::Ci::Config::External::Context do
+ let(:project) { double('Project') }
+ let(:user) { double('User') }
+ let(:sha) { '12345' }
+ let(:attributes) { { project: project, user: user, sha: sha } }
+
+ subject(:subject) { described_class.new(**attributes) }
+
+ describe 'attributes' do
+ context 'with values' do
+ it { is_expected.to have_attributes(**attributes) }
+ it { expect(subject.expandset).to eq(Set.new) }
+ it { expect(subject.execution_deadline).to eq(0) }
+ end
+
+ context 'without values' do
+ let(:attributes) { { project: nil, user: nil, sha: nil } }
+
+ it { is_expected.to have_attributes(**attributes) }
+ it { expect(subject.expandset).to eq(Set.new) }
+ it { expect(subject.execution_deadline).to eq(0) }
+ end
+ end
+
+ describe '#set_deadline' do
+ let(:stubbed_time) { 1 }
+
+ before do
+ allow(subject).to receive(:current_monotonic_time).and_return(stubbed_time)
+ end
+
+ context 'with a float value' do
+ let(:timeout_seconds) { 10.5.seconds }
+
+ it 'updates execution_deadline' do
+ expect { subject.set_deadline(timeout_seconds) }
+ .to change { subject.execution_deadline }
+ .to(timeout_seconds + stubbed_time)
+ end
+ end
+
+ context 'with nil as a value' do
+ let(:timeout_seconds) {}
+
+ it 'updates execution_deadline' do
+ expect { subject.set_deadline(timeout_seconds) }
+ .to change { subject.execution_deadline }
+ .to(stubbed_time)
+ end
+ end
+ end
+
+ describe '#check_execution_time!' do
+ before do
+ allow(subject).to receive(:current_monotonic_time).and_return(stubbed_time)
+ allow(subject).to receive(:execution_deadline).and_return(stubbed_deadline)
+ end
+
+ context 'when execution is expired' do
+ let(:stubbed_time) { 2 }
+ let(:stubbed_deadline) { 1 }
+
+ it 'raises an error' do
+ expect { subject.check_execution_time! }
+ .to raise_error(described_class::TimeoutError)
+ end
+ end
+
+ context 'when execution is not expired' do
+ let(:stubbed_time) { 1 }
+ let(:stubbed_deadline) { 2 }
+
+ it 'does not raises any errors' do
+ expect { subject.check_execution_time! }.not_to raise_error
+ end
+ end
+
+ context 'without setting a deadline' do
+ let(:stubbed_time) { 2 }
+ let(:stubbed_deadline) { 1 }
+
+ before do
+ allow(subject).to receive(:execution_deadline).and_call_original
+ end
+
+ it 'does not raises any errors' do
+ expect { subject.check_execution_time! }.not_to raise_error
+ end
+ end
+ end
+
+ describe '#mutate' do
+ shared_examples 'a mutated context' do
+ let(:mutated) { subject.mutate(new_attributes) }
+
+ before do
+ subject.expandset << :a_file
+ subject.set_deadline(15.seconds)
+ end
+
+ it { expect(mutated).not_to eq(subject) }
+ it { expect(mutated).to be_a(described_class) }
+ it { expect(mutated).to have_attributes(new_attributes) }
+ it { expect(mutated.expandset).to eq(subject.expandset) }
+ it { expect(mutated.execution_deadline).to eq(mutated.execution_deadline) }
+ end
+
+ context 'with attributes' do
+ let(:new_attributes) { { project: double, user: double, sha: '56789' } }
+
+ it_behaves_like 'a mutated context'
+ end
+
+ context 'without attributes' do
+ let(:new_attributes) { {} }
+
+ it_behaves_like 'a mutated context'
+ end
+ end
+
+ describe '#sentry_payload' do
+ it { expect(subject.sentry_payload).to match(a_hash_including(:project, :user)) }
+ end
+end
diff --git a/spec/lib/gitlab/ci/config/external/file/base_spec.rb b/spec/lib/gitlab/ci/config/external/file/base_spec.rb
index af995f4869a..d472d6527e2 100644
--- a/spec/lib/gitlab/ci/config/external/file/base_spec.rb
+++ b/spec/lib/gitlab/ci/config/external/file/base_spec.rb
@@ -1,13 +1,14 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'spec_helper'
describe Gitlab::Ci::Config::External::File::Base do
- let(:context) { described_class::Context.new(nil, 'HEAD', nil, Set.new) }
+ let(:context_params) { { sha: 'HEAD' } }
+ let(:context) { Gitlab::Ci::Config::External::Context.new(**context_params) }
let(:test_class) do
Class.new(described_class) do
- def initialize(params, context = {})
+ def initialize(params, context)
@location = params
super
@@ -20,6 +21,9 @@ describe Gitlab::Ci::Config::External::File::Base do
before do
allow_any_instance_of(test_class)
.to receive(:content).and_return('key: value')
+
+ allow_any_instance_of(Gitlab::Ci::Config::External::Context)
+ .to receive(:check_execution_time!)
end
describe '#matching?' do
diff --git a/spec/lib/gitlab/ci/config/external/file/local_spec.rb b/spec/lib/gitlab/ci/config/external/file/local_spec.rb
index 9451db9522a..95f0c93e758 100644
--- a/spec/lib/gitlab/ci/config/external/file/local_spec.rb
+++ b/spec/lib/gitlab/ci/config/external/file/local_spec.rb
@@ -7,10 +7,17 @@ describe Gitlab::Ci::Config::External::File::Local do
set(:user) { create(:user) }
let(:sha) { '12345' }
- let(:context) { described_class::Context.new(project, sha, user, Set.new) }
+ let(:context_params) { { project: project, sha: sha, user: user } }
+ let(:context) { Gitlab::Ci::Config::External::Context.new(**context_params) }
+
let(:params) { { local: location } }
let(:local_file) { described_class.new(params, context) }
+ before do
+ allow_any_instance_of(Gitlab::Ci::Config::External::Context)
+ .to receive(:check_execution_time!)
+ end
+
describe '#matching?' do
context 'when a local is specified' do
let(:params) { { local: 'file' } }
@@ -109,7 +116,7 @@ describe Gitlab::Ci::Config::External::File::Local do
describe '#expand_context' do
let(:location) { 'location.yml' }
- subject { local_file.send(:expand_context) }
+ subject { local_file.send(:expand_context_attrs) }
it 'inherits project, user and sha' do
is_expected.to include(user: user, project: project, sha: sha)
diff --git a/spec/lib/gitlab/ci/config/external/file/project_spec.rb b/spec/lib/gitlab/ci/config/external/file/project_spec.rb
index 4acb4f324d3..dd869c227a1 100644
--- a/spec/lib/gitlab/ci/config/external/file/project_spec.rb
+++ b/spec/lib/gitlab/ci/config/external/file/project_spec.rb
@@ -8,11 +8,15 @@ describe Gitlab::Ci::Config::External::File::Project do
set(:user) { create(:user) }
let(:context_user) { user }
- let(:context) { described_class::Context.new(context_project, '12345', context_user, Set.new) }
+ let(:context_params) { { project: context_project, sha: '12345', user: context_user } }
+ let(:context) { Gitlab::Ci::Config::External::Context.new(**context_params) }
let(:project_file) { described_class.new(params, context) }
before do
project.add_developer(user)
+
+ allow_any_instance_of(Gitlab::Ci::Config::External::Context)
+ .to receive(:check_execution_time!)
end
describe '#matching?' do
@@ -145,7 +149,7 @@ describe Gitlab::Ci::Config::External::File::Project do
describe '#expand_context' do
let(:params) { { file: 'file.yml', project: project.full_path, ref: 'master' } }
- subject { project_file.send(:expand_context) }
+ subject { project_file.send(:expand_context_attrs) }
it 'inherits user, and target project and sha' do
is_expected.to include(user: user, project: project, sha: project.commit('master').id)
diff --git a/spec/lib/gitlab/ci/config/external/file/remote_spec.rb b/spec/lib/gitlab/ci/config/external/file/remote_spec.rb
index 4a097b59216..08db00dda9d 100644
--- a/spec/lib/gitlab/ci/config/external/file/remote_spec.rb
+++ b/spec/lib/gitlab/ci/config/external/file/remote_spec.rb
@@ -5,7 +5,8 @@ require 'spec_helper'
describe Gitlab::Ci::Config::External::File::Remote do
include StubRequests
- let(:context) { described_class::Context.new(nil, '12345', nil, Set.new) }
+ let(:context_params) { { sha: '12345' } }
+ let(:context) { Gitlab::Ci::Config::External::Context.new(**context_params) }
let(:params) { { remote: location } }
let(:remote_file) { described_class.new(params, context) }
let(:location) { 'https://gitlab.com/gitlab-org/gitlab-foss/blob/1234/.gitlab-ci-1.yml' }
@@ -19,6 +20,11 @@ describe Gitlab::Ci::Config::External::File::Remote do
HEREDOC
end
+ before do
+ allow_any_instance_of(Gitlab::Ci::Config::External::Context)
+ .to receive(:check_execution_time!)
+ end
+
describe '#matching?' do
context 'when a remote is specified' do
let(:params) { { remote: 'http://remote' } }
@@ -187,10 +193,10 @@ describe Gitlab::Ci::Config::External::File::Remote do
describe '#expand_context' do
let(:params) { { remote: 'http://remote' } }
- subject { remote_file.send(:expand_context) }
+ subject { remote_file.send(:expand_context_attrs) }
it 'drops all parameters' do
- is_expected.to include(user: nil, project: nil, sha: nil)
+ is_expected.to be_empty
end
end
end
diff --git a/spec/lib/gitlab/ci/config/external/file/template_spec.rb b/spec/lib/gitlab/ci/config/external/file/template_spec.rb
index 1609b8fd66b..164b5800abf 100644
--- a/spec/lib/gitlab/ci/config/external/file/template_spec.rb
+++ b/spec/lib/gitlab/ci/config/external/file/template_spec.rb
@@ -6,12 +6,18 @@ describe Gitlab::Ci::Config::External::File::Template do
set(:project) { create(:project) }
set(:user) { create(:user) }
- let(:context) { described_class::Context.new(project, '12345', user, Set.new) }
+ let(:context_params) { { project: project, sha: '12345', user: user } }
+ let(:context) { Gitlab::Ci::Config::External::Context.new(**context_params) }
let(:template) { 'Auto-DevOps.gitlab-ci.yml' }
let(:params) { { template: template } }
let(:template_file) { described_class.new(params, context) }
+ before do
+ allow_any_instance_of(Gitlab::Ci::Config::External::Context)
+ .to receive(:check_execution_time!)
+ end
+
describe '#matching?' do
context 'when a template is specified' do
let(:params) { { template: 'some-template' } }
@@ -97,10 +103,10 @@ describe Gitlab::Ci::Config::External::File::Template do
describe '#expand_context' do
let(:location) { 'location.yml' }
- subject { template_file.send(:expand_context) }
+ subject { template_file.send(:expand_context_attrs) }
it 'drops all parameters' do
- is_expected.to include(user: nil, project: nil, sha: nil)
+ is_expected.to be_empty
end
end
end
diff --git a/spec/lib/gitlab/ci/config/external/mapper_spec.rb b/spec/lib/gitlab/ci/config/external/mapper_spec.rb
index 43708852594..8d09aa47f12 100644
--- a/spec/lib/gitlab/ci/config/external/mapper_spec.rb
+++ b/spec/lib/gitlab/ci/config/external/mapper_spec.rb
@@ -11,7 +11,8 @@ describe Gitlab::Ci::Config::External::Mapper do
let(:local_file) { '/lib/gitlab/ci/templates/non-existent-file.yml' }
let(:remote_url) { 'https://gitlab.com/gitlab-org/gitlab-foss/blob/1234/.gitlab-ci-1.yml' }
let(:template_file) { 'Auto-DevOps.gitlab-ci.yml' }
- let(:expandset) { Set.new }
+ let(:context_params) { { project: project, sha: '123456', user: user } }
+ let(:context) { Gitlab::Ci::Config::External::Context.new(**context_params) }
let(:file_content) do
<<~HEREDOC
@@ -21,10 +22,13 @@ describe Gitlab::Ci::Config::External::Mapper do
before do
stub_full_request(remote_url).to_return(body: file_content)
+
+ allow_any_instance_of(Gitlab::Ci::Config::External::Context)
+ .to receive(:check_execution_time!)
end
describe '#process' do
- subject { described_class.new(values, project: project, sha: '123456', user: user, expandset: expandset).process }
+ subject { described_class.new(values, context).process }
context "when single 'include' keyword is defined" do
context 'when the string is a local file' do
diff --git a/spec/lib/gitlab/ci/config/external/processor_spec.rb b/spec/lib/gitlab/ci/config/external/processor_spec.rb
index 3b1a1e804f0..bb2d3f66972 100644
--- a/spec/lib/gitlab/ci/config/external/processor_spec.rb
+++ b/spec/lib/gitlab/ci/config/external/processor_spec.rb
@@ -9,12 +9,16 @@ describe Gitlab::Ci::Config::External::Processor do
set(:another_project) { create(:project, :repository) }
set(:user) { create(:user) }
- let(:expandset) { Set.new }
let(:sha) { '12345' }
- let(:processor) { described_class.new(values, project: project, sha: '12345', user: user, expandset: expandset) }
+ let(:context_params) { { project: project, sha: sha, user: user } }
+ let(:context) { Gitlab::Ci::Config::External::Context.new(**context_params) }
+ let(:processor) { described_class.new(values, context) }
before do
project.add_developer(user)
+
+ allow_any_instance_of(Gitlab::Ci::Config::External::Context)
+ .to receive(:check_execution_time!)
end
describe "#perform" do
diff --git a/spec/lib/gitlab/ci/config_spec.rb b/spec/lib/gitlab/ci/config_spec.rb
index 839b4f9261d..b254f9af2f1 100644
--- a/spec/lib/gitlab/ci/config_spec.rb
+++ b/spec/lib/gitlab/ci/config_spec.rb
@@ -7,6 +7,11 @@ describe Gitlab::Ci::Config do
set(:user) { create(:user) }
+ before do
+ allow_any_instance_of(Gitlab::Ci::Config::External::Context)
+ .to receive(:check_execution_time!)
+ end
+
let(:config) do
described_class.new(yml, project: nil, sha: nil, user: nil)
end
@@ -46,6 +51,54 @@ describe Gitlab::Ci::Config do
end
end
end
+
+ describe '#stages' do
+ subject(:subject) { config.stages }
+
+ context 'with default stages' do
+ let(:default_stages) do
+ %w[.pre build test deploy .post]
+ end
+
+ it { is_expected.to eq default_stages }
+ end
+
+ context 'with custom stages' do
+ let(:yml) do
+ <<-EOS
+ stages:
+ - stage1
+ - stage2
+ job1:
+ stage: stage1
+ script:
+ - ls
+ EOS
+ end
+
+ it { is_expected.to eq %w[.pre stage1 stage2 .post] }
+ end
+
+ context 'with feature disabled' do
+ before do
+ stub_feature_flags(ci_pre_post_pipeline_stages: false)
+ end
+
+ let(:yml) do
+ <<-EOS
+ stages:
+ - stage1
+ - stage2
+ job1:
+ stage: stage1
+ script:
+ - ls
+ EOS
+ end
+
+ it { is_expected.to eq %w[stage1 stage2] }
+ end
+ end
end
context 'when using extendable hash' do
@@ -303,6 +356,49 @@ describe Gitlab::Ci::Config do
end
end
+ context "when it takes too long to evaluate includes" do
+ before do
+ allow_any_instance_of(Gitlab::Ci::Config::External::Context)
+ .to receive(:check_execution_time!)
+ .and_call_original
+
+ allow_any_instance_of(Gitlab::Ci::Config::External::Context)
+ .to receive(:set_deadline)
+ .with(described_class::TIMEOUT_SECONDS)
+ .and_call_original
+
+ allow_any_instance_of(Gitlab::Ci::Config::External::Context)
+ .to receive(:execution_expired?)
+ .and_return(true)
+ end
+
+ it 'raises error TimeoutError' do
+ expect(Gitlab::Sentry).to receive(:track_exception)
+
+ expect { config }.to raise_error(
+ described_class::ConfigError,
+ 'Resolving config took longer than expected'
+ )
+ end
+ end
+
+ context 'when context expansion timeout is disabled' do
+ before do
+ allow_any_instance_of(Gitlab::Ci::Config::External::Context)
+ .to receive(:check_execution_time!)
+ .and_call_original
+
+ allow(Feature)
+ .to receive(:enabled?)
+ .with(:ci_limit_yaml_expansion, project, default_enabled: true)
+ .and_return(false)
+ end
+
+ it 'does not raises errors' do
+ expect { config }.not_to raise_error
+ end
+ end
+
describe 'external file version' do
context 'when external local file SHA is defined' do
it 'is using a defined value' do
diff --git a/spec/lib/gitlab/ci/parsers/test/junit_spec.rb b/spec/lib/gitlab/ci/parsers/test/junit_spec.rb
index 8ff60710f67..6a7fe7a5927 100644
--- a/spec/lib/gitlab/ci/parsers/test/junit_spec.rb
+++ b/spec/lib/gitlab/ci/parsers/test/junit_spec.rb
@@ -38,12 +38,14 @@ describe Gitlab::Ci::Parsers::Test::Junit do
end
end
- context 'when there is only one <testcase> in <testsuite>' do
+ context 'when there is only one <testsuite> in <testsuites>' do
let(:junit) do
<<-EOF.strip_heredoc
- <testsuite>
- <testcase classname='Calculator' name='sumTest1' time='0.01'></testcase>
- </testsuite>
+ <testsuites>
+ <testsuite>
+ <testcase classname='Calculator' name='sumTest1' time='0.01'></testcase>
+ </testsuite>
+ </testsuites>
EOF
end
@@ -56,23 +58,65 @@ describe Gitlab::Ci::Parsers::Test::Junit do
end
end
- context 'when there is only one <testsuite> in <testsuites>' do
+ context 'when there is <testcase>' do
let(:junit) do
<<-EOF.strip_heredoc
- <testsuites>
<testsuite>
- <testcase classname='Calculator' name='sumTest1' time='0.01'></testcase>
+ <testcase classname='Calculator' name='sumTest1' time='0.01'>
+ #{testcase_content}
+ </testcase>
</testsuite>
- </testsuites>
EOF
end
- it 'parses XML and adds a test case to a suite' do
+ let(:test_case) { test_cases[0] }
+
+ before do
expect { subject }.not_to raise_error
+ end
- expect(test_cases[0].classname).to eq('Calculator')
- expect(test_cases[0].name).to eq('sumTest1')
- expect(test_cases[0].execution_time).to eq(0.01)
+ shared_examples_for '<testcase> XML parser' do |status, output|
+ it 'parses XML and adds a test case to the suite' do
+ aggregate_failures do
+ expect(test_case.classname).to eq('Calculator')
+ expect(test_case.name).to eq('sumTest1')
+ expect(test_case.execution_time).to eq(0.01)
+ expect(test_case.status).to eq(status)
+ expect(test_case.system_output).to eq(output)
+ end
+ end
+ end
+
+ context 'and has failure' do
+ let(:testcase_content) { '<failure>Some failure</failure>' }
+
+ it_behaves_like '<testcase> XML parser',
+ ::Gitlab::Ci::Reports::TestCase::STATUS_FAILED,
+ 'Some failure'
+ end
+
+ context 'and has error' do
+ let(:testcase_content) { '<error>Some error</error>' }
+
+ it_behaves_like '<testcase> XML parser',
+ ::Gitlab::Ci::Reports::TestCase::STATUS_FAILED,
+ 'Some error'
+ end
+
+ context 'and has an unknown type' do
+ let(:testcase_content) { '<foo>Some foo</foo>' }
+
+ it_behaves_like '<testcase> XML parser',
+ ::Gitlab::Ci::Reports::TestCase::STATUS_SUCCESS,
+ nil
+ end
+
+ context 'and has no content' do
+ let(:testcase_content) { '' }
+
+ it_behaves_like '<testcase> XML parser',
+ ::Gitlab::Ci::Reports::TestCase::STATUS_SUCCESS,
+ nil
end
end
diff --git a/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb b/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb
index 023d7530b4b..945baf47b7b 100644
--- a/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb
@@ -117,6 +117,24 @@ describe Gitlab::Ci::Pipeline::Seed::Build do
context 'when job is not a bridge' do
it { is_expected.to be_a(::Ci::Build) }
it { is_expected.to be_valid }
+
+ context 'when job has environment name' do
+ let(:attributes) { { name: 'rspec', ref: 'master', environment: 'production' } }
+
+ it 'returns a job with deployment' do
+ expect(subject.deployment).not_to be_nil
+ expect(subject.deployment.deployable).to eq(subject)
+ expect(subject.deployment.environment.name).to eq('production')
+ end
+
+ context 'when the environment name is invalid' do
+ let(:attributes) { { name: 'rspec', ref: 'master', environment: '!!!' } }
+
+ it 'returns a job without deployment' do
+ expect(subject.deployment).to be_nil
+ end
+ end
+ end
end
context 'when job is a bridge' do
diff --git a/spec/lib/gitlab/ci/pipeline/seed/deployment_spec.rb b/spec/lib/gitlab/ci/pipeline/seed/deployment_spec.rb
new file mode 100644
index 00000000000..4e63f60ea6b
--- /dev/null
+++ b/spec/lib/gitlab/ci/pipeline/seed/deployment_spec.rb
@@ -0,0 +1,76 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Ci::Pipeline::Seed::Deployment do
+ let_it_be(:project) { create(:project) }
+ let(:job) { build(:ci_build, project: project) }
+ let(:seed) { described_class.new(job) }
+ let(:attributes) { {} }
+
+ before do
+ job.assign_attributes(**attributes)
+ end
+
+ describe '#to_resource' do
+ subject { seed.to_resource }
+
+ context 'when job has environment attribute' do
+ let(:attributes) do
+ {
+ environment: 'production',
+ options: { environment: { name: 'production' } }
+ }
+ end
+
+ it 'returns a deployment object with environment' do
+ expect(subject).to be_a(Deployment)
+ expect(subject.iid).to be_present
+ expect(subject.environment.name).to eq('production')
+ expect(subject.cluster).to be_nil
+ end
+
+ context 'when environment has deployment platform' do
+ let!(:cluster) { create(:cluster, :provided_by_gcp, projects: [project]) }
+
+ it 'returns a deployment with cluster id' do
+ expect(subject.cluster).to eq(cluster)
+ end
+ end
+
+ context 'when environment has an invalid URL' do
+ let(:attributes) do
+ {
+ environment: '!!!',
+ options: { environment: { name: '!!!' } }
+ }
+ end
+
+ it 'returns nothing' do
+ is_expected.to be_nil
+ end
+ end
+
+ context 'when job has already deployment' do
+ let(:job) { build(:ci_build, :with_deployment, project: project, environment: 'production') }
+
+ it 'returns the persisted deployment' do
+ is_expected.to eq(job.deployment)
+ end
+ end
+ end
+
+ context 'when job has environment attribute with stop action' do
+ let(:attributes) do
+ {
+ environment: 'production',
+ options: { environment: { name: 'production', action: 'stop' } }
+ }
+ end
+
+ it 'returns nothing' do
+ is_expected.to be_nil
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/pipeline/seed/environment_spec.rb b/spec/lib/gitlab/ci/pipeline/seed/environment_spec.rb
new file mode 100644
index 00000000000..71389999c6e
--- /dev/null
+++ b/spec/lib/gitlab/ci/pipeline/seed/environment_spec.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Ci::Pipeline::Seed::Environment do
+ let_it_be(:project) { create(:project) }
+ let(:job) { build(:ci_build, project: project) }
+ let(:seed) { described_class.new(job) }
+ let(:attributes) { {} }
+
+ before do
+ job.assign_attributes(**attributes)
+ end
+
+ describe '#to_resource' do
+ subject { seed.to_resource }
+
+ context 'when job has environment attribute' do
+ let(:attributes) do
+ {
+ environment: 'production',
+ options: { environment: { name: 'production' } }
+ }
+ end
+
+ it 'returns a persisted environment object' do
+ expect(subject).to be_a(Environment)
+ expect(subject).to be_persisted
+ expect(subject.project).to eq(project)
+ expect(subject.name).to eq('production')
+ end
+
+ context 'when environment has already existed' do
+ let!(:environment) { create(:environment, project: project, name: 'production') }
+
+ it 'returns the existing environment object' do
+ expect(subject).to be_persisted
+ expect(subject).to eq(environment)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/status/composite_spec.rb b/spec/lib/gitlab/ci/status/composite_spec.rb
new file mode 100644
index 00000000000..1725d954b92
--- /dev/null
+++ b/spec/lib/gitlab/ci/status/composite_spec.rb
@@ -0,0 +1,61 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Status::Composite do
+ set(:pipeline) { create(:ci_pipeline) }
+
+ before(:all) do
+ @statuses = HasStatus::STATUSES_ENUM.map do |status, idx|
+ [status, create(:ci_build, pipeline: pipeline, status: status, importing: true)]
+ end.to_h
+
+ @statuses_with_allow_failure = HasStatus::STATUSES_ENUM.map do |status, idx|
+ [status, create(:ci_build, pipeline: pipeline, status: status, allow_failure: true, importing: true)]
+ end.to_h
+ end
+
+ describe '#status' do
+ shared_examples 'compares composite with SQL status' do
+ it 'returns exactly the same result' do
+ builds = Ci::Build.where(id: all_statuses)
+
+ expect(composite_status.status).to eq(builds.legacy_status)
+ expect(composite_status.warnings?).to eq(builds.failed_but_allowed.any?)
+ end
+ end
+
+ shared_examples 'validate all combinations' do |perms|
+ HasStatus::STATUSES_ENUM.keys.combination(perms).each do |statuses|
+ context "with #{statuses.join(",")}" do
+ it_behaves_like 'compares composite with SQL status' do
+ let(:all_statuses) do
+ statuses.map { |status| @statuses[status] }
+ end
+
+ let(:composite_status) do
+ described_class.new(all_statuses)
+ end
+ end
+
+ HasStatus::STATUSES_ENUM.each do |allow_failure_status, _|
+ context "and allow_failure #{allow_failure_status}" do
+ it_behaves_like 'compares composite with SQL status' do
+ let(:all_statuses) do
+ statuses.map { |status| @statuses[status] } +
+ [@statuses_with_allow_failure[allow_failure_status]]
+ end
+
+ let(:composite_status) do
+ described_class.new(all_statuses)
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+
+ it_behaves_like 'validate all combinations', 0
+ it_behaves_like 'validate all combinations', 1
+ it_behaves_like 'validate all combinations', 2
+ end
+end
diff --git a/spec/lib/gitlab/ci/status/external/factory_spec.rb b/spec/lib/gitlab/ci/status/external/factory_spec.rb
index 3b90fb60cca..9d7dfc42848 100644
--- a/spec/lib/gitlab/ci/status/external/factory_spec.rb
+++ b/spec/lib/gitlab/ci/status/external/factory_spec.rb
@@ -22,7 +22,7 @@ describe Gitlab::Ci::Status::External::Factory do
end
let(:expected_status) do
- Gitlab::Ci::Status.const_get(simple_status.capitalize)
+ Gitlab::Ci::Status.const_get(simple_status.capitalize, false)
end
it "fabricates a core status #{simple_status}" do
diff --git a/spec/lib/gitlab/ci/status/factory_spec.rb b/spec/lib/gitlab/ci/status/factory_spec.rb
index b51c0bec47e..c6d7a1ec5d9 100644
--- a/spec/lib/gitlab/ci/status/factory_spec.rb
+++ b/spec/lib/gitlab/ci/status/factory_spec.rb
@@ -13,7 +13,7 @@ describe Gitlab::Ci::Status::Factory do
let(:resource) { double('resource', status: simple_status) }
let(:expected_status) do
- Gitlab::Ci::Status.const_get(simple_status.capitalize)
+ Gitlab::Ci::Status.const_get(simple_status.capitalize, false)
end
it "fabricates a core status #{simple_status}" do
diff --git a/spec/lib/gitlab/ci/status/pipeline/factory_spec.rb b/spec/lib/gitlab/ci/status/pipeline/factory_spec.rb
index 8a36cd1b658..3acc767ab7a 100644
--- a/spec/lib/gitlab/ci/status/pipeline/factory_spec.rb
+++ b/spec/lib/gitlab/ci/status/pipeline/factory_spec.rb
@@ -18,7 +18,7 @@ describe Gitlab::Ci::Status::Pipeline::Factory do
let(:pipeline) { create(:ci_pipeline, status: simple_status) }
let(:expected_status) do
- Gitlab::Ci::Status.const_get(simple_status.capitalize)
+ Gitlab::Ci::Status.const_get(simple_status.capitalize, false)
end
it "matches correct core status for #{simple_status}" do
diff --git a/spec/lib/gitlab/ci/status/preparing_spec.rb b/spec/lib/gitlab/ci/status/preparing_spec.rb
index 7211c0e506d..33f6bab8d65 100644
--- a/spec/lib/gitlab/ci/status/preparing_spec.rb
+++ b/spec/lib/gitlab/ci/status/preparing_spec.rb
@@ -16,11 +16,11 @@ describe Gitlab::Ci::Status::Preparing do
end
describe '#icon' do
- it { expect(subject.icon).to eq 'status_created' }
+ it { expect(subject.icon).to eq 'status_preparing' }
end
describe '#favicon' do
- it { expect(subject.favicon).to eq 'favicon_status_created' }
+ it { expect(subject.favicon).to eq 'favicon_status_preparing' }
end
describe '#group' do
diff --git a/spec/lib/gitlab/ci/status/stage/factory_spec.rb b/spec/lib/gitlab/ci/status/stage/factory_spec.rb
index 8f5b1ff62a5..dcb53712157 100644
--- a/spec/lib/gitlab/ci/status/stage/factory_spec.rb
+++ b/spec/lib/gitlab/ci/status/stage/factory_spec.rb
@@ -34,7 +34,7 @@ describe Gitlab::Ci::Status::Stage::Factory do
it "fabricates a core status #{core_status}" do
expect(status).to be_a(
- Gitlab::Ci::Status.const_get(core_status.capitalize))
+ Gitlab::Ci::Status.const_get(core_status.capitalize, false))
end
it 'extends core status with common stage methods' do
diff --git a/spec/lib/gitlab/ci/trace/stream_spec.rb b/spec/lib/gitlab/ci/trace/stream_spec.rb
index dd5f2f97ac9..1baea13299b 100644
--- a/spec/lib/gitlab/ci/trace/stream_spec.rb
+++ b/spec/lib/gitlab/ci/trace/stream_spec.rb
@@ -248,60 +248,6 @@ describe Gitlab::Ci::Trace::Stream, :clean_gitlab_redis_cache do
end
end
- describe '#html_with_state' do
- shared_examples_for 'html_with_states' do
- it 'returns html content with state' do
- result = stream.html_with_state
-
- expect(result.html).to eq("<span>1234</span>")
- end
-
- context 'follow-up state' do
- let!(:last_result) { stream.html_with_state }
-
- before do
- data_stream.seek(4, IO::SEEK_SET)
- data_stream.write("5678")
- stream.seek(0)
- end
-
- it "returns appended trace" do
- result = stream.html_with_state(last_result.state)
-
- expect(result.append).to be_truthy
- expect(result.html).to eq("<span>5678</span>")
- end
- end
- end
-
- context 'when stream is StringIO' do
- let(:data_stream) do
- StringIO.new("1234")
- end
-
- let(:stream) do
- described_class.new { data_stream }
- end
-
- it_behaves_like 'html_with_states'
- end
-
- context 'when stream is ChunkedIO' do
- let(:data_stream) do
- Gitlab::Ci::Trace::ChunkedIO.new(build).tap do |chunked_io|
- chunked_io.write("1234")
- chunked_io.seek(0, IO::SEEK_SET)
- end
- end
-
- let(:stream) do
- described_class.new { data_stream }
- end
-
- it_behaves_like 'html_with_states'
- end
- end
-
describe '#html' do
shared_examples_for 'htmls' do
it "returns html" do
diff --git a/spec/lib/gitlab/ci/yaml_processor_spec.rb b/spec/lib/gitlab/ci/yaml_processor_spec.rb
index d43eb4e4b4a..cb5ebde16d7 100644
--- a/spec/lib/gitlab/ci/yaml_processor_spec.rb
+++ b/spec/lib/gitlab/ci/yaml_processor_spec.rb
@@ -26,7 +26,7 @@ module Gitlab
it 'returns valid build attributes' do
expect(subject).to eq({
stage: "test",
- stage_idx: 1,
+ stage_idx: 2,
name: "rspec",
options: {
before_script: ["pwd"],
@@ -56,7 +56,7 @@ module Gitlab
it 'returns valid build attributes' do
expect(subject).to eq({
stage: 'test',
- stage_idx: 1,
+ stage_idx: 2,
name: 'rspec',
options: { script: ['rspec'] },
rules: [
@@ -209,13 +209,16 @@ module Gitlab
end
let(:attributes) do
- [{ name: "build",
+ [{ name: ".pre",
index: 0,
builds: [] },
- { name: "test",
+ { name: "build",
index: 1,
+ builds: [] },
+ { name: "test",
+ index: 2,
builds:
- [{ stage_idx: 1,
+ [{ stage_idx: 2,
stage: "test",
name: "rspec",
allow_failure: false,
@@ -225,9 +228,9 @@ module Gitlab
only: { refs: ["branches"] },
except: {} }] },
{ name: "deploy",
- index: 2,
+ index: 3,
builds:
- [{ stage_idx: 2,
+ [{ stage_idx: 3,
stage: "deploy",
name: "prod",
allow_failure: false,
@@ -235,7 +238,10 @@ module Gitlab
yaml_variables: [],
options: { script: ["cap prod"] },
only: { refs: ["tags"] },
- except: {} }] }]
+ except: {} }] },
+ { name: ".post",
+ index: 4,
+ builds: [] }]
end
it 'returns stages seed attributes' do
@@ -425,7 +431,7 @@ module Gitlab
expect(config_processor.stage_builds_attributes("test").size).to eq(1)
expect(config_processor.stage_builds_attributes("test").first).to eq({
stage: "test",
- stage_idx: 1,
+ stage_idx: 2,
name: "rspec",
options: {
before_script: ["pwd"],
@@ -456,7 +462,7 @@ module Gitlab
expect(config_processor.stage_builds_attributes("test").size).to eq(1)
expect(config_processor.stage_builds_attributes("test").first).to eq({
stage: "test",
- stage_idx: 1,
+ stage_idx: 2,
name: "rspec",
options: {
before_script: ["pwd"],
@@ -485,7 +491,7 @@ module Gitlab
expect(config_processor.stage_builds_attributes("test").size).to eq(1)
expect(config_processor.stage_builds_attributes("test").first).to eq({
stage: "test",
- stage_idx: 1,
+ stage_idx: 2,
name: "rspec",
options: {
before_script: ["pwd"],
@@ -510,7 +516,7 @@ module Gitlab
expect(config_processor.stage_builds_attributes("test").size).to eq(1)
expect(config_processor.stage_builds_attributes("test").first).to eq({
stage: "test",
- stage_idx: 1,
+ stage_idx: 2,
name: "rspec",
options: {
before_script: ["pwd"],
@@ -977,7 +983,7 @@ module Gitlab
expect(config_processor.stage_builds_attributes("test").size).to eq(1)
expect(config_processor.stage_builds_attributes("test").first).to eq({
stage: "test",
- stage_idx: 1,
+ stage_idx: 2,
name: "rspec",
options: {
before_script: ["pwd"],
@@ -1272,7 +1278,7 @@ module Gitlab
expect(subject.builds.size).to eq(5)
expect(subject.builds[0]).to eq(
stage: "build",
- stage_idx: 0,
+ stage_idx: 1,
name: "build1",
options: {
script: ["test"]
@@ -1283,7 +1289,7 @@ module Gitlab
)
expect(subject.builds[2]).to eq(
stage: "test",
- stage_idx: 1,
+ stage_idx: 2,
name: "test1",
options: {
script: ["test"],
@@ -1398,7 +1404,7 @@ module Gitlab
expect(subject.size).to eq(1)
expect(subject.first).to eq({
stage: "test",
- stage_idx: 1,
+ stage_idx: 2,
name: "normal_job",
options: {
script: ["test"]
@@ -1442,7 +1448,7 @@ module Gitlab
expect(subject.size).to eq(2)
expect(subject.first).to eq({
stage: "build",
- stage_idx: 0,
+ stage_idx: 1,
name: "job1",
options: {
script: ["execute-script-for-job"]
@@ -1453,7 +1459,7 @@ module Gitlab
})
expect(subject.second).to eq({
stage: "build",
- stage_idx: 0,
+ stage_idx: 1,
name: "job2",
options: {
script: ["execute-script-for-job"]
@@ -1665,14 +1671,14 @@ module Gitlab
config = YAML.dump({ rspec: { script: "test", type: "acceptance" } })
expect do
Gitlab::Ci::YamlProcessor.new(config)
- end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "rspec job: stage parameter should be build, test, deploy")
+ end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "rspec job: stage parameter should be .pre, build, test, deploy, .post")
end
it "returns errors if job stage is not a defined stage" do
config = YAML.dump({ types: %w(build test), rspec: { script: "test", type: "acceptance" } })
expect do
Gitlab::Ci::YamlProcessor.new(config)
- end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "rspec job: stage parameter should be build, test")
+ end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "rspec job: stage parameter should be .pre, build, test, .post")
end
it "returns errors if stages is not an array" do
diff --git a/spec/lib/gitlab/cleanup/project_uploads_spec.rb b/spec/lib/gitlab/cleanup/project_uploads_spec.rb
index 7bad788e44e..5787cce7d20 100644
--- a/spec/lib/gitlab/cleanup/project_uploads_spec.rb
+++ b/spec/lib/gitlab/cleanup/project_uploads_spec.rb
@@ -4,6 +4,7 @@ require 'spec_helper'
describe Gitlab::Cleanup::ProjectUploads do
subject { described_class.new(logger: logger) }
+
let(:logger) { double(:logger) }
before do
diff --git a/spec/lib/gitlab/cluster/mixins/puma_cluster_spec.rb b/spec/lib/gitlab/cluster/mixins/puma_cluster_spec.rb
new file mode 100644
index 00000000000..1eddf488c5d
--- /dev/null
+++ b/spec/lib/gitlab/cluster/mixins/puma_cluster_spec.rb
@@ -0,0 +1,96 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+# For easier debugging set `PUMA_DEBUG=1`
+
+describe Gitlab::Cluster::Mixins::PumaCluster do
+ PUMA_STARTUP_TIMEOUT = 30
+
+ context 'when running Puma in Cluster-mode' do
+ %i[USR1 USR2 INT HUP].each do |signal|
+ it "for #{signal} does execute phased restart block" do
+ with_puma(workers: 1) do |pid|
+ Process.kill(signal, pid)
+
+ child_pid, child_status = Process.wait2(pid)
+ expect(child_pid).to eq(pid)
+ expect(child_status).to be_exited
+ expect(child_status.exitstatus).to eq(140)
+ end
+ end
+ end
+ end
+
+ private
+
+ def with_puma(workers:, timeout: PUMA_STARTUP_TIMEOUT)
+ with_puma_config(workers: workers) do |puma_rb|
+ cmdline = [
+ "bundle", "exec", "puma",
+ "-C", puma_rb,
+ "-I", Rails.root.to_s
+ ]
+
+ IO.popen(cmdline) do |process|
+ # wait for process to start:
+ # [2123] * Listening on tcp://127.0.0.1:0
+ wait_for_output(process, /Listening on/, timeout: timeout)
+ consume_output(process)
+
+ yield(process.pid)
+ ensure
+ begin
+ Process.kill(:KILL, process.pid)
+ rescue Errno::ESRCH
+ end
+ end
+ end
+ end
+
+ def with_puma_config(workers:)
+ Dir.mktmpdir do |dir|
+ File.write "#{dir}/puma.rb", <<-EOF
+ require './lib/gitlab/cluster/lifecycle_events'
+ require './lib/gitlab/cluster/mixins/puma_cluster'
+
+ workers #{workers}
+ bind "tcp://127.0.0.1:0"
+ preload_app!
+
+ app -> (env) { [404, {}, ['']] }
+
+ Puma::Cluster.prepend(#{described_class})
+
+ Gitlab::Cluster::LifecycleEvents.on_before_phased_restart do
+ exit(140)
+ end
+
+ # redirect stderr to stdout
+ $stderr.reopen($stdout)
+ EOF
+
+ yield("#{dir}/puma.rb")
+ end
+ end
+
+ def wait_for_output(process, output, timeout:)
+ Timeout.timeout(timeout) do
+ loop do
+ line = process.readline
+ puts "PUMA_DEBUG: #{line}" if ENV['PUMA_DEBUG']
+ break if line =~ output
+ end
+ end
+ end
+
+ def consume_output(process)
+ Thread.new do
+ loop do
+ line = process.readline
+ puts "PUMA_DEBUG: #{line}" if ENV['PUMA_DEBUG']
+ end
+ rescue
+ end
+ end
+end
diff --git a/spec/lib/gitlab/cluster/mixins/unicorn_http_server_spec.rb b/spec/lib/gitlab/cluster/mixins/unicorn_http_server_spec.rb
new file mode 100644
index 00000000000..2b3a267991c
--- /dev/null
+++ b/spec/lib/gitlab/cluster/mixins/unicorn_http_server_spec.rb
@@ -0,0 +1,112 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+# For easier debugging set `UNICORN_DEBUG=1`
+
+describe Gitlab::Cluster::Mixins::UnicornHttpServer do
+ UNICORN_STARTUP_TIMEOUT = 10
+
+ context 'when running Unicorn' do
+ %i[USR2].each do |signal|
+ it "for #{signal} does execute phased restart block" do
+ with_unicorn(workers: 1) do |pid|
+ Process.kill(signal, pid)
+
+ child_pid, child_status = Process.wait2(pid)
+ expect(child_pid).to eq(pid)
+ expect(child_status).to be_exited
+ expect(child_status.exitstatus).to eq(140)
+ end
+ end
+ end
+
+ %i[QUIT TERM INT].each do |signal|
+ it "for #{signal} does not execute phased restart block" do
+ with_unicorn(workers: 1) do |pid|
+ Process.kill(signal, pid)
+
+ child_pid, child_status = Process.wait2(pid)
+ expect(child_pid).to eq(pid)
+ expect(child_status).to be_exited
+ expect(child_status.exitstatus).to eq(0)
+ end
+ end
+ end
+ end
+
+ private
+
+ def with_unicorn(workers:, timeout: UNICORN_STARTUP_TIMEOUT)
+ with_unicorn_configs(workers: workers) do |unicorn_rb, config_ru|
+ cmdline = [
+ "bundle", "exec", "unicorn",
+ "-I", Rails.root.to_s,
+ "-c", unicorn_rb,
+ config_ru
+ ]
+
+ IO.popen(cmdline) do |process|
+ # wait for process to start:
+ # I, [2019-10-15T13:21:27.565225 #3089] INFO -- : master process ready
+ wait_for_output(process, /master process ready/, timeout: timeout)
+ consume_output(process)
+
+ yield(process.pid)
+ ensure
+ begin
+ Process.kill(:KILL, process.pid)
+ rescue Errno::ESRCH
+ end
+ end
+ end
+ end
+
+ def with_unicorn_configs(workers:)
+ Dir.mktmpdir do |dir|
+ File.write "#{dir}/unicorn.rb", <<-EOF
+ require './lib/gitlab/cluster/lifecycle_events'
+ require './lib/gitlab/cluster/mixins/unicorn_http_server'
+
+ worker_processes #{workers}
+ listen "127.0.0.1:0"
+ preload_app true
+
+ Unicorn::HttpServer.prepend(#{described_class})
+
+ Gitlab::Cluster::LifecycleEvents.on_before_phased_restart do
+ exit(140)
+ end
+
+ # redirect stderr to stdout
+ $stderr.reopen($stdout)
+ EOF
+
+ File.write "#{dir}/config.ru", <<-EOF
+ run -> (env) { [404, {}, ['']] }
+ EOF
+
+ yield("#{dir}/unicorn.rb", "#{dir}/config.ru")
+ end
+ end
+
+ def wait_for_output(process, output, timeout:)
+ Timeout.timeout(timeout) do
+ loop do
+ line = process.readline
+ puts "UNICORN_DEBUG: #{line}" if ENV['UNICORN_DEBUG']
+ break if line =~ output
+ end
+ end
+ end
+
+ def consume_output(process)
+ Thread.new do
+ loop do
+ line = process.readline
+ puts "UNICORN_DEBUG: #{line}" if ENV['UNICORN_DEBUG']
+ end
+ rescue
+ end
+ end
+end
diff --git a/spec/lib/gitlab/config/entry/simplifiable_spec.rb b/spec/lib/gitlab/config/entry/simplifiable_spec.rb
index 65e18fe3f10..5c208cab449 100644
--- a/spec/lib/gitlab/config/entry/simplifiable_spec.rb
+++ b/spec/lib/gitlab/config/entry/simplifiable_spec.rb
@@ -24,9 +24,9 @@ describe Gitlab::Config::Entry::Simplifiable do
let(:unknown) { double('unknown strategy') }
before do
- stub_const("#{described_class.name}::Something", first)
- stub_const("#{described_class.name}::DifferentOne", second)
- stub_const("#{described_class.name}::UnknownStrategy", unknown)
+ entry::Something = first
+ entry::DifferentOne = second
+ entry::UnknownStrategy = unknown
end
context 'when first strategy should be used' do
diff --git a/spec/lib/gitlab/cycle_analytics/code_stage_spec.rb b/spec/lib/gitlab/cycle_analytics/code_stage_spec.rb
index dd1d9ac0f16..aa12bc21d22 100644
--- a/spec/lib/gitlab/cycle_analytics/code_stage_spec.rb
+++ b/spec/lib/gitlab/cycle_analytics/code_stage_spec.rb
@@ -12,7 +12,8 @@ describe Gitlab::CycleAnalytics::CodeStage do
let(:issue_3) { create(:issue, project: project, created_at: 60.minutes.ago) }
let(:mr_1) { create(:merge_request, source_project: project, created_at: 15.minutes.ago) }
let(:mr_2) { create(:merge_request, source_project: project, created_at: 10.minutes.ago, source_branch: 'A') }
- let(:stage) { described_class.new(options: { from: 2.days.ago, current_user: project.creator, project: project }) }
+ let(:stage_options) { { from: 2.days.ago, current_user: project.creator, project: project } }
+ let(:stage) { described_class.new(options: stage_options) }
before do
issue_1.metrics.update!(first_associated_with_milestone_at: 60.minutes.ago, first_mentioned_in_commit_at: 45.minutes.ago)
@@ -25,6 +26,13 @@ describe Gitlab::CycleAnalytics::CodeStage do
it_behaves_like 'base stage'
+ context 'when using the new query backend' do
+ include_examples 'Gitlab::Analytics::CycleAnalytics::DataCollector backend examples' do
+ let(:expected_record_count) { 2 }
+ let(:expected_ordered_attribute_values) { [mr_2.title, mr_1.title] }
+ end
+ end
+
describe '#project_median' do
around do |example|
Timecop.freeze { example.run }
@@ -33,6 +41,8 @@ describe Gitlab::CycleAnalytics::CodeStage do
it 'counts median from issues with metrics' do
expect(stage.project_median).to eq(ISSUES_MEDIAN)
end
+
+ include_examples 'calculate #median with date range'
end
describe '#events' do
diff --git a/spec/lib/gitlab/cycle_analytics/issue_stage_spec.rb b/spec/lib/gitlab/cycle_analytics/issue_stage_spec.rb
index 4dd21239cde..497db88d850 100644
--- a/spec/lib/gitlab/cycle_analytics/issue_stage_spec.rb
+++ b/spec/lib/gitlab/cycle_analytics/issue_stage_spec.rb
@@ -10,7 +10,8 @@ describe Gitlab::CycleAnalytics::IssueStage do
let(:issue_2) { create(:issue, project: project, created_at: 60.minutes.ago) }
let(:issue_3) { create(:issue, project: project, created_at: 30.minutes.ago) }
let!(:issue_without_milestone) { create(:issue, project: project, created_at: 1.minute.ago) }
- let(:stage) { described_class.new(options: { from: 2.days.ago, current_user: project.creator, project: project }) }
+ let(:stage_options) { { from: 2.days.ago, current_user: project.creator, project: project } }
+ let(:stage) { described_class.new(options: stage_options) }
before do
issue_1.metrics.update!(first_associated_with_milestone_at: 60.minutes.ago )
@@ -20,6 +21,13 @@ describe Gitlab::CycleAnalytics::IssueStage do
it_behaves_like 'base stage'
+ context 'when using the new query backend' do
+ include_examples 'Gitlab::Analytics::CycleAnalytics::DataCollector backend examples' do
+ let(:expected_record_count) { 3 }
+ let(:expected_ordered_attribute_values) { [issue_3.title, issue_2.title, issue_1.title] }
+ end
+ end
+
describe '#median' do
around do |example|
Timecop.freeze { example.run }
@@ -28,6 +36,8 @@ describe Gitlab::CycleAnalytics::IssueStage do
it 'counts median from issues with metrics' do
expect(stage.project_median).to eq(ISSUES_MEDIAN)
end
+
+ include_examples 'calculate #median with date range'
end
describe '#events' do
diff --git a/spec/lib/gitlab/cycle_analytics/plan_stage_spec.rb b/spec/lib/gitlab/cycle_analytics/plan_stage_spec.rb
index 98d2593de66..01a46f5ba65 100644
--- a/spec/lib/gitlab/cycle_analytics/plan_stage_spec.rb
+++ b/spec/lib/gitlab/cycle_analytics/plan_stage_spec.rb
@@ -10,7 +10,8 @@ describe Gitlab::CycleAnalytics::PlanStage do
let!(:issue_2) { create(:issue, project: project, created_at: 60.minutes.ago) }
let!(:issue_3) { create(:issue, project: project, created_at: 30.minutes.ago) }
let!(:issue_without_milestone) { create(:issue, project: project, created_at: 1.minute.ago) }
- let(:stage) { described_class.new(options: { from: 2.days.ago, current_user: project.creator, project: project }) }
+ let(:stage_options) { { from: 2.days.ago, current_user: project.creator, project: project } }
+ let(:stage) { described_class.new(options: stage_options) }
before do
issue_1.metrics.update!(first_associated_with_milestone_at: 60.minutes.ago, first_mentioned_in_commit_at: 10.minutes.ago)
@@ -20,6 +21,13 @@ describe Gitlab::CycleAnalytics::PlanStage do
it_behaves_like 'base stage'
+ context 'when using the new query backend' do
+ include_examples 'Gitlab::Analytics::CycleAnalytics::DataCollector backend examples' do
+ let(:expected_record_count) { 2 }
+ let(:expected_ordered_attribute_values) { [issue_1.title, issue_2.title] }
+ end
+ end
+
describe '#project_median' do
around do |example|
Timecop.freeze { example.run }
@@ -28,6 +36,8 @@ describe Gitlab::CycleAnalytics::PlanStage do
it 'counts median from issues with metrics' do
expect(stage.project_median).to eq(ISSUES_MEDIAN)
end
+
+ include_examples 'calculate #median with date range'
end
describe '#events' do
diff --git a/spec/lib/gitlab/cycle_analytics/shared_stage_spec.rb b/spec/lib/gitlab/cycle_analytics/shared_stage_spec.rb
index cf95741908f..c5b17aafdd2 100644
--- a/spec/lib/gitlab/cycle_analytics/shared_stage_spec.rb
+++ b/spec/lib/gitlab/cycle_analytics/shared_stage_spec.rb
@@ -32,3 +32,41 @@ shared_examples 'base stage' do
expect(stage.events).not_to be_nil
end
end
+
+shared_examples 'calculate #median with date range' do
+ context 'when valid date range is given' do
+ before do
+ stage_options[:from] = 5.days.ago
+ stage_options[:to] = 5.days.from_now
+ end
+
+ it { expect(stage.project_median).to eq(ISSUES_MEDIAN) }
+ end
+
+ context 'when records are out of the date range' do
+ before do
+ stage_options[:from] = 2.years.ago
+ stage_options[:to] = 1.year.ago
+ end
+
+ it { expect(stage.project_median).to eq(nil) }
+ end
+end
+
+shared_examples 'Gitlab::Analytics::CycleAnalytics::DataCollector backend examples' do
+ let(:stage_params) { Gitlab::Analytics::CycleAnalytics::DefaultStages.send("params_for_#{stage_name}_stage").merge(project: project) }
+ let(:stage) { Analytics::CycleAnalytics::ProjectStage.new(stage_params) }
+ let(:data_collector) { Gitlab::Analytics::CycleAnalytics::DataCollector.new(stage: stage, params: { from: stage_options[:from], current_user: project.creator }) }
+ let(:attribute_to_verify) { :title }
+
+ context 'provides the same results as the old implementation' do
+ it 'for the median' do
+ expect(data_collector.median.seconds).to eq(ISSUES_MEDIAN)
+ end
+
+ it 'for the list of event records' do
+ records = data_collector.records_fetcher.serialized_records
+ expect(records.map { |event| event[attribute_to_verify] }).to eq(expected_ordered_attribute_values)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/cycle_analytics/stage_summary_spec.rb b/spec/lib/gitlab/cycle_analytics/stage_summary_spec.rb
index 778c2f479b5..8f9dac6d281 100644
--- a/spec/lib/gitlab/cycle_analytics/stage_summary_spec.rb
+++ b/spec/lib/gitlab/cycle_analytics/stage_summary_spec.rb
@@ -4,52 +4,98 @@ require 'spec_helper'
describe Gitlab::CycleAnalytics::StageSummary do
let(:project) { create(:project, :repository) }
- let(:from) { 1.day.ago }
+ let(:options) { { from: 1.day.ago, current_user: user } }
let(:user) { create(:user, :admin) }
- subject { described_class.new(project, from: Time.now, current_user: user).data }
+ let(:stage_summary) { described_class.new(project, options).data }
describe "#new_issues" do
+ subject { stage_summary.first[:value] }
+
it "finds the number of issues created after the 'from date'" do
Timecop.freeze(5.days.ago) { create(:issue, project: project) }
Timecop.freeze(5.days.from_now) { create(:issue, project: project) }
- expect(subject.first[:value]).to eq(1)
+ expect(subject).to eq(1)
end
it "doesn't find issues from other projects" do
Timecop.freeze(5.days.from_now) { create(:issue, project: create(:project)) }
- expect(subject.first[:value]).to eq(0)
+ expect(subject).to eq(0)
+ end
+
+ context 'when `to` parameter is given' do
+ before do
+ Timecop.freeze(5.days.ago) { create(:issue, project: project) }
+ Timecop.freeze(5.days.from_now) { create(:issue, project: project) }
+ end
+
+ it "doesn't find any record" do
+ options[:to] = Time.now
+
+ expect(subject).to eq(0)
+ end
+
+ it "finds records created between `from` and `to` range" do
+ options[:from] = 10.days.ago
+ options[:to] = 10.days.from_now
+
+ expect(subject).to eq(2)
+ end
end
end
describe "#commits" do
+ subject { stage_summary.second[:value] }
+
it "finds the number of commits created after the 'from date'" do
Timecop.freeze(5.days.ago) { create_commit("Test message", project, user, 'master') }
Timecop.freeze(5.days.from_now) { create_commit("Test message", project, user, 'master') }
- expect(subject.second[:value]).to eq(1)
+ expect(subject).to eq(1)
end
it "doesn't find commits from other projects" do
Timecop.freeze(5.days.from_now) { create_commit("Test message", create(:project, :repository), user, 'master') }
- expect(subject.second[:value]).to eq(0)
+ expect(subject).to eq(0)
end
it "finds a large (> 100) snumber of commits if present" do
Timecop.freeze(5.days.from_now) { create_commit("Test message", project, user, 'master', count: 100) }
- expect(subject.second[:value]).to eq(100)
+ expect(subject).to eq(100)
+ end
+
+ context 'when `to` parameter is given' do
+ before do
+ Timecop.freeze(5.days.ago) { create_commit("Test message", project, user, 'master') }
+ Timecop.freeze(5.days.from_now) { create_commit("Test message", project, user, 'master') }
+ end
+
+ it "doesn't find any record" do
+ options[:to] = Time.now
+
+ expect(subject).to eq(0)
+ end
+
+ it "finds records created between `from` and `to` range" do
+ options[:from] = 10.days.ago
+ options[:to] = 10.days.from_now
+
+ expect(subject).to eq(2)
+ end
end
end
describe "#deploys" do
+ subject { stage_summary.third[:value] }
+
it "finds the number of deploys made created after the 'from date'" do
Timecop.freeze(5.days.ago) { create(:deployment, :success, project: project) }
Timecop.freeze(5.days.from_now) { create(:deployment, :success, project: project) }
- expect(subject.third[:value]).to eq(1)
+ expect(subject).to eq(1)
end
it "doesn't find commits from other projects" do
@@ -57,7 +103,27 @@ describe Gitlab::CycleAnalytics::StageSummary do
create(:deployment, :success, project: create(:project, :repository))
end
- expect(subject.third[:value]).to eq(0)
+ expect(subject).to eq(0)
+ end
+
+ context 'when `to` parameter is given' do
+ before do
+ Timecop.freeze(5.days.ago) { create(:deployment, :success, project: project) }
+ Timecop.freeze(5.days.from_now) { create(:deployment, :success, project: project) }
+ end
+
+ it "doesn't find any record" do
+ options[:to] = Time.now
+
+ expect(subject).to eq(0)
+ end
+
+ it "finds records created between `from` and `to` range" do
+ options[:from] = 10.days.ago
+ options[:to] = 10.days.from_now
+
+ expect(subject).to eq(2)
+ end
end
end
end
diff --git a/spec/lib/gitlab/cycle_analytics/staging_stage_spec.rb b/spec/lib/gitlab/cycle_analytics/staging_stage_spec.rb
index bd64c4aca42..306b08a60e1 100644
--- a/spec/lib/gitlab/cycle_analytics/staging_stage_spec.rb
+++ b/spec/lib/gitlab/cycle_analytics/staging_stage_spec.rb
@@ -16,7 +16,8 @@ describe Gitlab::CycleAnalytics::StagingStage do
let(:build_1) { create(:ci_build, project: project) }
let(:build_2) { create(:ci_build, project: project) }
- let(:stage) { described_class.new(options: { from: 2.days.ago, current_user: project.creator, project: project }) }
+ let(:stage_options) { { from: 2.days.ago, current_user: project.creator, project: project } }
+ let(:stage) { described_class.new(options: stage_options) }
before do
mr_1.metrics.update!(merged_at: 80.minutes.ago, first_deployed_to_production_at: 50.minutes.ago, pipeline_id: build_1.commit_id)
@@ -38,6 +39,8 @@ describe Gitlab::CycleAnalytics::StagingStage do
it 'counts median from issues with metrics' do
expect(stage.project_median).to eq(ISSUES_MEDIAN)
end
+
+ it_behaves_like 'calculate #median with date range'
end
describe '#events' do
diff --git a/spec/lib/gitlab/cycle_analytics/test_stage_spec.rb b/spec/lib/gitlab/cycle_analytics/test_stage_spec.rb
index 9162686d17d..e347f36dfce 100644
--- a/spec/lib/gitlab/cycle_analytics/test_stage_spec.rb
+++ b/spec/lib/gitlab/cycle_analytics/test_stage_spec.rb
@@ -6,22 +6,26 @@ require 'lib/gitlab/cycle_analytics/shared_stage_spec'
describe Gitlab::CycleAnalytics::TestStage do
let(:stage_name) { :test }
let(:project) { create(:project) }
- let(:stage) { described_class.new(options: { from: 2.days.ago, current_user: project.creator, project: project }) }
+ let(:stage_options) { { from: 2.days.ago, current_user: project.creator, project: project } }
+ let(:stage) { described_class.new(options: stage_options) }
it_behaves_like 'base stage'
describe '#median' do
+ let(:mr_1) { create(:merge_request, :closed, source_project: project, created_at: 60.minutes.ago) }
+ let(:mr_2) { create(:merge_request, :closed, source_project: project, created_at: 40.minutes.ago, source_branch: 'A') }
+ let(:mr_3) { create(:merge_request, source_project: project, created_at: 10.minutes.ago, source_branch: 'B') }
+ let(:mr_4) { create(:merge_request, source_project: project, created_at: 10.minutes.ago, source_branch: 'C') }
+ let(:mr_5) { create(:merge_request, source_project: project, created_at: 10.minutes.ago, source_branch: 'D') }
+ let(:ci_build1) { create(:ci_build, project: project) }
+ let(:ci_build2) { create(:ci_build, project: project) }
+
before do
issue_1 = create(:issue, project: project, created_at: 90.minutes.ago)
issue_2 = create(:issue, project: project, created_at: 60.minutes.ago)
issue_3 = create(:issue, project: project, created_at: 60.minutes.ago)
- mr_1 = create(:merge_request, :closed, source_project: project, created_at: 60.minutes.ago)
- mr_2 = create(:merge_request, :closed, source_project: project, created_at: 40.minutes.ago, source_branch: 'A')
- mr_3 = create(:merge_request, source_project: project, created_at: 10.minutes.ago, source_branch: 'B')
- mr_4 = create(:merge_request, source_project: project, created_at: 10.minutes.ago, source_branch: 'C')
- mr_5 = create(:merge_request, source_project: project, created_at: 10.minutes.ago, source_branch: 'D')
- mr_1.metrics.update!(latest_build_started_at: 32.minutes.ago, latest_build_finished_at: 2.minutes.ago)
- mr_2.metrics.update!(latest_build_started_at: 62.minutes.ago, latest_build_finished_at: 32.minutes.ago)
+ mr_1.metrics.update!(latest_build_started_at: 32.minutes.ago, latest_build_finished_at: 2.minutes.ago, pipeline_id: ci_build1.commit_id)
+ mr_2.metrics.update!(latest_build_started_at: 62.minutes.ago, latest_build_finished_at: 32.minutes.ago, pipeline_id: ci_build2.commit_id)
mr_3.metrics.update!(latest_build_started_at: nil, latest_build_finished_at: nil)
mr_4.metrics.update!(latest_build_started_at: nil, latest_build_finished_at: nil)
mr_5.metrics.update!(latest_build_started_at: nil, latest_build_finished_at: nil)
@@ -40,5 +44,15 @@ describe Gitlab::CycleAnalytics::TestStage do
it 'counts median from issues with metrics' do
expect(stage.project_median).to eq(ISSUES_MEDIAN)
end
+
+ include_examples 'calculate #median with date range'
+
+ context 'when using the new query backend' do
+ include_examples 'Gitlab::Analytics::CycleAnalytics::DataCollector backend examples' do
+ let(:expected_record_count) { 2 }
+ let(:attribute_to_verify) { :id }
+ let(:expected_ordered_attribute_values) { [mr_1.metrics.pipeline.builds.first.id, mr_2.metrics.pipeline.builds.first.id] }
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/daemon_spec.rb b/spec/lib/gitlab/daemon_spec.rb
index 0372b770844..cf1f089c577 100644
--- a/spec/lib/gitlab/daemon_spec.rb
+++ b/spec/lib/gitlab/daemon_spec.rb
@@ -6,7 +6,7 @@ describe Gitlab::Daemon do
subject { described_class.new }
before do
- allow(subject).to receive(:start_working)
+ allow(subject).to receive(:run_thread)
allow(subject).to receive(:stop_working)
end
@@ -44,7 +44,7 @@ describe Gitlab::Daemon do
it 'starts the Daemon' do
expect { subject.start.join }.to change { subject.thread? }.from(false).to(true)
- expect(subject).to have_received(:start_working)
+ expect(subject).to have_received(:run_thread)
end
end
@@ -52,7 +52,21 @@ describe Gitlab::Daemon do
it "doesn't shutdown stopped Daemon" do
expect { subject.stop }.not_to change { subject.thread? }
- expect(subject).not_to have_received(:start_working)
+ expect(subject).not_to have_received(:run_thread)
+ end
+ end
+ end
+
+ describe '#start_working' do
+ context 'when start_working fails' do
+ before do
+ expect(subject).to receive(:start_working) { false }
+ end
+
+ it 'does not start thread' do
+ expect(subject).not_to receive(:run_thread)
+
+ expect(subject.start).to eq(nil)
end
end
end
@@ -66,7 +80,7 @@ describe Gitlab::Daemon do
it "doesn't start running Daemon" do
expect { subject.start.join }.not_to change { subject.thread }
- expect(subject).to have_received(:start_working).once
+ expect(subject).to have_received(:run_thread).once
end
end
@@ -79,7 +93,7 @@ describe Gitlab::Daemon do
context 'when stop_working raises exception' do
before do
- allow(subject).to receive(:start_working) do
+ allow(subject).to receive(:run_thread) do
sleep(1000)
end
end
@@ -108,7 +122,7 @@ describe Gitlab::Daemon do
expect(subject.start).to be_nil
expect { subject.start }.not_to change { subject.thread? }
- expect(subject).not_to have_received(:start_working)
+ expect(subject).not_to have_received(:run_thread)
end
end
diff --git a/spec/lib/gitlab/danger/helper_spec.rb b/spec/lib/gitlab/danger/helper_spec.rb
index 1b4d366ce7b..1696d3566ad 100644
--- a/spec/lib/gitlab/danger/helper_spec.rb
+++ b/spec/lib/gitlab/danger/helper_spec.rb
@@ -86,30 +86,30 @@ describe Gitlab::Danger::Helper do
describe '#ee?' do
subject { helper.ee? }
- it 'returns true if CI_PROJECT_NAME if set to gitlab-ee' do
- stub_env('CI_PROJECT_NAME', 'gitlab-ee')
- expect(File).not_to receive(:exist?)
+ it 'returns true if CI_PROJECT_NAME if set to gitlab' do
+ stub_env('CI_PROJECT_NAME', 'gitlab')
+ expect(Dir).not_to receive(:exist?)
is_expected.to be_truthy
end
it 'delegates to CHANGELOG-EE.md existence if CI_PROJECT_NAME is set to something else' do
stub_env('CI_PROJECT_NAME', 'something else')
- expect(File).to receive(:exist?).with('../../CHANGELOG-EE.md') { true }
+ expect(Dir).to receive(:exist?).with('../../ee') { true }
is_expected.to be_truthy
end
- it 'returns true if CHANGELOG-EE.md exists' do
+ it 'returns true if ee exists' do
stub_env('CI_PROJECT_NAME', nil)
- expect(File).to receive(:exist?).with('../../CHANGELOG-EE.md') { true }
+ expect(Dir).to receive(:exist?).with('../../ee') { true }
is_expected.to be_truthy
end
- it "returns false if CHANGELOG-EE.md doesn't exist" do
+ it "returns false if ee doesn't exist" do
stub_env('CI_PROJECT_NAME', nil)
- expect(File).to receive(:exist?).with('../../CHANGELOG-EE.md') { false }
+ expect(Dir).to receive(:exist?).with('../../ee') { false }
is_expected.to be_falsy
end
@@ -118,16 +118,16 @@ describe Gitlab::Danger::Helper do
describe '#project_name' do
subject { helper.project_name }
- it 'returns gitlab-ee if ee? returns true' do
+ it 'returns gitlab if ee? returns true' do
expect(helper).to receive(:ee?) { true }
- is_expected.to eq('gitlab-ee')
+ is_expected.to eq('gitlab')
end
it 'returns gitlab-ce if ee? returns false' do
expect(helper).to receive(:ee?) { false }
- is_expected.to eq('gitlab-ce')
+ is_expected.to eq('gitlab-foss')
end
end
@@ -273,7 +273,7 @@ describe Gitlab::Danger::Helper do
where(:category, :expected_label) do
:backend | '~backend'
:database | '~database'
- :docs | '~Documentation'
+ :docs | '~documentation'
:foo | '~foo'
:frontend | '~frontend'
:none | ''
diff --git a/spec/lib/gitlab/danger/roulette_spec.rb b/spec/lib/gitlab/danger/roulette_spec.rb
index 121c5d8ecd9..4d41e2c45aa 100644
--- a/spec/lib/gitlab/danger/roulette_spec.rb
+++ b/spec/lib/gitlab/danger/roulette_spec.rb
@@ -104,11 +104,13 @@ describe Gitlab::Danger::Roulette do
let(:person2) { Gitlab::Danger::Teammate.new('username' => 'godfat') }
let(:author) { Gitlab::Danger::Teammate.new('username' => 'filipa') }
let(:ooo) { Gitlab::Danger::Teammate.new('username' => 'jacopo-beschi') }
+ let(:no_capacity) { Gitlab::Danger::Teammate.new('username' => 'uncharged') }
before do
- stub_person_message(person1, 'making GitLab magic')
- stub_person_message(person2, 'making GitLab magic')
- stub_person_message(ooo, 'OOO till 15th')
+ stub_person_status(person1, message: 'making GitLab magic')
+ stub_person_status(person2, message: 'making GitLab magic')
+ stub_person_status(ooo, message: 'OOO till 15th')
+ stub_person_status(no_capacity, message: 'At capacity for the next few days', emoji: 'red_circle')
# we don't stub Filipa, as she is the author and
# we should not fire request checking for her
@@ -131,10 +133,14 @@ describe Gitlab::Danger::Roulette do
expect(subject.spin_for_person([author], random: Random.new)).to be_nil
end
+ it 'excludes person with no capacity' do
+ expect(subject.spin_for_person([no_capacity], random: Random.new)).to be_nil
+ end
+
private
- def stub_person_message(person, message)
- body = { message: message }.to_json
+ def stub_person_status(person, message: 'dummy message', emoji: 'unicorn')
+ body = { message: message, emoji: emoji }.to_json
WebMock
.stub_request(:get, "https://gitlab.com/api/v4/users/#{person.username}/status")
diff --git a/spec/lib/gitlab/danger/teammate_spec.rb b/spec/lib/gitlab/danger/teammate_spec.rb
index ca036390bde..bd1c2b10dc8 100644
--- a/spec/lib/gitlab/danger/teammate_spec.rb
+++ b/spec/lib/gitlab/danger/teammate_spec.rb
@@ -2,11 +2,14 @@
require 'fast_spec_helper'
+require 'rspec-parameterized'
+
require 'gitlab/danger/teammate'
describe Gitlab::Danger::Teammate do
- subject { described_class.new(options) }
- let(:options) { { 'projects' => projects, 'role' => role } }
+ subject { described_class.new(options.stringify_keys) }
+
+ let(:options) { { username: 'luigi', projects: projects, role: role } }
let(:projects) { { project => capabilities } }
let(:role) { 'Engineer, Manage' }
let(:labels) { [] }
@@ -95,4 +98,72 @@ describe Gitlab::Danger::Teammate do
expect(subject.maintainer?(project, :frontend, labels)).to be_falsey
end
end
+
+ describe '#status' do
+ let(:capabilities) { ['dish washing'] }
+
+ context 'with empty cache' do
+ context 'for successful request' do
+ it 'returns the response' do
+ mock_status = double(does_not: 'matter')
+ expect(Gitlab::Danger::RequestHelper).to receive(:http_get_json)
+ .and_return(mock_status)
+
+ expect(subject.status).to be mock_status
+ end
+ end
+
+ context 'for failing request' do
+ it 'returns nil' do
+ expect(Gitlab::Danger::RequestHelper).to receive(:http_get_json)
+ .and_raise(Gitlab::Danger::RequestHelper::HTTPError.new)
+
+ expect(subject.status).to be nil
+ end
+ end
+ end
+
+ context 'with filled cache' do
+ it 'returns the cached response' do
+ mock_status = double(does_not: 'matter')
+ expect(Gitlab::Danger::RequestHelper).to receive(:http_get_json)
+ .and_return(mock_status)
+ subject.status
+
+ expect(Gitlab::Danger::RequestHelper).not_to receive(:http_get_json)
+ expect(subject.status).to be mock_status
+ end
+ end
+ end
+
+ describe '#available?' do
+ using RSpec::Parameterized::TableSyntax
+
+ let(:capabilities) { ['dry head'] }
+
+ where(:status, :result) do
+ {} | true
+ { message: 'dear reader' } | true
+ { message: 'OOO: massage' } | false
+ { message: 'love it SOOO much' } | false
+ { emoji: 'red_circle' } | false
+ end
+
+ with_them do
+ before do
+ expect(Gitlab::Danger::RequestHelper).to receive(:http_get_json)
+ .and_return(status&.stringify_keys)
+ end
+
+ it { expect(subject.available?).to be result }
+ end
+
+ it 'returns true if request fails' do
+ expect(Gitlab::Danger::RequestHelper).to receive(:http_get_json)
+ .exactly(2).times
+ .and_raise(Gitlab::Danger::RequestHelper::HTTPError.new)
+
+ expect(subject.available?).to be true
+ end
+ end
end
diff --git a/spec/lib/gitlab/data_builder/push_spec.rb b/spec/lib/gitlab/data_builder/push_spec.rb
index e8a9f0b06a8..58509b69463 100644
--- a/spec/lib/gitlab/data_builder/push_spec.rb
+++ b/spec/lib/gitlab/data_builder/push_spec.rb
@@ -90,4 +90,12 @@ describe Gitlab::DataBuilder::Push do
.not_to raise_error
end
end
+
+ describe '.build_bulk' do
+ subject do
+ described_class.build_bulk(action: :created, ref_type: :branch, changes: [double, double])
+ end
+
+ it { is_expected.to eq(action: :created, ref_count: 2, ref_type: :branch) }
+ end
end
diff --git a/spec/lib/gitlab/database_spec.rb b/spec/lib/gitlab/database_spec.rb
index 8d37de32179..15fb1503529 100644
--- a/spec/lib/gitlab/database_spec.rb
+++ b/spec/lib/gitlab/database_spec.rb
@@ -101,20 +101,6 @@ describe Gitlab::Database do
end
end
- describe '.join_lateral_supported?' do
- it 'returns false when using PostgreSQL 9.2' do
- allow(described_class).to receive(:version).and_return('9.2.1')
-
- expect(described_class.join_lateral_supported?).to eq(false)
- end
-
- it 'returns true when using PostgreSQL 9.3.0 or newer' do
- allow(described_class).to receive(:version).and_return('9.3.0')
-
- expect(described_class.join_lateral_supported?).to eq(true)
- end
- end
-
describe '.replication_slots_supported?' do
it 'returns false when using PostgreSQL 9.3' do
allow(described_class).to receive(:version).and_return('9.3.1')
diff --git a/spec/lib/gitlab/diff/file_collection/merge_request_diff_batch_spec.rb b/spec/lib/gitlab/diff/file_collection/merge_request_diff_batch_spec.rb
new file mode 100644
index 00000000000..265c6260ca9
--- /dev/null
+++ b/spec/lib/gitlab/diff/file_collection/merge_request_diff_batch_spec.rb
@@ -0,0 +1,126 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Diff::FileCollection::MergeRequestDiffBatch do
+ let(:merge_request) { create(:merge_request) }
+ let(:batch_page) { 1 }
+ let(:batch_size) { 10 }
+ let(:diffable) { merge_request.merge_request_diff }
+ let(:diff_files_relation) { diffable.merge_request_diff_files }
+
+ subject do
+ described_class.new(diffable,
+ batch_page,
+ batch_size,
+ diff_options: nil)
+ end
+
+ let(:diff_files) { subject.diff_files }
+
+ describe 'initialize' do
+ it 'memoizes pagination_data' do
+ expect(subject.pagination_data).to eq(current_page: 1, next_page: 2, total_pages: 2)
+ end
+ end
+
+ describe '#diff_files' do
+ let(:batch_size) { 3 }
+ let(:paginated_rel) { diff_files_relation.page(batch_page).per(batch_size) }
+
+ let(:expected_batch_files) do
+ paginated_rel.map(&:new_path)
+ end
+
+ it 'returns paginated diff files' do
+ expect(diff_files.size).to eq(3)
+ end
+
+ it 'returns a valid instance of a DiffCollection' do
+ expect(diff_files).to be_a(Gitlab::Git::DiffCollection)
+ end
+
+ context 'first page' do
+ it 'returns correct diff files' do
+ expect(diff_files.map(&:new_path)).to eq(expected_batch_files)
+ end
+ end
+
+ context 'another page' do
+ let(:batch_page) { 2 }
+
+ it 'returns correct diff files' do
+ expect(diff_files.map(&:new_path)).to eq(expected_batch_files)
+ end
+ end
+
+ context 'nil batch_page' do
+ let(:batch_page) { nil }
+
+ it 'returns correct diff files' do
+ expected_batch_files =
+ diff_files_relation.page(described_class::DEFAULT_BATCH_PAGE).per(batch_size).map(&:new_path)
+
+ expect(diff_files.map(&:new_path)).to eq(expected_batch_files)
+ end
+ end
+
+ context 'nil batch_size' do
+ let(:batch_size) { nil }
+
+ it 'returns correct diff files' do
+ expected_batch_files =
+ diff_files_relation.page(batch_page).per(described_class::DEFAULT_BATCH_SIZE).map(&:new_path)
+
+ expect(diff_files.map(&:new_path)).to eq(expected_batch_files)
+ end
+ end
+
+ context 'invalid page' do
+ let(:batch_page) { 999 }
+
+ it 'returns correct diff files' do
+ expect(diff_files.map(&:new_path)).to be_empty
+ end
+ end
+
+ context 'last page' do
+ it 'returns correct diff files' do
+ last_page = paginated_rel.total_pages
+ collection = described_class.new(diffable,
+ last_page,
+ batch_size,
+ diff_options: nil)
+
+ expected_batch_files = diff_files_relation.page(last_page).per(batch_size).map(&:new_path)
+
+ expect(collection.diff_files.map(&:new_path)).to eq(expected_batch_files)
+ end
+ end
+ end
+
+ it_behaves_like 'unfoldable diff' do
+ subject do
+ described_class.new(merge_request.merge_request_diff,
+ batch_page,
+ batch_size,
+ diff_options: nil)
+ end
+ end
+
+ it_behaves_like 'diff statistics' do
+ let(:collection_default_args) do
+ { diff_options: {} }
+ end
+
+ let(:diffable) { merge_request.merge_request_diff }
+ let(:stub_path) { '.gitignore' }
+
+ subject do
+ described_class.new(merge_request.merge_request_diff,
+ batch_page,
+ batch_size,
+ collection_default_args)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/diff/position_collection_spec.rb b/spec/lib/gitlab/diff/position_collection_spec.rb
new file mode 100644
index 00000000000..f2a8312587c
--- /dev/null
+++ b/spec/lib/gitlab/diff/position_collection_spec.rb
@@ -0,0 +1,86 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Diff::PositionCollection do
+ let(:merge_request) { build(:merge_request) }
+
+ def build_text_position(attrs = {})
+ attributes = {
+ old_path: "files/ruby/popen.rb",
+ new_path: "files/ruby/popen.rb",
+ old_line: nil,
+ new_line: 14,
+ diff_refs: merge_request.diff_refs
+ }.merge(attrs)
+
+ Gitlab::Diff::Position.new(attributes)
+ end
+
+ def build_image_position(attrs = {})
+ attributes = {
+ old_path: "files/images/any_image.png",
+ new_path: "files/images/any_image.png",
+ width: 10,
+ height: 10,
+ x: 1,
+ y: 1,
+ diff_refs: merge_request.diff_refs,
+ position_type: "image"
+ }.merge(attrs)
+
+ Gitlab::Diff::Position.new(attributes)
+ end
+
+ let(:text_position) { build_text_position }
+ let(:folded_text_position) { build_text_position(old_line: 1, new_line: 1) }
+ let(:image_position) { build_image_position }
+ let(:invalid_position) { 'a position' }
+ let(:head_sha) { merge_request.diff_head_sha }
+
+ let(:collection) do
+ described_class.new([text_position, folded_text_position, image_position, invalid_position], head_sha)
+ end
+
+ describe '#to_a' do
+ it 'returns all positions that are Gitlab::Diff::Position' do
+ expect(collection.to_a).to eq([text_position, folded_text_position, image_position])
+ end
+ end
+
+ describe '#unfoldable' do
+ it 'returns unfoldable diff positions' do
+ expect(collection.unfoldable).to eq([folded_text_position])
+ end
+
+ context 'when given head_sha does not match with positions head_sha' do
+ let(:head_sha) { 'unknown' }
+
+ it 'returns no position' do
+ expect(collection.unfoldable).to be_empty
+ end
+ end
+
+ context 'when given head_sha is nil' do
+ let(:head_sha) { nil }
+
+ it 'returns unfoldable diff positions unfiltered by head_sha' do
+ expect(collection.unfoldable).to eq([folded_text_position])
+ end
+ end
+ end
+
+ describe '#concat' do
+ let(:new_text_position) { build_text_position(old_line: 1, new_line: 1) }
+
+ it 'returns a Gitlab::Diff::Position' do
+ expect(collection.concat([new_text_position])).to be_a(described_class)
+ end
+
+ it 'concatenates the new position to the collection' do
+ collection.concat([new_text_position])
+
+ expect(collection.to_a).to eq([text_position, folded_text_position, image_position, new_text_position])
+ end
+ end
+end
diff --git a/spec/lib/gitlab/diff/position_spec.rb b/spec/lib/gitlab/diff/position_spec.rb
index 399787635c0..839780b53fe 100644
--- a/spec/lib/gitlab/diff/position_spec.rb
+++ b/spec/lib/gitlab/diff/position_spec.rb
@@ -130,6 +130,26 @@ describe Gitlab::Diff::Position do
expect(diff_file.new_path).to eq(subject.new_path)
expect(diff_file.diff_refs).to eq(subject.diff_refs)
end
+
+ context 'different folded positions in the same diff file' do
+ def diff_file(args = {})
+ described_class
+ .new(args_for_text.merge(args))
+ .diff_file(project.repository)
+ end
+
+ it 'expands the diff file', :request_store do
+ expect_any_instance_of(Gitlab::Diff::File)
+ .to receive(:unfold_diff_lines).and_call_original
+
+ diff_file(old_line: 1, new_line: 1, diff_refs: commit.diff_refs)
+
+ expect_any_instance_of(Gitlab::Diff::File)
+ .to receive(:unfold_diff_lines).and_call_original
+
+ diff_file(old_line: 5, new_line: 5, diff_refs: commit.diff_refs)
+ end
+ end
end
describe "#diff_line" do
diff --git a/spec/lib/gitlab/discussions_diff/file_collection_spec.rb b/spec/lib/gitlab/discussions_diff/file_collection_spec.rb
index 6ef1e41450f..a13727b62ea 100644
--- a/spec/lib/gitlab/discussions_diff/file_collection_spec.rb
+++ b/spec/lib/gitlab/discussions_diff/file_collection_spec.rb
@@ -40,6 +40,14 @@ describe Gitlab::DiscussionsDiff::FileCollection do
subject.load_highlight
end
+ it 'does not write cache for empty mapping' do
+ allow(subject).to receive(:highlighted_lines_by_ids).and_return([])
+
+ expect(Gitlab::DiscussionsDiff::HighlightCache).not_to receive(:write_multiple)
+
+ subject.load_highlight
+ end
+
it 'does not write cache for resolved notes' do
diff_note_a.update_column(:resolved_at, Time.now)
diff --git a/spec/lib/gitlab/downtime_check_spec.rb b/spec/lib/gitlab/downtime_check_spec.rb
index 56ad49d528f..5a5e34961a4 100644
--- a/spec/lib/gitlab/downtime_check_spec.rb
+++ b/spec/lib/gitlab/downtime_check_spec.rb
@@ -4,6 +4,7 @@ require 'spec_helper'
describe Gitlab::DowntimeCheck do
subject { described_class.new }
+
let(:path) { 'foo.rb' }
describe '#check' do
diff --git a/spec/lib/gitlab/email/message/repository_push_spec.rb b/spec/lib/gitlab/email/message/repository_push_spec.rb
index 84c5b38127e..b57764bceef 100644
--- a/spec/lib/gitlab/email/message/repository_push_spec.rb
+++ b/spec/lib/gitlab/email/message/repository_push_spec.rb
@@ -28,90 +28,107 @@ describe Gitlab::Email::Message::RepositoryPush do
describe '#project' do
subject { message.project }
+
it { is_expected.to eq project }
it { is_expected.to be_an_instance_of Project }
end
describe '#project_namespace' do
subject { message.project_namespace }
+
it { is_expected.to eq group }
it { is_expected.to be_kind_of Namespace }
end
describe '#project_name_with_namespace' do
subject { message.project_name_with_namespace }
+
it { is_expected.to eq "#{group.name} / #{project.path}" }
end
describe '#author' do
subject { message.author }
+
it { is_expected.to eq author }
it { is_expected.to be_an_instance_of User }
end
describe '#author_name' do
subject { message.author_name }
+
it { is_expected.to eq 'Author' }
end
describe '#commits' do
subject { message.commits }
+
it { is_expected.to be_kind_of Array }
it { is_expected.to all(be_instance_of Commit) }
end
describe '#diffs' do
subject { message.diffs }
+
it { is_expected.to all(be_an_instance_of Gitlab::Diff::File) }
end
describe '#diffs_count' do
subject { message.diffs_count }
+
it { is_expected.to eq raw_compare.diffs.size }
end
describe '#compare' do
subject { message.compare }
+
it { is_expected.to be_an_instance_of Compare }
end
describe '#compare_timeout' do
subject { message.compare_timeout }
+
it { is_expected.to eq raw_compare.diffs.overflow? }
end
describe '#reverse_compare?' do
subject { message.reverse_compare? }
+
it { is_expected.to eq false }
end
describe '#disable_diffs?' do
subject { message.disable_diffs? }
+
it { is_expected.to eq false }
end
describe '#send_from_committer_email?' do
subject { message.send_from_committer_email? }
+
it { is_expected.to eq true }
end
describe '#action_name' do
subject { message.action_name }
+
it { is_expected.to eq 'pushed to' }
end
describe '#ref_name' do
subject { message.ref_name }
+
it { is_expected.to eq 'master' }
end
describe '#ref_type' do
subject { message.ref_type }
+
it { is_expected.to eq 'branch' }
end
describe '#target_url' do
subject { message.target_url }
+
it { is_expected.to include 'compare' }
it { is_expected.to include compare.commits.first.parents.first.id }
it { is_expected.to include compare.commits.last.id }
@@ -119,6 +136,7 @@ describe Gitlab::Email::Message::RepositoryPush do
describe '#subject' do
subject { message.subject }
+
it { is_expected.to include "[Git][#{project.full_path}]" }
it { is_expected.to include "#{compare.commits.length} commits" }
it { is_expected.to include compare.commits.first.message.split("\n").first }
diff --git a/spec/lib/gitlab/email/receiver_spec.rb b/spec/lib/gitlab/email/receiver_spec.rb
index 6b5a355e598..43c73242f5f 100644
--- a/spec/lib/gitlab/email/receiver_spec.rb
+++ b/spec/lib/gitlab/email/receiver_spec.rb
@@ -40,7 +40,15 @@ describe Gitlab::Email::Receiver do
end
end
- context "when the email was auto generated" do
+ context "when the email was auto generated with Auto-Submitted header" do
+ let(:email_raw) { fixture_file("emails/auto_submitted.eml") }
+
+ it "raises an AutoGeneratedEmailError" do
+ expect { receiver.execute }.to raise_error(Gitlab::Email::AutoGeneratedEmailError)
+ end
+ end
+
+ context "when the email was auto generated with X-Autoreply header" do
let(:email_raw) { fixture_file("emails/auto_reply.eml") }
it "raises an AutoGeneratedEmailError" do
diff --git a/spec/lib/gitlab/experimentation_spec.rb b/spec/lib/gitlab/experimentation_spec.rb
new file mode 100644
index 00000000000..2e5fd16d370
--- /dev/null
+++ b/spec/lib/gitlab/experimentation_spec.rb
@@ -0,0 +1,157 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Experimentation::ControllerConcern, type: :controller do
+ controller(ApplicationController) do
+ include Gitlab::Experimentation::ControllerConcern
+
+ def index
+ head :ok
+ end
+ end
+
+ describe '#set_experimentation_subject_id_cookie' do
+ before do
+ get :index
+ end
+
+ context 'cookie is present' do
+ before do
+ cookies[:experimentation_subject_id] = 'test'
+ end
+
+ it 'does not change the cookie' do
+ expect(cookies[:experimentation_subject_id]).to eq 'test'
+ end
+ end
+
+ context 'cookie is not present' do
+ it 'sets a permanent signed cookie' do
+ expect(cookies.permanent.signed[:experimentation_subject_id]).to be_present
+ end
+ end
+ end
+
+ describe '#experiment_enabled?' do
+ context 'cookie is not present' do
+ it 'calls Gitlab::Experimentation.enabled? with the name of the experiment and an experimentation_subject_index of nil' do
+ expect(Gitlab::Experimentation).to receive(:enabled?).with(:test_experiment, nil)
+ controller.experiment_enabled?(:test_experiment)
+ end
+ end
+
+ context 'cookie is present' do
+ before do
+ cookies.permanent.signed[:experimentation_subject_id] = 'abcd-1234'
+ get :index
+ end
+
+ it 'calls Gitlab::Experimentation.enabled? with the name of the experiment and an experimentation_subject_index of the modulo 100 of the hex value of the uuid' do
+ # 'abcd1234'.hex % 100 = 76
+ expect(Gitlab::Experimentation).to receive(:enabled?).with(:test_experiment, 76)
+ controller.experiment_enabled?(:test_experiment)
+ end
+ end
+ end
+end
+
+describe Gitlab::Experimentation do
+ before do
+ stub_const('Gitlab::Experimentation::EXPERIMENTS', {
+ test_experiment: {
+ feature_toggle: feature_toggle,
+ environment: environment,
+ enabled_ratio: enabled_ratio
+ }
+ })
+
+ stub_feature_flags(feature_toggle => true)
+ end
+
+ let(:feature_toggle) { :test_experiment_toggle }
+ let(:environment) { Rails.env.test? }
+ let(:enabled_ratio) { 0.1 }
+
+ describe '.enabled?' do
+ subject { described_class.enabled?(:test_experiment, experimentation_subject_index) }
+
+ let(:experimentation_subject_index) { 9 }
+
+ context 'feature toggle is enabled, we are on the right environment and we are selected' do
+ it { is_expected.to be_truthy }
+ end
+
+ describe 'experiment is not defined' do
+ it 'returns false' do
+ expect(described_class.enabled?(:missing_experiment, experimentation_subject_index)).to be_falsey
+ end
+ end
+
+ describe 'feature toggle' do
+ context 'feature toggle is not set' do
+ let(:feature_toggle) { nil }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'feature toggle is not set, but a feature with the experiment key as name does exist' do
+ before do
+ stub_feature_flags(test_experiment: false)
+ end
+
+ let(:feature_toggle) { nil }
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'feature toggle is disabled' do
+ before do
+ stub_feature_flags(feature_toggle => false)
+ end
+
+ it { is_expected.to be_falsey }
+ end
+ end
+
+ describe 'environment' do
+ context 'environment is not set' do
+ let(:environment) { nil }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'we are on the wrong environment' do
+ let(:environment) { ::Gitlab.com? }
+
+ it { is_expected.to be_falsey }
+ end
+ end
+
+ describe 'enabled ratio' do
+ context 'enabled ratio is not set' do
+ let(:enabled_ratio) { nil }
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'experimentation_subject_index is not set' do
+ let(:experimentation_subject_index) { nil }
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'experimentation_subject_index is an empty string' do
+ let(:experimentation_subject_index) { '' }
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'experimentation_subject_index outside enabled ratio' do
+ let(:experimentation_subject_index) { 11 }
+
+ it { is_expected.to be_falsey }
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/favicon_spec.rb b/spec/lib/gitlab/favicon_spec.rb
index d221f39c2ed..617c0f88a89 100644
--- a/spec/lib/gitlab/favicon_spec.rb
+++ b/spec/lib/gitlab/favicon_spec.rb
@@ -57,6 +57,7 @@ RSpec.describe Gitlab::Favicon, :request_store do
favicon_status_manual
favicon_status_not_found
favicon_status_pending
+ favicon_status_preparing
favicon_status_running
favicon_status_scheduled
favicon_status_skipped
diff --git a/spec/lib/gitlab/file_markdown_link_builder_spec.rb b/spec/lib/gitlab/file_markdown_link_builder_spec.rb
index d9e2e162ae8..de0ac9733e6 100644
--- a/spec/lib/gitlab/file_markdown_link_builder_spec.rb
+++ b/spec/lib/gitlab/file_markdown_link_builder_spec.rb
@@ -27,19 +27,35 @@ describe Gitlab::FileMarkdownLinkBuilder do
end
end
- context 'when file is an image or video' do
- let(:filename) { 'dk.png' }
+ context 'when file is an image' do
+ let(:filename) { 'my_image.png' }
it 'returns preview markdown link' do
- expect(custom_class.markdown_link).to eq '![dk](/uploads/dk.png)'
+ expect(custom_class.markdown_link).to eq '![my_image](/uploads/my_image.png)'
end
end
- context 'when file is not an image or video' do
- let(:filename) { 'dk.zip' }
+ context 'when file is video' do
+ let(:filename) { 'my_video.mp4' }
+
+ it 'returns preview markdown link' do
+ expect(custom_class.markdown_link).to eq '![my_video](/uploads/my_video.mp4)'
+ end
+ end
+
+ context 'when file is audio' do
+ let(:filename) { 'my_audio.wav' }
+
+ it 'returns preview markdown link' do
+ expect(custom_class.markdown_link).to eq '![my_audio](/uploads/my_audio.wav)'
+ end
+ end
+
+ context 'when file is not embeddable' do
+ let(:filename) { 'my_zip.zip' }
it 'returns markdown link' do
- expect(custom_class.markdown_link).to eq '[dk.zip](/uploads/dk.zip)'
+ expect(custom_class.markdown_link).to eq '[my_zip.zip](/uploads/my_zip.zip)'
end
end
@@ -53,19 +69,35 @@ describe Gitlab::FileMarkdownLinkBuilder do
end
describe 'mardown_name' do
- context 'when file is an image or video' do
- let(:filename) { 'dk.png' }
+ context 'when file is an image' do
+ let(:filename) { 'my_image.png' }
+
+ it 'retrieves the name without the extension' do
+ expect(custom_class.markdown_name).to eq 'my_image'
+ end
+ end
+
+ context 'when file is video' do
+ let(:filename) { 'my_video.mp4' }
+
+ it 'retrieves the name without the extension' do
+ expect(custom_class.markdown_name).to eq 'my_video'
+ end
+ end
+
+ context 'when file is audio' do
+ let(:filename) { 'my_audio.wav' }
it 'retrieves the name without the extension' do
- expect(custom_class.markdown_name).to eq 'dk'
+ expect(custom_class.markdown_name).to eq 'my_audio'
end
end
- context 'when file is not an image or video' do
- let(:filename) { 'dk.zip' }
+ context 'when file is not embeddable' do
+ let(:filename) { 'my_zip.zip' }
it 'retrieves the name with the extesion' do
- expect(custom_class.markdown_name).to eq 'dk.zip'
+ expect(custom_class.markdown_name).to eq 'my_zip.zip'
end
end
diff --git a/spec/lib/gitlab/file_type_detection_spec.rb b/spec/lib/gitlab/file_type_detection_spec.rb
index 22ec7d414e8..05008bf895c 100644
--- a/spec/lib/gitlab/file_type_detection_spec.rb
+++ b/spec/lib/gitlab/file_type_detection_spec.rb
@@ -2,38 +2,298 @@
require 'spec_helper'
describe Gitlab::FileTypeDetection do
- def upload_fixture(filename)
- fixture_file_upload(File.join('spec', 'fixtures', filename))
- end
+ context 'when class is an uploader' do
+ let(:uploader) do
+ example_uploader = Class.new(CarrierWave::Uploader::Base) do
+ include Gitlab::FileTypeDetection
+
+ storage :file
+ end
- describe '#image_or_video?' do
- context 'when class is an uploader' do
- let(:uploader) do
- example_uploader = Class.new(CarrierWave::Uploader::Base) do
- include Gitlab::FileTypeDetection
+ example_uploader.new
+ end
+
+ def upload_fixture(filename)
+ fixture_file_upload(File.join('spec', 'fixtures', filename))
+ end
+
+ describe '#image?' do
+ it 'returns true for an image file' do
+ uploader.store!(upload_fixture('dk.png'))
+
+ expect(uploader).to be_image
+ end
+
+ it 'returns false if filename has a dangerous image extension' do
+ uploader.store!(upload_fixture('unsanitized.svg'))
+
+ expect(uploader).to be_dangerous_image
+ expect(uploader).not_to be_image
+ end
+
+ it 'returns false for a video file' do
+ uploader.store!(upload_fixture('video_sample.mp4'))
+
+ expect(uploader).not_to be_image
+ end
+
+ it 'returns false for an audio file' do
+ uploader.store!(upload_fixture('audio_sample.wav'))
+
+ expect(uploader).not_to be_image
+ end
+
+ it 'returns false if filename is blank' do
+ uploader.store!(upload_fixture('dk.png'))
+
+ allow(uploader).to receive(:filename).and_return(nil)
+
+ expect(uploader).not_to be_image
+ end
+ end
+
+ describe '#video?' do
+ it 'returns true for a video file' do
+ uploader.store!(upload_fixture('video_sample.mp4'))
+
+ expect(uploader).to be_video
+ end
+
+ it 'returns false for an image file' do
+ uploader.store!(upload_fixture('dk.png'))
+
+ expect(uploader).not_to be_video
+ end
+
+ it 'returns false for an audio file' do
+ uploader.store!(upload_fixture('audio_sample.wav'))
+
+ expect(uploader).not_to be_video
+ end
- storage :file
- end
+ it 'returns false if file has a dangerous image extension' do
+ uploader.store!(upload_fixture('unsanitized.svg'))
- example_uploader.new
+ expect(uploader).to be_dangerous_image
+ expect(uploader).not_to be_video
end
+ it 'returns false if filename is blank' do
+ uploader.store!(upload_fixture('video_sample.mp4'))
+
+ allow(uploader).to receive(:filename).and_return(nil)
+
+ expect(uploader).not_to be_video
+ end
+ end
+
+ describe '#audio?' do
+ it 'returns true for an audio file' do
+ uploader.store!(upload_fixture('audio_sample.wav'))
+
+ expect(uploader).to be_audio
+ end
+
+ it 'returns false for an image file' do
+ uploader.store!(upload_fixture('dk.png'))
+
+ expect(uploader).not_to be_audio
+ end
+
+ it 'returns false for a video file' do
+ uploader.store!(upload_fixture('video_sample.mp4'))
+
+ expect(uploader).not_to be_audio
+ end
+
+ it 'returns false if file has a dangerous image extension' do
+ uploader.store!(upload_fixture('unsanitized.svg'))
+
+ expect(uploader).to be_dangerous_image
+ expect(uploader).not_to be_audio
+ end
+
+ it 'returns false if filename is blank' do
+ uploader.store!(upload_fixture('audio_sample.wav'))
+
+ allow(uploader).to receive(:filename).and_return(nil)
+
+ expect(uploader).not_to be_audio
+ end
+ end
+
+ describe '#embeddable?' do
it 'returns true for an image file' do
uploader.store!(upload_fixture('dk.png'))
- expect(uploader).to be_image_or_video
+ expect(uploader).to be_embeddable
end
it 'returns true for a video file' do
uploader.store!(upload_fixture('video_sample.mp4'))
- expect(uploader).to be_image_or_video
+ expect(uploader).to be_embeddable
+ end
+
+ it 'returns true for an audio file' do
+ uploader.store!(upload_fixture('audio_sample.wav'))
+
+ expect(uploader).to be_embeddable
+ end
+
+ it 'returns false if not an embeddable file' do
+ uploader.store!(upload_fixture('doc_sample.txt'))
+
+ expect(uploader).not_to be_embeddable
+ end
+
+ it 'returns false if filename has a dangerous image extension' do
+ uploader.store!(upload_fixture('unsanitized.svg'))
+
+ expect(uploader).to be_dangerous_image
+ expect(uploader).not_to be_embeddable
+ end
+
+ it 'returns false if filename is blank' do
+ uploader.store!(upload_fixture('dk.png'))
+
+ allow(uploader).to receive(:filename).and_return(nil)
+
+ expect(uploader).not_to be_embeddable
+ end
+ end
+
+ describe '#dangerous_image?' do
+ it 'returns true if filename has a dangerous extension' do
+ uploader.store!(upload_fixture('unsanitized.svg'))
+
+ expect(uploader).to be_dangerous_image
+ end
+
+ it 'returns false for an image file' do
+ uploader.store!(upload_fixture('dk.png'))
+
+ expect(uploader).not_to be_dangerous_image
+ end
+
+ it 'returns false for a video file' do
+ uploader.store!(upload_fixture('video_sample.mp4'))
+
+ expect(uploader).not_to be_dangerous_image
+ end
+
+ it 'returns false for an audio file' do
+ uploader.store!(upload_fixture('audio_sample.wav'))
+
+ expect(uploader).not_to be_dangerous_image
+ end
+
+ it 'returns false if filename is blank' do
+ uploader.store!(upload_fixture('dk.png'))
+
+ allow(uploader).to receive(:filename).and_return(nil)
+
+ expect(uploader).not_to be_dangerous_image
+ end
+ end
+
+ describe '#dangerous_video?' do
+ it 'returns false for a safe video file' do
+ uploader.store!(upload_fixture('video_sample.mp4'))
+
+ expect(uploader).not_to be_dangerous_video
+ end
+
+ it 'returns false if filename is a dangerous image extension' do
+ uploader.store!(upload_fixture('unsanitized.svg'))
+
+ expect(uploader).not_to be_dangerous_video
+ end
+
+ it 'returns false for an image file' do
+ uploader.store!(upload_fixture('dk.png'))
+
+ expect(uploader).not_to be_dangerous_video
+ end
+
+ it 'returns false for an audio file' do
+ uploader.store!(upload_fixture('audio_sample.wav'))
+
+ expect(uploader).not_to be_dangerous_video
+ end
+
+ it 'returns false if filename is blank' do
+ uploader.store!(upload_fixture('dk.png'))
+
+ allow(uploader).to receive(:filename).and_return(nil)
+
+ expect(uploader).not_to be_dangerous_video
+ end
+ end
+
+ describe '#dangerous_audio?' do
+ it 'returns false for a safe audio file' do
+ uploader.store!(upload_fixture('audio_sample.wav'))
+
+ expect(uploader).not_to be_dangerous_audio
+ end
+
+ it 'returns false if filename is a dangerous image extension' do
+ uploader.store!(upload_fixture('unsanitized.svg'))
+
+ expect(uploader).not_to be_dangerous_audio
+ end
+
+ it 'returns false for an image file' do
+ uploader.store!(upload_fixture('dk.png'))
+
+ expect(uploader).not_to be_dangerous_audio
+ end
+
+ it 'returns false for an video file' do
+ uploader.store!(upload_fixture('video_sample.mp4'))
+
+ expect(uploader).not_to be_dangerous_audio
+ end
+
+ it 'returns false if filename is blank' do
+ uploader.store!(upload_fixture('dk.png'))
+
+ allow(uploader).to receive(:filename).and_return(nil)
+
+ expect(uploader).not_to be_dangerous_audio
+ end
+ end
+
+ describe '#dangerous_embeddable?' do
+ it 'returns true if filename has a dangerous image extension' do
+ uploader.store!(upload_fixture('unsanitized.svg'))
+
+ expect(uploader).to be_dangerous_embeddable
+ end
+
+ it 'returns false for an image file' do
+ uploader.store!(upload_fixture('dk.png'))
+
+ expect(uploader).not_to be_dangerous_embeddable
+ end
+
+ it 'returns false for a video file' do
+ uploader.store!(upload_fixture('video_sample.mp4'))
+
+ expect(uploader).not_to be_dangerous_embeddable
end
- it 'returns false for other extensions' do
+ it 'returns false for an audio file' do
+ uploader.store!(upload_fixture('audio_sample.wav'))
+
+ expect(uploader).not_to be_dangerous_embeddable
+ end
+
+ it 'returns false for a non-embeddable file' do
uploader.store!(upload_fixture('doc_sample.txt'))
- expect(uploader).not_to be_image_or_video
+ expect(uploader).not_to be_dangerous_embeddable
end
it 'returns false if filename is blank' do
@@ -41,41 +301,289 @@ describe Gitlab::FileTypeDetection do
allow(uploader).to receive(:filename).and_return(nil)
- expect(uploader).not_to be_image_or_video
+ expect(uploader).not_to be_dangerous_embeddable
end
end
+ end
- context 'when class is a regular class' do
- let(:custom_class) do
- custom_class = Class.new do
- include Gitlab::FileTypeDetection
- end
+ context 'when class is a regular class' do
+ let(:custom_class) do
+ custom_class = Class.new do
+ include Gitlab::FileTypeDetection
+ end
+
+ custom_class.new
+ end
+
+ describe '#image?' do
+ it 'returns true for an image file' do
+ allow(custom_class).to receive(:filename).and_return('dk.png')
+
+ expect(custom_class).to be_image
+ end
+
+ it 'returns false if file has a dangerous image extension' do
+ allow(custom_class).to receive(:filename).and_return('unsanitized.svg')
+
+ expect(custom_class).to be_dangerous_image
+ expect(custom_class).not_to be_image
+ end
+
+ it 'returns false for a video file' do
+ allow(custom_class).to receive(:filename).and_return('video_sample.mp4')
- custom_class.new
+ expect(custom_class).not_to be_image
end
+ it 'returns false for an audio file' do
+ allow(custom_class).to receive(:filename).and_return('audio_sample.wav')
+
+ expect(custom_class).not_to be_image
+ end
+
+ it 'returns false if filename is blank' do
+ allow(custom_class).to receive(:filename).and_return(nil)
+
+ expect(custom_class).not_to be_image
+ end
+ end
+
+ describe '#video?' do
+ it 'returns true for a video file' do
+ allow(custom_class).to receive(:filename).and_return('video_sample.mp4')
+
+ expect(custom_class).to be_video
+ end
+
+ it 'returns false for an image file' do
+ allow(custom_class).to receive(:filename).and_return('dk.png')
+
+ expect(custom_class).not_to be_video
+ end
+
+ it 'returns false for an audio file' do
+ allow(custom_class).to receive(:filename).and_return('audio_sample.wav')
+
+ expect(custom_class).not_to be_video
+ end
+
+ it 'returns false if file has a dangerous image extension' do
+ allow(custom_class).to receive(:filename).and_return('unsanitized.svg')
+
+ expect(custom_class).to be_dangerous_image
+ expect(custom_class).not_to be_video
+ end
+
+ it 'returns false if filename is blank' do
+ allow(custom_class).to receive(:filename).and_return(nil)
+
+ expect(custom_class).not_to be_video
+ end
+ end
+
+ describe '#audio?' do
+ it 'returns true for an audio file' do
+ allow(custom_class).to receive(:filename).and_return('audio_sample.wav')
+
+ expect(custom_class).to be_audio
+ end
+
+ it 'returns false for an image file' do
+ allow(custom_class).to receive(:filename).and_return('dk.png')
+
+ expect(custom_class).not_to be_audio
+ end
+
+ it 'returns false for a video file' do
+ allow(custom_class).to receive(:filename).and_return('video_sample.mp4')
+
+ expect(custom_class).not_to be_audio
+ end
+
+ it 'returns false if file has a dangerous image extension' do
+ allow(custom_class).to receive(:filename).and_return('unsanitized.svg')
+
+ expect(custom_class).to be_dangerous_image
+ expect(custom_class).not_to be_audio
+ end
+
+ it 'returns false if filename is blank' do
+ allow(custom_class).to receive(:filename).and_return(nil)
+
+ expect(custom_class).not_to be_audio
+ end
+ end
+
+ describe '#embeddable?' do
it 'returns true for an image file' do
allow(custom_class).to receive(:filename).and_return('dk.png')
- expect(custom_class).to be_image_or_video
+ expect(custom_class).to be_embeddable
end
it 'returns true for a video file' do
allow(custom_class).to receive(:filename).and_return('video_sample.mp4')
- expect(custom_class).to be_image_or_video
+ expect(custom_class).to be_embeddable
+ end
+
+ it 'returns true for an audio file' do
+ allow(custom_class).to receive(:filename).and_return('audio_sample.wav')
+
+ expect(custom_class).to be_embeddable
+ end
+
+ it 'returns false if not an embeddable file' do
+ allow(custom_class).to receive(:filename).and_return('doc_sample.txt')
+
+ expect(custom_class).not_to be_embeddable
+ end
+
+ it 'returns false if filename has a dangerous image extension' do
+ allow(custom_class).to receive(:filename).and_return('unsanitized.svg')
+
+ expect(custom_class).to be_dangerous_image
+ expect(custom_class).not_to be_embeddable
+ end
+
+ it 'returns false if filename is blank' do
+ allow(custom_class).to receive(:filename).and_return(nil)
+
+ expect(custom_class).not_to be_embeddable
+ end
+ end
+
+ describe '#dangerous_image?' do
+ it 'returns true if file has a dangerous image extension' do
+ allow(custom_class).to receive(:filename).and_return('unsanitized.svg')
+
+ expect(custom_class).to be_dangerous_image
+ end
+
+ it 'returns false for an image file' do
+ allow(custom_class).to receive(:filename).and_return('dk.png')
+
+ expect(custom_class).not_to be_dangerous_image
+ end
+
+ it 'returns false for a video file' do
+ allow(custom_class).to receive(:filename).and_return('video_sample.mp4')
+
+ expect(custom_class).not_to be_dangerous_image
+ end
+
+ it 'returns false for an audio file' do
+ allow(custom_class).to receive(:filename).and_return('audio_sample.wav')
+
+ expect(custom_class).not_to be_dangerous_image
+ end
+
+ it 'returns false if filename is blank' do
+ allow(custom_class).to receive(:filename).and_return(nil)
+
+ expect(custom_class).not_to be_dangerous_image
+ end
+ end
+
+ describe '#dangerous_video?' do
+ it 'returns false for a safe video file' do
+ allow(custom_class).to receive(:filename).and_return('video_sample.mp4')
+
+ expect(custom_class).not_to be_dangerous_video
+ end
+
+ it 'returns false for an image file' do
+ allow(custom_class).to receive(:filename).and_return('dk.png')
+
+ expect(custom_class).not_to be_dangerous_video
+ end
+
+ it 'returns false for an audio file' do
+ allow(custom_class).to receive(:filename).and_return('audio_sample.wav')
+
+ expect(custom_class).not_to be_dangerous_video
+ end
+
+ it 'returns false if file has a dangerous image extension' do
+ allow(custom_class).to receive(:filename).and_return('unsanitized.svg')
+
+ expect(custom_class).not_to be_dangerous_video
+ end
+
+ it 'returns false if filename is blank' do
+ allow(custom_class).to receive(:filename).and_return(nil)
+
+ expect(custom_class).not_to be_dangerous_video
+ end
+ end
+
+ describe '#dangerous_audio?' do
+ it 'returns false for a safe audio file' do
+ allow(custom_class).to receive(:filename).and_return('audio_sample.wav')
+
+ expect(custom_class).not_to be_dangerous_audio
+ end
+
+ it 'returns false for an image file' do
+ allow(custom_class).to receive(:filename).and_return('dk.png')
+
+ expect(custom_class).not_to be_dangerous_audio
+ end
+
+ it 'returns false for a video file' do
+ allow(custom_class).to receive(:filename).and_return('video_sample.mp4')
+
+ expect(custom_class).not_to be_dangerous_audio
+ end
+
+ it 'returns false if file has a dangerous image extension' do
+ allow(custom_class).to receive(:filename).and_return('unsanitized.svg')
+
+ expect(custom_class).not_to be_dangerous_audio
+ end
+
+ it 'returns false if filename is blank' do
+ allow(custom_class).to receive(:filename).and_return(nil)
+
+ expect(custom_class).not_to be_dangerous_audio
+ end
+ end
+
+ describe '#dangerous_embeddable?' do
+ it 'returns true if file has a dangerous image extension' do
+ allow(custom_class).to receive(:filename).and_return('unsanitized.svg')
+
+ expect(custom_class).to be_dangerous_embeddable
+ end
+
+ it 'returns false for an image file' do
+ allow(custom_class).to receive(:filename).and_return('dk.png')
+
+ expect(custom_class).not_to be_dangerous_embeddable
+ end
+
+ it 'returns false for a video file' do
+ allow(custom_class).to receive(:filename).and_return('video_sample.mp4')
+
+ expect(custom_class).not_to be_dangerous_embeddable
+ end
+
+ it 'returns false for an audio file' do
+ allow(custom_class).to receive(:filename).and_return('audio_sample.wav')
+
+ expect(custom_class).not_to be_dangerous_embeddable
end
- it 'returns false for other extensions' do
+ it 'returns false for a non-embeddable file' do
allow(custom_class).to receive(:filename).and_return('doc_sample.txt')
- expect(custom_class).not_to be_image_or_video
+ expect(custom_class).not_to be_dangerous_embeddable
end
it 'returns false if filename is blank' do
allow(custom_class).to receive(:filename).and_return(nil)
- expect(custom_class).not_to be_image_or_video
+ expect(custom_class).not_to be_dangerous_embeddable
end
end
end
diff --git a/spec/lib/gitlab/gfm/uploads_rewriter_spec.rb b/spec/lib/gitlab/gfm/uploads_rewriter_spec.rb
index d24f5c45107..eef3b9de476 100644
--- a/spec/lib/gitlab/gfm/uploads_rewriter_spec.rb
+++ b/spec/lib/gitlab/gfm/uploads_rewriter_spec.rb
@@ -84,11 +84,13 @@ describe Gitlab::Gfm::UploadsRewriter do
describe '#needs_rewrite?' do
subject { rewriter.needs_rewrite? }
+
it { is_expected.to eq true }
end
describe '#files' do
subject { rewriter.files }
+
it { is_expected.to be_an(Array) }
end
end
diff --git a/spec/lib/gitlab/git/branch_spec.rb b/spec/lib/gitlab/git/branch_spec.rb
index 0764e525ede..02ef7b92538 100644
--- a/spec/lib/gitlab/git/branch_spec.rb
+++ b/spec/lib/gitlab/git/branch_spec.rb
@@ -44,6 +44,7 @@ describe Gitlab::Git::Branch, :seed_helper do
describe '#size' do
subject { super().size }
+
it { is_expected.to eq(SeedRepo::Repo::BRANCHES.size) }
end
diff --git a/spec/lib/gitlab/git/changes_spec.rb b/spec/lib/gitlab/git/changes_spec.rb
new file mode 100644
index 00000000000..7f56d30cb48
--- /dev/null
+++ b/spec/lib/gitlab/git/changes_spec.rb
@@ -0,0 +1,81 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Git::Changes do
+ let(:changes) { described_class.new }
+
+ describe '#includes_branches?' do
+ subject { changes.includes_branches? }
+
+ context 'has changes for branches' do
+ before do
+ changes.add_branch_change(oldrev: 'abc123', newrev: 'def456', ref: 'branch')
+ end
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'has no changes for branches' do
+ before do
+ changes.add_tag_change(oldrev: 'abc123', newrev: 'def456', ref: 'tag')
+ end
+
+ it { is_expected.to be_falsey }
+ end
+ end
+
+ describe '#includes_tags?' do
+ subject { changes.includes_tags? }
+
+ context 'has changes for tags' do
+ before do
+ changes.add_tag_change(oldrev: 'abc123', newrev: 'def456', ref: 'tag')
+ end
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'has no changes for tags' do
+ before do
+ changes.add_branch_change(oldrev: 'abc123', newrev: 'def456', ref: 'branch')
+ end
+
+ it { is_expected.to be_falsey }
+ end
+ end
+
+ describe '#add_branch_change' do
+ let(:change) { { oldrev: 'abc123', newrev: 'def456', ref: 'branch' } }
+
+ subject { changes.add_branch_change(change) }
+
+ it 'adds the branch change to the collection' do
+ expect(subject).to include(change)
+ expect(subject.refs).to include(change[:ref])
+ expect(subject.repository_data).to include(before: change[:oldrev], after: change[:newrev], ref: change[:ref])
+ expect(subject.branch_changes).to include(change)
+ end
+
+ it 'does not add the change as a tag change' do
+ expect(subject.tag_changes).not_to include(change)
+ end
+ end
+
+ describe '#add_tag_change' do
+ let(:change) { { oldrev: 'abc123', newrev: 'def456', ref: 'tag' } }
+
+ subject { changes.add_tag_change(change) }
+
+ it 'adds the tag change to the collection' do
+ expect(subject).to include(change)
+ expect(subject.refs).to include(change[:ref])
+ expect(subject.repository_data).to include(before: change[:oldrev], after: change[:newrev], ref: change[:ref])
+ expect(subject.tag_changes).to include(change)
+ end
+
+ it 'does not add the change as a branch change' do
+ expect(subject.branch_changes).not_to include(change)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/git/commit_spec.rb b/spec/lib/gitlab/git/commit_spec.rb
index 3f0e6b34291..23651e3d7f2 100644
--- a/spec/lib/gitlab/git/commit_spec.rb
+++ b/spec/lib/gitlab/git/commit_spec.rb
@@ -174,6 +174,7 @@ describe Gitlab::Git::Commit, :seed_helper do
describe '#id' do
subject { super().id }
+
it { is_expected.to eq(SeedRepo::LastCommit::ID) }
end
end
@@ -183,6 +184,7 @@ describe Gitlab::Git::Commit, :seed_helper do
describe '#id' do
subject { super().id }
+
it { is_expected.to eq(SeedRepo::Commit::ID) }
end
end
@@ -192,6 +194,7 @@ describe Gitlab::Git::Commit, :seed_helper do
describe '#id' do
subject { super().id }
+
it { is_expected.to eq(SeedRepo::BigCommit::ID) }
end
end
@@ -544,11 +547,13 @@ describe Gitlab::Git::Commit, :seed_helper do
describe '#id' do
subject { super().id }
+
it { is_expected.to eq(sample_commit_hash[:id])}
end
describe '#message' do
subject { super().message }
+
it { is_expected.to eq(sample_commit_hash[:message])}
end
end
@@ -558,16 +563,19 @@ describe Gitlab::Git::Commit, :seed_helper do
describe '#additions' do
subject { super().additions }
+
it { is_expected.to eq(11) }
end
describe '#deletions' do
subject { super().deletions }
+
it { is_expected.to eq(6) }
end
describe '#total' do
subject { super().total }
+
it { is_expected.to eq(17) }
end
end
@@ -596,6 +604,7 @@ describe Gitlab::Git::Commit, :seed_helper do
describe '#keys' do
subject { super().keys.sort }
+
it { is_expected.to match(sample_commit_hash.keys.sort) }
end
end
diff --git a/spec/lib/gitlab/git/diff_collection_spec.rb b/spec/lib/gitlab/git/diff_collection_spec.rb
index be6ab0c1200..ce45d6e24ba 100644
--- a/spec/lib/gitlab/git/diff_collection_spec.rb
+++ b/spec/lib/gitlab/git/diff_collection_spec.rb
@@ -10,6 +10,7 @@ describe Gitlab::Git::DiffCollection, :seed_helper do
expanded: expanded
)
end
+
let(:iterator) { MutatingConstantIterator.new(file_count, fake_diff(line_length, line_count)) }
let(:file_count) { 0 }
let(:line_length) { 1 }
@@ -21,6 +22,7 @@ describe Gitlab::Git::DiffCollection, :seed_helper do
describe '#to_a' do
subject { super().to_a }
+
it { is_expected.to be_kind_of ::Array }
end
@@ -52,16 +54,19 @@ describe Gitlab::Git::DiffCollection, :seed_helper do
describe '#overflow?' do
subject { super().overflow? }
+
it { is_expected.to be_falsey }
end
describe '#empty?' do
subject { super().empty? }
+
it { is_expected.to be_falsey }
end
describe '#real_size' do
subject { super().real_size }
+
it { is_expected.to eq('3') }
end
@@ -76,6 +81,7 @@ describe Gitlab::Git::DiffCollection, :seed_helper do
describe '#line_count' do
subject { super().line_count }
+
it { is_expected.to eq file_count * line_count }
end
@@ -84,16 +90,19 @@ describe Gitlab::Git::DiffCollection, :seed_helper do
describe '#overflow?' do
subject { super().overflow? }
+
it { is_expected.to be_falsey }
end
describe '#empty?' do
subject { super().empty? }
+
it { is_expected.to be_falsey }
end
describe '#real_size' do
subject { super().real_size }
+
it { is_expected.to eq('3') }
end
@@ -108,6 +117,7 @@ describe Gitlab::Git::DiffCollection, :seed_helper do
describe '#line_count' do
subject { super().line_count }
+
it { is_expected.to eq file_count * line_count }
end
end
@@ -118,21 +128,25 @@ describe Gitlab::Git::DiffCollection, :seed_helper do
describe '#overflow?' do
subject { super().overflow? }
+
it { is_expected.to be_truthy }
end
describe '#empty?' do
subject { super().empty? }
+
it { is_expected.to be_falsey }
end
describe '#real_size' do
subject { super().real_size }
+
it { is_expected.to eq('0+') }
end
describe '#line_count' do
subject { super().line_count }
+
it { is_expected.to eq 1000 }
end
@@ -143,21 +157,25 @@ describe Gitlab::Git::DiffCollection, :seed_helper do
describe '#overflow?' do
subject { super().overflow? }
+
it { is_expected.to be_falsey }
end
describe '#empty?' do
subject { super().empty? }
+
it { is_expected.to be_falsey }
end
describe '#real_size' do
subject { super().real_size }
+
it { is_expected.to eq('3') }
end
describe '#line_count' do
subject { super().line_count }
+
it { is_expected.to eq file_count * line_count }
end
@@ -174,21 +192,25 @@ describe Gitlab::Git::DiffCollection, :seed_helper do
describe '#overflow?' do
subject { super().overflow? }
+
it { is_expected.to be_truthy }
end
describe '#empty?' do
subject { super().empty? }
+
it { is_expected.to be_falsey }
end
describe '#real_size' do
subject { super().real_size }
+
it { is_expected.to eq('10+') }
end
describe '#line_count' do
subject { super().line_count }
+
it { is_expected.to eq 10 }
end
@@ -199,21 +221,25 @@ describe Gitlab::Git::DiffCollection, :seed_helper do
describe '#overflow?' do
subject { super().overflow? }
+
it { is_expected.to be_falsey }
end
describe '#empty?' do
subject { super().empty? }
+
it { is_expected.to be_falsey }
end
describe '#real_size' do
subject { super().real_size }
+
it { is_expected.to eq('11') }
end
describe '#line_count' do
subject { super().line_count }
+
it { is_expected.to eq file_count * line_count }
end
@@ -226,21 +252,25 @@ describe Gitlab::Git::DiffCollection, :seed_helper do
describe '#overflow?' do
subject { super().overflow? }
+
it { is_expected.to be_truthy }
end
describe '#empty?' do
subject { super().empty? }
+
it { is_expected.to be_falsey }
end
describe '#real_size' do
subject { super().real_size }
+
it { is_expected.to eq('3+') }
end
describe '#line_count' do
subject { super().line_count }
+
it { is_expected.to eq 120 }
end
@@ -251,21 +281,25 @@ describe Gitlab::Git::DiffCollection, :seed_helper do
describe '#overflow?' do
subject { super().overflow? }
+
it { is_expected.to be_falsey }
end
describe '#empty?' do
subject { super().empty? }
+
it { is_expected.to be_falsey }
end
describe '#real_size' do
subject { super().real_size }
+
it { is_expected.to eq('11') }
end
describe '#line_count' do
subject { super().line_count }
+
it { is_expected.to eq file_count * line_count }
end
@@ -282,21 +316,25 @@ describe Gitlab::Git::DiffCollection, :seed_helper do
describe '#overflow?' do
subject { super().overflow? }
+
it { is_expected.to be_falsey }
end
describe '#empty?' do
subject { super().empty? }
+
it { is_expected.to be_falsey }
end
describe '#real_size' do
subject { super().real_size }
+
it { is_expected.to eq('10') }
end
describe '#line_count' do
subject { super().line_count }
+
it { is_expected.to eq file_count * line_count }
end
@@ -310,21 +348,25 @@ describe Gitlab::Git::DiffCollection, :seed_helper do
describe '#overflow?' do
subject { super().overflow? }
+
it { is_expected.to be_truthy }
end
describe '#empty?' do
subject { super().empty? }
+
it { is_expected.to be_falsey }
end
describe '#real_size' do
subject { super().real_size }
+
it { is_expected.to eq('9+') }
end
describe '#line_count' do
subject { super().line_count }
+
it { is_expected.to eq file_count * line_count }
end
@@ -335,21 +377,25 @@ describe Gitlab::Git::DiffCollection, :seed_helper do
describe '#overflow?' do
subject { super().overflow? }
+
it { is_expected.to be_falsey }
end
describe '#empty?' do
subject { super().empty? }
+
it { is_expected.to be_falsey }
end
describe '#real_size' do
subject { super().real_size }
+
it { is_expected.to eq('10') }
end
describe '#line_count' do
subject { super().line_count }
+
it { is_expected.to eq file_count * line_count }
end
@@ -363,26 +409,31 @@ describe Gitlab::Git::DiffCollection, :seed_helper do
describe '#overflow?' do
subject { super().overflow? }
+
it { is_expected.to be_falsey }
end
describe '#empty?' do
subject { super().empty? }
+
it { is_expected.to be_truthy }
end
describe '#size' do
subject { super().size }
+
it { is_expected.to eq(0) }
end
describe '#real_size' do
subject { super().real_size }
+
it { is_expected.to eq('0')}
end
describe '#line_count' do
subject { super().line_count }
+
it { is_expected.to eq 0 }
end
end
@@ -537,6 +588,70 @@ describe Gitlab::Git::DiffCollection, :seed_helper do
end
end
end
+
+ context 'when offset_index is given' do
+ subject do
+ Gitlab::Git::DiffCollection.new(
+ iterator,
+ max_files: max_files,
+ max_lines: max_lines,
+ limits: limits,
+ offset_index: 2,
+ expanded: expanded
+ )
+ end
+
+ def diff(raw)
+ raw['diff']
+ end
+
+ let(:iterator) do
+ [
+ fake_diff(1, 1),
+ fake_diff(2, 2),
+ fake_diff(3, 3),
+ fake_diff(4, 4)
+ ]
+ end
+
+ it 'does not yield diffs before the offset' do
+ expect(subject.to_a.map(&:diff)).to eq(
+ [
+ diff(fake_diff(3, 3)),
+ diff(fake_diff(4, 4))
+ ]
+ )
+ end
+
+ context 'when go over safe limits on bytes' do
+ let(:iterator) do
+ [
+ fake_diff(1, 10), # 10
+ fake_diff(1, 10), # 20
+ fake_diff(1, 15), # 35
+ fake_diff(1, 20), # 55
+ fake_diff(1, 45), # 100 - limit hit
+ fake_diff(1, 45),
+ fake_diff(1, 20480),
+ fake_diff(1, 1)
+ ]
+ end
+
+ before do
+ stub_const('Gitlab::Git::DiffCollection::DEFAULT_LIMITS',
+ { max_files: max_files, max_lines: 80 })
+ end
+
+ it 'considers size of diffs before the offset for prunning' do
+ expect(subject.to_a.map(&:diff)).to eq(
+ [
+ diff(fake_diff(1, 15)),
+ diff(fake_diff(1, 20))
+ ]
+ )
+ end
+ end
+ end
end
def fake_diff(line_length, line_count)
diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb
index dcb7401b695..44c41da7560 100644
--- a/spec/lib/gitlab/git/repository_spec.rb
+++ b/spec/lib/gitlab/git/repository_spec.rb
@@ -93,6 +93,7 @@ describe Gitlab::Git::Repository, :seed_helper do
describe '#last' do
subject { super().last }
+
it { is_expected.to eq("v1.2.1") }
end
it { is_expected.to include("v1.0.0") }
@@ -215,11 +216,13 @@ describe Gitlab::Git::Repository, :seed_helper do
describe '#first' do
subject { super().first }
+
it { is_expected.to eq('feature') }
end
describe '#last' do
subject { super().last }
+
it { is_expected.to eq('v1.2.1') }
end
end
@@ -2236,4 +2239,45 @@ describe Gitlab::Git::Repository, :seed_helper do
expect(repository.commit(new_commit.oid).id).to eq(new_commit.oid)
end
end
+
+ describe '#rename' do
+ let(:project) { create(:project, :repository)}
+ let(:repository) { project.repository }
+
+ it 'moves the repository' do
+ checksum = repository.checksum
+ new_relative_path = "rename_test/relative/path"
+ renamed_repository = Gitlab::Git::Repository.new(repository.storage, new_relative_path, nil, nil)
+
+ repository.rename(new_relative_path)
+
+ expect(renamed_repository.checksum).to eq(checksum)
+ expect(repository.exists?).to be false
+ end
+ end
+
+ describe '#remove' do
+ let(:project) { create(:project, :repository) }
+ let(:repository) { project.repository }
+
+ it 'removes the repository' do
+ expect(repository.exists?).to be true
+
+ repository.remove
+
+ expect(repository.raw_repository.exists?).to be false
+ end
+
+ context 'when the repository does not exist' do
+ let(:repository) { create(:project).repository }
+
+ it 'is idempotent' do
+ expect(repository.exists?).to be false
+
+ repository.remove
+
+ expect(repository.raw_repository.exists?).to be false
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/git_access_spec.rb b/spec/lib/gitlab/git_access_spec.rb
index d584cdbe280..81dc96b538a 100644
--- a/spec/lib/gitlab/git_access_spec.rb
+++ b/spec/lib/gitlab/git_access_spec.rb
@@ -541,6 +541,13 @@ describe Gitlab::GitAccess do
expect { pull_access_check }.to raise_unauthorized('Your account has been blocked.')
end
+ it 'disallows deactivated users to pull' do
+ project.add_maintainer(user)
+ user.deactivate!
+
+ expect { pull_access_check }.to raise_unauthorized("Your account has been deactivated by your administrator. Please log back in from a web browser to reactivate your account at #{Gitlab.config.gitlab.url}")
+ end
+
context 'when the project repository does not exist' do
it 'returns not found' do
project.add_guest(user)
@@ -925,6 +932,12 @@ describe Gitlab::GitAccess do
project.add_developer(user)
end
+ it 'does not allow deactivated users to push' do
+ user.deactivate!
+
+ expect { push_access_check }.to raise_unauthorized("Your account has been deactivated by your administrator. Please log back in from a web browser to reactivate your account at #{Gitlab.config.gitlab.url}")
+ end
+
it 'cleans up the files' do
expect(project.repository).to receive(:clean_stale_repository_files).and_call_original
expect { push_access_check }.not_to raise_error
diff --git a/spec/lib/gitlab/gitaly_client/commit_service_spec.rb b/spec/lib/gitlab/gitaly_client/commit_service_spec.rb
index ba6abba4e61..71489adb373 100644
--- a/spec/lib/gitlab/gitaly_client/commit_service_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/commit_service_spec.rb
@@ -252,31 +252,6 @@ describe Gitlab::GitalyClient::CommitService do
end
end
- describe '#patch' do
- let(:request) do
- Gitaly::CommitPatchRequest.new(
- repository: repository_message, revision: revision
- )
- end
- let(:response) { [double(data: "my "), double(data: "diff")] }
-
- subject { described_class.new(repository).patch(revision) }
-
- it 'sends an RPC request' do
- expect_any_instance_of(Gitaly::DiffService::Stub).to receive(:commit_patch)
- .with(request, kind_of(Hash)).and_return([])
-
- subject
- end
-
- it 'concatenates the responses data' do
- allow_any_instance_of(Gitaly::DiffService::Stub).to receive(:commit_patch)
- .with(request, kind_of(Hash)).and_return(response)
-
- expect(subject).to eq("my diff")
- end
- end
-
describe '#commit_stats' do
let(:request) do
Gitaly::CommitStatsRequest.new(
diff --git a/spec/lib/gitlab/gitaly_client/conflict_files_stitcher_spec.rb b/spec/lib/gitlab/gitaly_client/conflict_files_stitcher_spec.rb
index 1c933410bd5..a3602463756 100644
--- a/spec/lib/gitlab/gitaly_client/conflict_files_stitcher_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/conflict_files_stitcher_spec.rb
@@ -32,7 +32,7 @@ describe Gitlab::GitalyClient::ConflictFilesStitcher do
double(files: [double(header: nil, content: content_2[11..-1])])
]
- conflict_files = described_class.new(messages).to_a
+ conflict_files = described_class.new(messages, target_repository.gitaly_repository).to_a
expect(conflict_files.size).to be(2)
diff --git a/spec/lib/gitlab/gitaly_client/repository_service_spec.rb b/spec/lib/gitlab/gitaly_client/repository_service_spec.rb
index a3808adb376..f4b73931f21 100644
--- a/spec/lib/gitlab/gitaly_client/repository_service_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/repository_service_spec.rb
@@ -272,4 +272,26 @@ describe Gitlab::GitalyClient::RepositoryService do
end
end
end
+
+ describe 'remove' do
+ it 'sends a remove_repository message' do
+ expect_any_instance_of(Gitaly::RepositoryService::Stub)
+ .to receive(:remove_repository)
+ .with(gitaly_request_with_path(storage_name, relative_path), kind_of(Hash))
+ .and_return(double(value: true))
+
+ client.remove
+ end
+ end
+
+ describe 'rename' do
+ it 'sends a rename_repository message' do
+ expect_any_instance_of(Gitaly::RepositoryService::Stub)
+ .to receive(:rename_repository)
+ .with(gitaly_request_with_path(storage_name, relative_path), kind_of(Hash))
+ .and_return(double(value: true))
+
+ client.rename('some/new/path')
+ end
+ end
end
diff --git a/spec/lib/gitlab/gitaly_client/storage_service_spec.rb b/spec/lib/gitlab/gitaly_client/storage_service_spec.rb
deleted file mode 100644
index 6c25e2d6ebd..00000000000
--- a/spec/lib/gitlab/gitaly_client/storage_service_spec.rb
+++ /dev/null
@@ -1,13 +0,0 @@
-require 'spec_helper'
-
-describe Gitlab::GitalyClient::StorageService do
- describe '#delete_all_repositories' do
- let!(:project) { create(:project, :repository) }
-
- it 'removes all repositories' do
- described_class.new(project.repository_storage).delete_all_repositories
-
- expect(project.repository.exists?).to be(false)
- end
- end
-end
diff --git a/spec/lib/gitlab/gitaly_client/storage_settings_spec.rb b/spec/lib/gitlab/gitaly_client/storage_settings_spec.rb
index f2f53982b09..2f83e5a5221 100644
--- a/spec/lib/gitlab/gitaly_client/storage_settings_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/storage_settings_spec.rb
@@ -27,6 +27,38 @@ describe Gitlab::GitalyClient::StorageSettings do
end
end
+ describe '.gitaly_address' do
+ context 'when the storage settings have no gitaly address but one is requested' do
+ it 'raises an error' do
+ expect do
+ described_class.new("path" => Rails.root).gitaly_address
+ end.to raise_error("key not found: \"gitaly_address\"")
+ end
+ end
+
+ context 'when the storage settings have a gitaly address and one is requested' do
+ it 'returns the setting value' do
+ expect(described_class.new("path" => Rails.root, "gitaly_address" => "test").gitaly_address).to eq("test")
+ end
+ end
+
+ context 'when the storage settings have a gitaly address keyed symbolically' do
+ it 'raises no error' do
+ expect do
+ described_class.new("path" => Rails.root, :gitaly_address => "test").gitaly_address
+ end.not_to raise_error
+ end
+ end
+
+ context 'when the storage settings have a gitaly address keyed with a string' do
+ it 'raises no error' do
+ expect do
+ described_class.new("path" => Rails.root, "gitaly_address" => "test").gitaly_address
+ end.not_to raise_error
+ end
+ end
+ end
+
describe '.disk_access_denied?' do
context 'when Rugged is enabled', :enable_rugged do
it 'returns false' do
diff --git a/spec/lib/gitlab/gitaly_client_spec.rb b/spec/lib/gitlab/gitaly_client_spec.rb
index 1c5f72a4396..ea3bb12d049 100644
--- a/spec/lib/gitlab/gitaly_client_spec.rb
+++ b/spec/lib/gitlab/gitaly_client_spec.rb
@@ -182,24 +182,24 @@ describe Gitlab::GitalyClient do
end
it 'sets the gitaly-session-id in the metadata' do
- results = described_class.request_kwargs('default', nil)
+ results = described_class.request_kwargs('default', timeout: 1)
expect(results[:metadata]).to include('gitaly-session-id')
end
context 'when RequestStore is not enabled' do
it 'sets a different gitaly-session-id per request' do
- gitaly_session_id = described_class.request_kwargs('default', nil)[:metadata]['gitaly-session-id']
+ gitaly_session_id = described_class.request_kwargs('default', timeout: 1)[:metadata]['gitaly-session-id']
- expect(described_class.request_kwargs('default', nil)[:metadata]['gitaly-session-id']).not_to eq(gitaly_session_id)
+ expect(described_class.request_kwargs('default', timeout: 1)[:metadata]['gitaly-session-id']).not_to eq(gitaly_session_id)
end
end
context 'when RequestStore is enabled', :request_store do
it 'sets the same gitaly-session-id on every outgoing request metadata' do
- gitaly_session_id = described_class.request_kwargs('default', nil)[:metadata]['gitaly-session-id']
+ gitaly_session_id = described_class.request_kwargs('default', timeout: 1)[:metadata]['gitaly-session-id']
3.times do
- expect(described_class.request_kwargs('default', nil)[:metadata]['gitaly-session-id']).to eq(gitaly_session_id)
+ expect(described_class.request_kwargs('default', timeout: 1)[:metadata]['gitaly-session-id']).to eq(gitaly_session_id)
end
end
end
diff --git a/spec/lib/gitlab/github_import/importer/pull_request_importer_spec.rb b/spec/lib/gitlab/github_import/importer/pull_request_importer_spec.rb
index 6d614c6527a..8331f0b6bc7 100644
--- a/spec/lib/gitlab/github_import/importer/pull_request_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/pull_request_importer_spec.rb
@@ -311,10 +311,11 @@ describe Gitlab::GithubImport::Importer::PullRequestImporter, :clean_gitlab_redi
end
end
- it 'creates the merge request diffs' do
+ it 'creates a merge request diff and sets it as the latest' do
mr = insert_git_data
expect(mr.merge_request_diffs.exists?).to eq(true)
+ expect(mr.reload.latest_merge_request_diff_id).to eq(mr.merge_request_diffs.first.id)
end
it 'creates the merge request diff commits' do
diff --git a/spec/lib/gitlab/github_import/importer/releases_importer_spec.rb b/spec/lib/gitlab/github_import/importer/releases_importer_spec.rb
index 0e5419e6c5e..6a31c57a73d 100644
--- a/spec/lib/gitlab/github_import/importer/releases_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/releases_importer_spec.rb
@@ -4,17 +4,17 @@ describe Gitlab::GithubImport::Importer::ReleasesImporter do
let(:project) { create(:project) }
let(:client) { double(:client) }
let(:importer) { described_class.new(project, client) }
+ let(:github_release_name) { 'Initial Release' }
let(:created_at) { Time.new(2017, 1, 1, 12, 00) }
- let(:updated_at) { Time.new(2017, 1, 1, 12, 15) }
let(:released_at) { Time.new(2017, 1, 1, 12, 00) }
- let(:release) do
+ let(:github_release) do
double(
- :release,
+ :github_release,
tag_name: '1.0',
+ name: github_release_name,
body: 'This is my release',
created_at: created_at,
- updated_at: updated_at,
published_at: released_at
)
end
@@ -25,7 +25,7 @@ describe Gitlab::GithubImport::Importer::ReleasesImporter do
tag_name: '1.0',
description: 'This is my release',
created_at: created_at,
- updated_at: updated_at,
+ updated_at: created_at,
released_at: released_at
}
@@ -34,11 +34,27 @@ describe Gitlab::GithubImport::Importer::ReleasesImporter do
importer.execute
end
+
+ it 'imports draft releases' do
+ release_double = double(
+ name: 'Test',
+ body: 'This is description',
+ tag_name: '1.0',
+ description: 'This is my release',
+ created_at: created_at,
+ updated_at: created_at,
+ published_at: nil
+ )
+
+ expect(importer).to receive(:each_release).and_return([release_double])
+
+ expect { importer.execute }.to change { Release.count }.by(1)
+ end
end
describe '#build_releases' do
- it 'returns an Array containnig release rows' do
- expect(importer).to receive(:each_release).and_return([release])
+ it 'returns an Array containing release rows' do
+ expect(importer).to receive(:each_release).and_return([github_release])
rows = importer.build_releases
@@ -49,13 +65,13 @@ describe Gitlab::GithubImport::Importer::ReleasesImporter do
it 'does not create releases that already exist' do
create(:release, project: project, tag: '1.0', description: '1.0')
- expect(importer).to receive(:each_release).and_return([release])
+ expect(importer).to receive(:each_release).and_return([github_release])
expect(importer.build_releases).to be_empty
end
it 'uses a default release description if none is provided' do
- expect(release).to receive(:body).and_return('')
- expect(importer).to receive(:each_release).and_return([release])
+ expect(github_release).to receive(:body).and_return('')
+ expect(importer).to receive(:each_release).and_return([github_release])
release = importer.build_releases.first
@@ -64,7 +80,7 @@ describe Gitlab::GithubImport::Importer::ReleasesImporter do
end
describe '#build' do
- let(:release_hash) { importer.build(release) }
+ let(:release_hash) { importer.build(github_release) }
it 'returns the attributes of the release as a Hash' do
expect(release_hash).to be_an_instance_of(Hash)
@@ -88,13 +104,17 @@ describe Gitlab::GithubImport::Importer::ReleasesImporter do
end
it 'includes the updated timestamp' do
- expect(release_hash[:updated_at]).to eq(updated_at)
+ expect(release_hash[:updated_at]).to eq(created_at)
+ end
+
+ it 'includes the release name' do
+ expect(release_hash[:name]).to eq(github_release_name)
end
end
end
describe '#each_release' do
- let(:release) { double(:release) }
+ let(:github_release) { double(:github_release) }
before do
allow(project).to receive(:import_source).and_return('foo/bar')
@@ -102,7 +122,7 @@ describe Gitlab::GithubImport::Importer::ReleasesImporter do
allow(client)
.to receive(:releases)
.with('foo/bar')
- .and_return([release].to_enum)
+ .and_return([github_release].to_enum)
end
it 'returns an Enumerator' do
@@ -110,19 +130,19 @@ describe Gitlab::GithubImport::Importer::ReleasesImporter do
end
it 'yields every release to the Enumerator' do
- expect(importer.each_release.next).to eq(release)
+ expect(importer.each_release.next).to eq(github_release)
end
end
describe '#description_for' do
it 'returns the description when present' do
- expect(importer.description_for(release)).to eq(release.body)
+ expect(importer.description_for(github_release)).to eq(github_release.body)
end
it 'returns a generated description when one is not present' do
- allow(release).to receive(:body).and_return('')
+ allow(github_release).to receive(:body).and_return('')
- expect(importer.description_for(release)).to eq('Release for tag 1.0')
+ expect(importer.description_for(github_release)).to eq('Release for tag 1.0')
end
end
end
diff --git a/spec/lib/gitlab/gitlab_import/client_spec.rb b/spec/lib/gitlab/gitlab_import/client_spec.rb
index 22ad88e28cb..0f1745fcc02 100644
--- a/spec/lib/gitlab/gitlab_import/client_spec.rb
+++ b/spec/lib/gitlab/gitlab_import/client_spec.rb
@@ -52,6 +52,7 @@ describe Gitlab::GitlabImport::Client do
describe '#projects' do
subject(:method) { :projects }
+
let(:args) { [] }
let(:element_list) { build_list(:project, 2) }
@@ -67,6 +68,7 @@ describe Gitlab::GitlabImport::Client do
describe '#issues' do
subject(:method) { :issues }
+
let(:args) { [1] }
let(:element_list) { build_list(:issue, 2) }
@@ -82,6 +84,7 @@ describe Gitlab::GitlabImport::Client do
describe '#issue_comments' do
subject(:method) { :issue_comments }
+
let(:args) { [1, 1] }
let(:element_list) { build_list(:note_on_issue, 2) }
diff --git a/spec/lib/gitlab/gl_repository/repo_type_spec.rb b/spec/lib/gitlab/gl_repository/repo_type_spec.rb
index f06a2448ff7..9e09e1411ab 100644
--- a/spec/lib/gitlab/gl_repository/repo_type_spec.rb
+++ b/spec/lib/gitlab/gl_repository/repo_type_spec.rb
@@ -4,36 +4,6 @@ require 'spec_helper'
describe Gitlab::GlRepository::RepoType do
set(:project) { create(:project) }
- shared_examples 'a repo type' do
- describe "#identifier_for_subject" do
- subject { described_class.identifier_for_subject(project) }
-
- it { is_expected.to eq(expected_identifier) }
- end
-
- describe "#fetch_id" do
- it "finds an id match in the identifier" do
- expect(described_class.fetch_id(expected_identifier)).to eq(expected_id)
- end
-
- it 'does not break on other identifiers' do
- expect(described_class.fetch_id("wiki-noid")).to eq(nil)
- end
- end
-
- describe "#path_suffix" do
- subject { described_class.path_suffix }
-
- it { is_expected.to eq(expected_suffix) }
- end
-
- describe "#repository_for" do
- it "finds the repository for the repo type" do
- expect(described_class.repository_for(project)).to eq(expected_repository)
- end
- end
- end
-
describe Gitlab::GlRepository::PROJECT do
it_behaves_like 'a repo type' do
let(:expected_identifier) { "project-#{project.id}" }
diff --git a/spec/lib/gitlab/gpg/commit_spec.rb b/spec/lib/gitlab/gpg/commit_spec.rb
index fa47cfd519b..8401b683fd5 100644
--- a/spec/lib/gitlab/gpg/commit_spec.rb
+++ b/spec/lib/gitlab/gpg/commit_spec.rb
@@ -370,5 +370,33 @@ describe Gitlab::Gpg::Commit do
it_behaves_like 'returns the cached signature on second call'
end
+
+ context 'multiple commits with signatures' do
+ let(:first_signature) { create(:gpg_signature) }
+
+ let(:gpg_key) { create(:gpg_key, key: GpgHelpers::User2.public_key) }
+ let(:second_signature) { create(:gpg_signature, gpg_key: gpg_key) }
+
+ let!(:first_commit) { create(:commit, project: project, sha: first_signature.commit_sha) }
+ let!(:second_commit) { create(:commit, project: project, sha: second_signature.commit_sha) }
+
+ let(:commits) do
+ [first_commit, second_commit].map do |commit|
+ gpg_commit = described_class.new(commit)
+
+ allow(gpg_commit).to receive(:has_signature?).and_return(true)
+
+ gpg_commit
+ end
+ end
+
+ it 'does an aggregated sql request instead of 2 separate ones' do
+ recorder = ActiveRecord::QueryRecorder.new do
+ commits.each(&:signature)
+ end
+
+ expect(recorder.count).to eq(1)
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/graphs/commits_spec.rb b/spec/lib/gitlab/graphs/commits_spec.rb
index 530d4a981bf..09654e0439e 100644
--- a/spec/lib/gitlab/graphs/commits_spec.rb
+++ b/spec/lib/gitlab/graphs/commits_spec.rb
@@ -11,12 +11,14 @@ describe Gitlab::Graphs::Commits do
describe '#commit_per_day' do
context 'when range is only commits from today' do
subject { described_class.new([commit2, commit1]).commit_per_day }
+
it { is_expected.to eq 2 }
end
end
context 'when range is only commits from today' do
subject { described_class.new([commit2, commit1]) }
+
describe '#commit_per_day' do
it { expect(subject.commit_per_day).to eq 2 }
end
@@ -28,6 +30,7 @@ describe Gitlab::Graphs::Commits do
context 'with commits from yesterday and today' do
subject { described_class.new([commit2, commit1_yesterday]) }
+
describe '#commit_per_day' do
it { expect(subject.commit_per_day).to eq 1.0 }
end
diff --git a/spec/lib/gitlab/health_checks/gitaly_check_spec.rb b/spec/lib/gitlab/health_checks/gitaly_check_spec.rb
index 4912cd48761..36e2fd04aeb 100644
--- a/spec/lib/gitlab/health_checks/gitaly_check_spec.rb
+++ b/spec/lib/gitlab/health_checks/gitaly_check_spec.rb
@@ -18,18 +18,19 @@ describe Gitlab::HealthChecks::GitalyCheck do
context 'Gitaly server is up' do
let(:gitaly_check) { double(check: { success: true }) }
- it { is_expected.to eq([result_class.new(true, nil, shard: 'default')]) }
+ it { is_expected.to eq([result_class.new('gitaly_check', true, nil, shard: 'default')]) }
end
context 'Gitaly server is down' do
let(:gitaly_check) { double(check: { success: false, message: 'Connection refused' }) }
- it { is_expected.to eq([result_class.new(false, 'Connection refused', shard: 'default')]) }
+ it { is_expected.to eq([result_class.new('gitaly_check', false, 'Connection refused', shard: 'default')]) }
end
end
describe '#metrics' do
subject { described_class.metrics }
+
let(:server) { double(storage: 'default', read_writeable?: up) }
before do
diff --git a/spec/lib/gitlab/health_checks/probes/collection_spec.rb b/spec/lib/gitlab/health_checks/probes/collection_spec.rb
new file mode 100644
index 00000000000..33efc640257
--- /dev/null
+++ b/spec/lib/gitlab/health_checks/probes/collection_spec.rb
@@ -0,0 +1,62 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::HealthChecks::Probes::Collection do
+ let(:readiness) { described_class.new(*checks) }
+
+ describe '#call' do
+ subject { readiness.execute }
+
+ context 'with all checks' do
+ let(:checks) do
+ [
+ Gitlab::HealthChecks::DbCheck,
+ Gitlab::HealthChecks::Redis::RedisCheck,
+ Gitlab::HealthChecks::Redis::CacheCheck,
+ Gitlab::HealthChecks::Redis::QueuesCheck,
+ Gitlab::HealthChecks::Redis::SharedStateCheck,
+ Gitlab::HealthChecks::GitalyCheck
+ ]
+ end
+
+ it 'responds with readiness checks data' do
+ expect(subject.http_status).to eq(200)
+
+ expect(subject.json[:status]).to eq('ok')
+ expect(subject.json['db_check']).to contain_exactly(status: 'ok')
+ expect(subject.json['cache_check']).to contain_exactly(status: 'ok')
+ expect(subject.json['queues_check']).to contain_exactly(status: 'ok')
+ expect(subject.json['shared_state_check']).to contain_exactly(status: 'ok')
+ expect(subject.json['gitaly_check']).to contain_exactly(
+ status: 'ok', labels: { shard: 'default' })
+ end
+
+ context 'when Redis fails' do
+ before do
+ allow(Gitlab::HealthChecks::Redis::RedisCheck).to receive(:readiness).and_return(
+ Gitlab::HealthChecks::Result.new('redis_check', false, "check error"))
+ end
+
+ it 'responds with failure' do
+ expect(subject.http_status).to eq(503)
+
+ expect(subject.json[:status]).to eq('failed')
+ expect(subject.json['cache_check']).to contain_exactly(status: 'ok')
+ expect(subject.json['redis_check']).to contain_exactly(
+ status: 'failed', message: 'check error')
+ end
+ end
+ end
+
+ context 'without checks' do
+ let(:checks) { [] }
+
+ it 'responds with success' do
+ expect(subject.http_status).to eq(200)
+
+ expect(subject.json).to eq(status: 'ok')
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/health_checks/prometheus_text_format_spec.rb b/spec/lib/gitlab/health_checks/prometheus_text_format_spec.rb
deleted file mode 100644
index ed757ed60d8..00000000000
--- a/spec/lib/gitlab/health_checks/prometheus_text_format_spec.rb
+++ /dev/null
@@ -1,41 +0,0 @@
-describe Gitlab::HealthChecks::PrometheusTextFormat do
- let(:metric_class) { Gitlab::HealthChecks::Metric }
- subject { described_class.new }
-
- describe '#marshal' do
- let(:sample_metrics) do
- [metric_class.new('metric1', 1),
- metric_class.new('metric2', 2)]
- end
-
- it 'marshal to text with non repeating type definition' do
- expected = <<-EXPECTED.strip_heredoc
- # TYPE metric1 gauge
- metric1 1
- # TYPE metric2 gauge
- metric2 2
- EXPECTED
-
- expect(subject.marshal(sample_metrics)).to eq(expected)
- end
-
- context 'metrics where name repeats' do
- let(:sample_metrics) do
- [metric_class.new('metric1', 1),
- metric_class.new('metric1', 2),
- metric_class.new('metric2', 3)]
- end
-
- it 'marshal to text with non repeating type definition' do
- expected = <<-EXPECTED.strip_heredoc
- # TYPE metric1 gauge
- metric1 1
- metric1 2
- # TYPE metric2 gauge
- metric2 3
- EXPECTED
- expect(subject.marshal(sample_metrics)).to eq(expected)
- end
- end
- end
-end
diff --git a/spec/lib/gitlab/health_checks/puma_check_spec.rb b/spec/lib/gitlab/health_checks/puma_check_spec.rb
new file mode 100644
index 00000000000..71b6386b174
--- /dev/null
+++ b/spec/lib/gitlab/health_checks/puma_check_spec.rb
@@ -0,0 +1,65 @@
+require 'spec_helper'
+
+describe Gitlab::HealthChecks::PumaCheck do
+ let(:result_class) { Gitlab::HealthChecks::Result }
+ let(:readiness) { described_class.readiness }
+ let(:metrics) { described_class.metrics }
+
+ shared_examples 'with state' do |(state, message)|
+ it "does provide readiness" do
+ expect(readiness).to eq(result_class.new('puma_check', state, message))
+ end
+
+ it "does provide metrics" do
+ expect(metrics).to include(
+ an_object_having_attributes(name: 'puma_check_success', value: state ? 1 : 0))
+ expect(metrics).to include(
+ an_object_having_attributes(name: 'puma_check_latency_seconds', value: be >= 0))
+ end
+ end
+
+ context 'when Puma is not loaded' do
+ before do
+ hide_const('Puma')
+ end
+
+ it "does not provide readiness and metrics" do
+ expect(readiness).to be_nil
+ expect(metrics).to be_nil
+ end
+ end
+
+ context 'when Puma is loaded' do
+ before do
+ stub_const('Puma', Module.new)
+ end
+
+ context 'when stats are missing' do
+ before do
+ expect(Puma).to receive(:stats).and_raise(NoMethodError)
+ end
+
+ it_behaves_like 'with state', [false, 'unexpected Puma check result: 0']
+ end
+
+ context 'for Single mode' do
+ before do
+ expect(Puma).to receive(:stats) do
+ '{}'
+ end
+ end
+
+ it_behaves_like 'with state', true
+ end
+
+ context 'for Cluster mode' do
+ before do
+ expect(Puma).to receive(:stats) do
+ '{"workers":2}'
+ end
+ end
+
+ it_behaves_like 'with state', true
+ end
+ end
+end
diff --git a/spec/lib/gitlab/health_checks/simple_check_shared.rb b/spec/lib/gitlab/health_checks/simple_check_shared.rb
index e2643458aca..03a7cf249cf 100644
--- a/spec/lib/gitlab/health_checks/simple_check_shared.rb
+++ b/spec/lib/gitlab/health_checks/simple_check_shared.rb
@@ -1,6 +1,7 @@
shared_context 'simple_check' do |metrics_prefix, check_name, success_result|
describe '#metrics' do
subject { described_class.metrics }
+
context 'Check is passing' do
before do
allow(described_class).to receive(:check).and_return success_result
@@ -34,6 +35,7 @@ shared_context 'simple_check' do |metrics_prefix, check_name, success_result|
describe '#readiness' do
subject { described_class.readiness }
+
context 'Check returns ok' do
before do
allow(described_class).to receive(:check).and_return success_result
@@ -57,10 +59,13 @@ shared_context 'simple_check' do |metrics_prefix, check_name, success_result|
it { is_expected.to have_attributes(success: false, message: "#{described_class.human_name} check timed out") }
end
- end
- describe '#liveness' do
- subject { described_class.readiness }
- it { is_expected.to eq(Gitlab::HealthChecks::Result.new(true)) }
+ context 'Check is raising an unhandled exception' do
+ before do
+ allow(described_class).to receive(:check ).and_raise "unexpected error"
+ end
+
+ it { is_expected.to have_attributes(success: false, message: "unexpected #{described_class.human_name} check result: unexpected error") }
+ end
end
end
diff --git a/spec/lib/gitlab/health_checks/unicorn_check_spec.rb b/spec/lib/gitlab/health_checks/unicorn_check_spec.rb
new file mode 100644
index 00000000000..c02d0c37738
--- /dev/null
+++ b/spec/lib/gitlab/health_checks/unicorn_check_spec.rb
@@ -0,0 +1,63 @@
+require 'spec_helper'
+
+describe Gitlab::HealthChecks::UnicornCheck do
+ let(:result_class) { Gitlab::HealthChecks::Result }
+ let(:readiness) { described_class.readiness }
+ let(:metrics) { described_class.metrics }
+
+ before do
+ described_class.clear_memoization(:http_servers)
+ end
+
+ shared_examples 'with state' do |(state, message)|
+ it "does provide readiness" do
+ expect(readiness).to eq(result_class.new('unicorn_check', state, message))
+ end
+
+ it "does provide metrics" do
+ expect(metrics).to include(
+ an_object_having_attributes(name: 'unicorn_check_success', value: state ? 1 : 0))
+ expect(metrics).to include(
+ an_object_having_attributes(name: 'unicorn_check_latency_seconds', value: be >= 0))
+ end
+ end
+
+ context 'when Unicorn is not loaded' do
+ before do
+ hide_const('Unicorn')
+ end
+
+ it "does not provide readiness and metrics" do
+ expect(readiness).to be_nil
+ expect(metrics).to be_nil
+ end
+ end
+
+ context 'when Unicorn is loaded' do
+ let(:http_server_class) { Struct.new(:worker_processes) }
+
+ before do
+ stub_const('Unicorn::HttpServer', http_server_class)
+ end
+
+ context 'when no servers are running' do
+ it_behaves_like 'with state', [false, 'unexpected Unicorn check result: 0']
+ end
+
+ context 'when servers without workers are running' do
+ before do
+ http_server_class.new(0)
+ end
+
+ it_behaves_like 'with state', [false, 'unexpected Unicorn check result: 0']
+ end
+
+ context 'when servers with workers are running' do
+ before do
+ http_server_class.new(1)
+ end
+
+ it_behaves_like 'with state', true
+ end
+ end
+end
diff --git a/spec/lib/gitlab/hook_data/issue_builder_spec.rb b/spec/lib/gitlab/hook_data/issue_builder_spec.rb
index 6013fb78bc7..ebd7feb0055 100644
--- a/spec/lib/gitlab/hook_data/issue_builder_spec.rb
+++ b/spec/lib/gitlab/hook_data/issue_builder_spec.rb
@@ -26,7 +26,7 @@ describe Gitlab::HookData::IssueBuilder do
duplicated_to_id
project_id
relative_position
- state
+ state_id
time_estimate
title
updated_at
@@ -41,6 +41,7 @@ describe Gitlab::HookData::IssueBuilder do
expect(data).to include(:human_time_estimate)
expect(data).to include(:human_total_time_spent)
expect(data).to include(:assignee_ids)
+ expect(data).to include(:state)
expect(data).to include('labels' => [label.hook_attrs])
end
diff --git a/spec/lib/gitlab/import/merge_request_creator_spec.rb b/spec/lib/gitlab/import/merge_request_creator_spec.rb
index 7c73e9b39f7..ff2c3032dbf 100644
--- a/spec/lib/gitlab/import/merge_request_creator_spec.rb
+++ b/spec/lib/gitlab/import/merge_request_creator_spec.rb
@@ -21,8 +21,11 @@ describe Gitlab::Import::MergeRequestCreator do
subject.execute(attributes)
- expect(merge_request.reload.merge_request_diffs.count).to eq(1)
- expect(merge_request.reload.merge_request_diffs.first.commits.count).to eq(commits_count)
+ merge_request.reload
+
+ expect(merge_request.merge_request_diffs.count).to eq(1)
+ expect(merge_request.merge_request_diffs.first.commits.count).to eq(commits_count)
+ expect(merge_request.latest_merge_request_diff_id).to eq(merge_request.merge_request_diffs.first.id)
end
end
diff --git a/spec/lib/gitlab/import_export/after_export_strategies/base_after_export_strategy_spec.rb b/spec/lib/gitlab/import_export/after_export_strategies/base_after_export_strategy_spec.rb
index 9a442de2900..a3d2880182d 100644
--- a/spec/lib/gitlab/import_export/after_export_strategies/base_after_export_strategy_spec.rb
+++ b/spec/lib/gitlab/import_export/after_export_strategies/base_after_export_strategy_spec.rb
@@ -24,14 +24,22 @@ describe Gitlab::ImportExport::AfterExportStrategies::BaseAfterExportStrategy do
service.execute(user, project)
- expect(lock_path_exist?).to be_truthy
+ expect(service.locks_present?).to be_truthy
end
context 'when the method succeeds' do
it 'removes the lock file' do
service.execute(user, project)
- expect(lock_path_exist?).to be_falsey
+ expect(service.locks_present?).to be_falsey
+ end
+
+ it 'removes the archive path' do
+ FileUtils.mkdir_p(shared.archive_path)
+
+ service.execute(user, project)
+
+ expect(File.exist?(shared.archive_path)).to be_falsey
end
end
@@ -62,13 +70,21 @@ describe Gitlab::ImportExport::AfterExportStrategies::BaseAfterExportStrategy do
service.execute(user, project)
end
+
+ it 'removes the archive path' do
+ FileUtils.mkdir_p(shared.archive_path)
+
+ service.execute(user, project)
+
+ expect(File.exist?(shared.archive_path)).to be_falsey
+ end
end
context 'when an exception is raised' do
it 'removes the lock' do
expect { service.execute(user, project) }.to raise_error(NotImplementedError)
- expect(lock_path_exist?).to be_falsey
+ expect(service.locks_present?).to be_falsey
end
end
end
@@ -97,8 +113,4 @@ describe Gitlab::ImportExport::AfterExportStrategies::BaseAfterExportStrategy do
expect(described_class.new(params).to_json).to eq result
end
end
-
- def lock_path_exist?
- File.exist?(described_class.lock_file_path(project))
- end
end
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index d3be1e86539..4fd61383c6b 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -25,6 +25,10 @@ issues:
- epic
- designs
- design_versions
+- description_versions
+- prometheus_alerts
+- prometheus_alert_events
+- self_managed_prometheus_alert_events
events:
- author
- project
@@ -79,6 +83,7 @@ releases:
- links
- milestone_releases
- milestones
+- evidence
links:
- release
project_members:
@@ -128,6 +133,7 @@ merge_requests:
- blocks_as_blockee
- blocking_merge_requests
- blocked_merge_requests
+- description_versions
external_pull_requests:
- project
merge_request_diff:
@@ -172,7 +178,7 @@ ci_pipelines:
- downstream_bridges
- job_artifacts
- vulnerabilities_occurrence_pipelines
-- vulnerabilities
+- vulnerability_findings
pipeline_variables:
- pipeline
stages:
@@ -250,11 +256,11 @@ project:
- cluster
- clusters
- cluster_project
-- cluster_ingresses
- creator
- cycle_analytics_stages
- group
- namespace
+- management_clusters
- boards
- last_event
- services
@@ -349,6 +355,7 @@ project:
- members_and_requesters
- build_trace_section_names
- build_trace_chunks
+- job_artifacts
- root_of_fork_network
- fork_network_member
- fork_network
@@ -389,6 +396,7 @@ project:
- sourced_pipelines
- prometheus_metrics
- vulnerabilities
+- vulnerability_findings
- vulnerability_feedback
- vulnerability_identifiers
- vulnerability_scanners
@@ -396,6 +404,7 @@ project:
- operations_feature_flags_client
- prometheus_alerts
- prometheus_alert_events
+- self_managed_prometheus_alert_events
- software_license_policies
- project_registry
- packages
@@ -409,6 +418,9 @@ project:
- designs
- project_aliases
- external_pull_requests
+- pages_metadatum
+- alerts_service
+- grafana_integration
award_emoji:
- awardable
- user
@@ -466,6 +478,8 @@ prometheus_alerts:
- prometheus_alert_events
prometheus_alert_events:
- project
+self_managed_prometheus_alert_events:
+- project
epic_issues:
- issue
- epic
@@ -499,3 +513,19 @@ lists:
milestone_releases:
- milestone
- release
+evidences:
+- release
+design: &design
+- issue
+- actions
+- versions
+- notes
+designs: *design
+actions:
+- design
+- version
+versions: &version
+- issue
+- designs
+- actions
+design_versions: *version
diff --git a/spec/lib/gitlab/import_export/fast_hash_serializer_spec.rb b/spec/lib/gitlab/import_export/fast_hash_serializer_spec.rb
index d23b27c9d8e..934e676d020 100644
--- a/spec/lib/gitlab/import_export/fast_hash_serializer_spec.rb
+++ b/spec/lib/gitlab/import_export/fast_hash_serializer_spec.rb
@@ -1,7 +1,12 @@
require 'spec_helper'
describe Gitlab::ImportExport::FastHashSerializer do
- subject { described_class.new(project, tree).execute }
+ # FastHashSerializer#execute generates the hash which is not easily accessible
+ # and includes `JSONBatchRelation` items which are serialized at this point.
+ # Wrapping the result into JSON generating/parsing is for making
+ # the testing more convenient. Doing this, we can check that
+ # all items are properly serialized while traversing the simple hash.
+ subject { JSON.parse(JSON.generate(described_class.new(project, tree).execute)) }
let!(:project) { setup_project }
let(:user) { create(:user) }
diff --git a/spec/lib/gitlab/import_export/model_configuration_spec.rb b/spec/lib/gitlab/import_export/model_configuration_spec.rb
index 3442e22c11f..4426e68b474 100644
--- a/spec/lib/gitlab/import_export/model_configuration_spec.rb
+++ b/spec/lib/gitlab/import_export/model_configuration_spec.rb
@@ -12,9 +12,9 @@ describe 'Import/Export model configuration' do
# Remove duplicated or add missing models
# - project is not part of the tree, so it has to be added manually.
- # - milestone, labels have both singular and plural versions in the tree, so remove the duplicates.
+ # - milestone, labels, merge_request have both singular and plural versions in the tree, so remove the duplicates.
# - User, Author... Models we do not care about for checking models
- names.flatten.uniq - %w(milestones labels user author) + ['project']
+ names.flatten.uniq - %w(milestones labels user author merge_request) + ['project']
end
let(:all_models_yml) { 'spec/lib/gitlab/import_export/all_models.yml' }
let(:all_models_hash) { YAML.load_file(all_models_yml) }
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 87be7857e67..676973ff5e7 100644
--- a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
@@ -2,6 +2,8 @@ require 'spec_helper'
include ImportExport::CommonUtil
describe Gitlab::ImportExport::ProjectTreeRestorer do
+ let(:shared) { project.import_export_shared }
+
describe 'restore project tree' do
before(:context) do
# Using an admin for import, so we can check assignment of existing members
@@ -14,7 +16,7 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
RSpec::Mocks.with_temporary_scope do
@project = create(:project, :builds_enabled, :issues_disabled, name: 'project', path: 'project')
@shared = @project.import_export_shared
- allow(@shared).to receive(:export_path).and_return('spec/lib/gitlab/import_export/')
+ allow(@shared).to receive(:export_path).and_return('spec/fixtures/lib/gitlab/import_export/')
allow_any_instance_of(Repository).to receive(:fetch_source_branch!).and_return(true)
allow_any_instance_of(Gitlab::Git::Repository).to receive(:branch_exists?).and_return(false)
@@ -94,6 +96,17 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
expect(Ci::Pipeline.where(ref: nil)).not_to be_empty
end
+ it 'restores pipeline for merge request' do
+ pipeline = Ci::Pipeline.find_by_sha('048721d90c449b244b7b4c53a9186b04330174ec')
+
+ expect(pipeline).to be_valid
+ expect(pipeline.tag).to be_falsey
+ expect(pipeline.source).to eq('merge_request_event')
+ expect(pipeline.merge_request.id).to be > 0
+ expect(pipeline.merge_request.target_branch).to eq('feature')
+ expect(pipeline.merge_request.source_branch).to eq('feature_conflict')
+ end
+
it 'preserves updated_at on issues' do
issue = Issue.where(description: 'Aliquam enim illo et possimus.').first
@@ -274,36 +287,6 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
end
end
- shared_examples 'restores project successfully' do
- it 'correctly restores project' do
- expect(shared.errors).to be_empty
- expect(restored_project_json).to be_truthy
- end
- end
-
- shared_examples 'restores project correctly' do |**results|
- it 'has labels' do
- expect(project.labels.size).to eq(results.fetch(:labels, 0))
- end
-
- it 'has label priorities' do
- expect(project.labels.find_by(title: 'A project label').priorities).not_to be_empty
- end
-
- it 'has milestones' do
- expect(project.milestones.size).to eq(results.fetch(:milestones, 0))
- end
-
- it 'has issues' do
- expect(project.issues.size).to eq(results.fetch(:issues, 0))
- end
-
- it 'does not set params that are excluded from import_export settings' do
- expect(project.import_type).to be_nil
- expect(project.creator_id).not_to eq 123
- end
- end
-
shared_examples 'restores group correctly' do |**results|
it 'has group label' do
expect(project.group.labels.size).to eq(results.fetch(:labels, 0))
@@ -322,18 +305,17 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
context 'Light JSON' do
let(:user) { create(:user) }
- let(:shared) { project.import_export_shared }
let!(:project) { create(:project, :builds_disabled, :issues_disabled, name: 'project', path: 'project') }
let(:project_tree_restorer) { described_class.new(user: user, shared: shared, project: project) }
let(:restored_project_json) { project_tree_restorer.restore }
before do
- allow(shared).to receive(:export_path).and_return('spec/lib/gitlab/import_export/')
+ allow(shared).to receive(:export_path).and_return('spec/fixtures/lib/gitlab/import_export/')
end
context 'with a simple project' do
before do
- project_tree_restorer.instance_variable_set(:@path, "spec/lib/gitlab/import_export/project.light.json")
+ project_tree_restorer.instance_variable_set(:@path, "spec/fixtures/lib/gitlab/import_export/project.light.json")
restored_project_json
end
@@ -341,6 +323,7 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
it_behaves_like 'restores project correctly',
issues: 1,
labels: 2,
+ label_with_priorities: 'A project label',
milestones: 1,
first_issue_labels: 1,
services: 1
@@ -363,7 +346,12 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
create(:ci_build, token: 'abcd')
end
- it_behaves_like 'restores project successfully'
+ it_behaves_like 'restores project correctly',
+ issues: 1,
+ labels: 2,
+ label_with_priorities: 'A project label',
+ milestones: 1,
+ first_issue_labels: 1
end
end
@@ -430,15 +418,15 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
end
before do
- project_tree_restorer.instance_variable_set(:@path, "spec/lib/gitlab/import_export/project.group.json")
+ project_tree_restorer.instance_variable_set(:@path, "spec/fixtures/lib/gitlab/import_export/project.group.json")
restored_project_json
end
- it_behaves_like 'restores project successfully'
it_behaves_like 'restores project correctly',
issues: 2,
labels: 2,
+ label_with_priorities: 'A project label',
milestones: 2,
first_issue_labels: 1
@@ -446,6 +434,11 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
labels: 0,
milestones: 0,
first_issue_labels: 1
+
+ it 'restores issue states' do
+ expect(project.issues.with_state(:closed).count).to eq(1)
+ expect(project.issues.with_state(:opened).count).to eq(1)
+ end
end
context 'with existing group models' do
@@ -459,7 +452,7 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
end
before do
- project_tree_restorer.instance_variable_set(:@path, "spec/lib/gitlab/import_export/project.light.json")
+ project_tree_restorer.instance_variable_set(:@path, "spec/fixtures/lib/gitlab/import_export/project.light.json")
end
it 'does not import any templated services' do
@@ -501,7 +494,7 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
end
it 'preserves the project milestone IID' do
- project_tree_restorer.instance_variable_set(:@path, "spec/lib/gitlab/import_export/project.milestone-iid.json")
+ project_tree_restorer.instance_variable_set(:@path, "spec/fixtures/lib/gitlab/import_export/project.milestone-iid.json")
expect_any_instance_of(Gitlab::ImportExport::Shared).not_to receive(:error)
@@ -532,21 +525,21 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
end
end
- describe '#restored_project' do
+ context 'Minimal JSON' do
let(:project) { create(:project) }
- let(:shared) { project.import_export_shared }
let(:tree_hash) { { 'visibility_level' => visibility } }
let(:restorer) { described_class.new(user: nil, shared: shared, project: project) }
before do
- restorer.instance_variable_set(:@tree_hash, tree_hash)
+ expect(restorer).to receive(:read_tree_hash) { tree_hash }
end
context 'no group visibility' do
let(:visibility) { Gitlab::VisibilityLevel::PRIVATE }
it 'uses the project visibility' do
- expect(restorer.restored_project.visibility_level).to eq(visibility)
+ expect(restorer.restore).to eq(true)
+ expect(restorer.project.visibility_level).to eq(visibility)
end
end
@@ -557,7 +550,8 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
it 'uses private visibility' do
stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::INTERNAL])
- expect(restorer.restored_project.visibility_level).to eq(Gitlab::VisibilityLevel::PRIVATE)
+ expect(restorer.restore).to eq(true)
+ expect(restorer.project.visibility_level).to eq(Gitlab::VisibilityLevel::PRIVATE)
end
end
end
@@ -574,7 +568,8 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
let(:visibility) { Gitlab::VisibilityLevel::PUBLIC }
it 'uses the group visibility' do
- expect(restorer.restored_project.visibility_level).to eq(group_visibility)
+ expect(restorer.restore).to eq(true)
+ expect(restorer.project.visibility_level).to eq(group_visibility)
end
end
@@ -583,7 +578,8 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
let(:visibility) { Gitlab::VisibilityLevel::PRIVATE }
it 'uses the project visibility' do
- expect(restorer.restored_project.visibility_level).to eq(visibility)
+ expect(restorer.restore).to eq(true)
+ expect(restorer.project.visibility_level).to eq(visibility)
end
end
@@ -592,14 +588,16 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
let(:visibility) { Gitlab::VisibilityLevel::PUBLIC }
it 'uses the group visibility' do
- expect(restorer.restored_project.visibility_level).to eq(group_visibility)
+ expect(restorer.restore).to eq(true)
+ expect(restorer.project.visibility_level).to eq(group_visibility)
end
context 'with restricted internal visibility' do
it 'sets private visibility' do
stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::INTERNAL])
- expect(restorer.restored_project.visibility_level).to eq(Gitlab::VisibilityLevel::PRIVATE)
+ expect(restorer.restore).to eq(true)
+ expect(restorer.project.visibility_level).to eq(Gitlab::VisibilityLevel::PRIVATE)
end
end
end
diff --git a/spec/lib/gitlab/import_export/relation_factory_spec.rb b/spec/lib/gitlab/import_export/relation_factory_spec.rb
index a31f77484d8..a23e68a8f00 100644
--- a/spec/lib/gitlab/import_export/relation_factory_spec.rb
+++ b/spec/lib/gitlab/import_export/relation_factory_spec.rb
@@ -3,12 +3,14 @@ require 'spec_helper'
describe Gitlab::ImportExport::RelationFactory do
let(:project) { create(:project) }
let(:members_mapper) { double('members_mapper').as_null_object }
+ let(:merge_requests_mapping) { {} }
let(:user) { create(:admin) }
let(:excluded_keys) { [] }
let(:created_object) do
described_class.create(relation_sym: relation_sym,
relation_hash: relation_hash,
members_mapper: members_mapper,
+ merge_requests_mapping: merge_requests_mapping,
user: user,
project: project,
excluded_keys: excluded_keys)
@@ -83,7 +85,7 @@ describe Gitlab::ImportExport::RelationFactory do
class FooModel
include ActiveModel::Model
- def initialize(params)
+ def initialize(params = {})
params.each { |key, value| send("#{key}=", value) }
end
diff --git a/spec/lib/gitlab/import_export/relation_rename_service_spec.rb b/spec/lib/gitlab/import_export/relation_rename_service_spec.rb
index 17bb5bcc155..472bf55d37e 100644
--- a/spec/lib/gitlab/import_export/relation_rename_service_spec.rb
+++ b/spec/lib/gitlab/import_export/relation_rename_service_spec.rb
@@ -21,7 +21,7 @@ describe Gitlab::ImportExport::RelationRenameService do
context 'when importing' do
let(:project_tree_restorer) { Gitlab::ImportExport::ProjectTreeRestorer.new(user: user, shared: shared, project: project) }
- let(:import_path) { 'spec/lib/gitlab/import_export' }
+ let(:import_path) { 'spec/fixtures/lib/gitlab/import_export' }
let(:file_content) { IO.read("#{import_path}/project.json") }
let!(:json_file) { ActiveSupport::JSON.decode(file_content) }
diff --git a/spec/lib/gitlab/import_export/repo_saver_spec.rb b/spec/lib/gitlab/import_export/repo_saver_spec.rb
index 5a646b4aac8..c3df371af43 100644
--- a/spec/lib/gitlab/import_export/repo_saver_spec.rb
+++ b/spec/lib/gitlab/import_export/repo_saver_spec.rb
@@ -2,8 +2,8 @@ require 'spec_helper'
describe Gitlab::ImportExport::RepoSaver do
describe 'bundle a project Git repo' do
- let(:user) { create(:user) }
- let!(:project) { create(:project, :public, name: 'searchable_project') }
+ set(:user) { create(:user) }
+ let!(:project) { create(:project, :repository) }
let(:export_path) { "#{Dir.tmpdir}/project_tree_saver_spec" }
let(:shared) { project.import_export_shared }
let(:bundler) { described_class.new(project: project, shared: shared) }
@@ -20,5 +20,13 @@ describe Gitlab::ImportExport::RepoSaver do
it 'bundles the repo successfully' do
expect(bundler.save).to be true
end
+
+ context 'when the repo is empty' do
+ let!(:project) { create(:project) }
+
+ it 'bundles the repo successfully' do
+ expect(bundler.save).to be true
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml
index 2f178648838..8ae571a69ef 100644
--- a/spec/lib/gitlab/import_export/safe_model_attributes.yml
+++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml
@@ -47,6 +47,7 @@ PushEventPayload:
- commit_to
- ref
- commit_title
+- ref_count
Note:
- id
- note
@@ -126,6 +127,12 @@ Release:
- created_at
- updated_at
- released_at
+Evidence:
+- id
+- release_id
+- summary
+- created_at
+- updated_at
Releases::Link:
- id
- release_id
@@ -717,6 +724,7 @@ List:
- updated_at
- milestone_id
- user_id
+- max_issue_count
ExternalPullRequest:
- id
- created_at
@@ -730,3 +738,18 @@ ExternalPullRequest:
- target_repository
- source_sha
- target_sha
+DesignManagement::Design:
+- id
+- project_id
+- issue_id
+- filename
+DesignManagement::Action:
+- design_id
+- event
+- version_id
+DesignManagement::Version:
+- id
+- created_at
+- sha
+- issue_id
+- user_id
diff --git a/spec/lib/gitlab/import_export/shared_spec.rb b/spec/lib/gitlab/import_export/shared_spec.rb
index 2c288cff6ef..62669836973 100644
--- a/spec/lib/gitlab/import_export/shared_spec.rb
+++ b/spec/lib/gitlab/import_export/shared_spec.rb
@@ -5,6 +5,35 @@ describe Gitlab::ImportExport::Shared do
let(:project) { build(:project) }
subject { project.import_export_shared }
+ context 'with a repository on disk' do
+ let(:project) { create(:project, :repository) }
+ let(:base_path) { %(/tmp/project_exports/#{project.disk_path}/) }
+
+ describe '#archive_path' do
+ it 'uses a random hash to avoid conflicts' do
+ expect(subject.archive_path).to match(/#{base_path}\h{32}/)
+ end
+
+ it 'memoizes the path' do
+ path = subject.archive_path
+
+ 2.times { expect(subject.archive_path).to eq(path) }
+ end
+ end
+
+ describe '#export_path' do
+ it 'uses a random hash relative to project path' do
+ expect(subject.export_path).to match(/#{base_path}\h{32}\/\h{32}/)
+ end
+
+ it 'memoizes the path' do
+ path = subject.export_path
+
+ 2.times { expect(subject.export_path).to eq(path) }
+ end
+ end
+ end
+
describe '#error' do
let(:error) { StandardError.new('Error importing into /my/folder Permission denied @ unlink_internal - /var/opt/gitlab/gitlab-rails/shared/a/b/c/uploads/file') }
@@ -24,16 +53,17 @@ describe Gitlab::ImportExport::Shared do
subject.error(error)
end
- it 'calls the error logger with the full message' do
- expect(subject).to receive(:log_error).with(hash_including(message: error.message))
+ it 'calls the error logger without a backtrace' do
+ expect(subject).to receive(:log_error).with(message: error.message)
subject.error(error)
end
- it 'calls the debug logger with a backtrace' do
- error.set_backtrace('backtrace')
+ it 'calls the error logger with the full message' do
+ backtrace = caller
+ allow(error).to receive(:backtrace).and_return(caller)
- expect(subject).to receive(:log_debug).with(hash_including(backtrace: 'backtrace'))
+ expect(subject).to receive(:log_error).with(message: error.message, error_backtrace: Gitlab::Profiler.clean_backtrace(backtrace))
subject.error(error)
end
diff --git a/spec/lib/gitlab/import_export/uploads_manager_spec.rb b/spec/lib/gitlab/import_export/uploads_manager_spec.rb
index 792117e1df1..f13f639d6b7 100644
--- a/spec/lib/gitlab/import_export/uploads_manager_spec.rb
+++ b/spec/lib/gitlab/import_export/uploads_manager_spec.rb
@@ -83,7 +83,7 @@ describe Gitlab::ImportExport::UploadsManager do
it 'restores the file' do
manager.restore
- expect(project.uploads.map { |u| u.build_uploader.filename }).to include('dummy.txt')
+ expect(project.uploads.map { |u| u.retrieve_uploader.filename }).to include('dummy.txt')
end
end
end
diff --git a/spec/lib/gitlab/import_export/uploads_restorer_spec.rb b/spec/lib/gitlab/import_export/uploads_restorer_spec.rb
index 6072f18b8c7..e2e8204b2fa 100644
--- a/spec/lib/gitlab/import_export/uploads_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/uploads_restorer_spec.rb
@@ -27,7 +27,7 @@ describe Gitlab::ImportExport::UploadsRestorer do
it 'copies the uploads to the project path' do
subject.restore
- expect(project.uploads.map { |u| u.build_uploader.filename }).to include('dummy.txt')
+ expect(project.uploads.map { |u| u.retrieve_uploader.filename }).to include('dummy.txt')
end
end
@@ -43,7 +43,7 @@ describe Gitlab::ImportExport::UploadsRestorer do
it 'copies the uploads to the project path' do
subject.restore
- expect(project.uploads.map { |u| u.build_uploader.filename }).to include('dummy.txt')
+ expect(project.uploads.map { |u| u.retrieve_uploader.filename }).to include('dummy.txt')
end
end
end
diff --git a/spec/lib/gitlab/import_export/wiki_repo_saver_spec.rb b/spec/lib/gitlab/import_export/wiki_repo_saver_spec.rb
index 441aa1defe6..249afbd23d1 100644
--- a/spec/lib/gitlab/import_export/wiki_repo_saver_spec.rb
+++ b/spec/lib/gitlab/import_export/wiki_repo_saver_spec.rb
@@ -2,8 +2,8 @@ require 'spec_helper'
describe Gitlab::ImportExport::WikiRepoSaver do
describe 'bundle a wiki Git repo' do
- let(:user) { create(:user) }
- let!(:project) { create(:project, :public, :wiki_repo, name: 'searchable_project') }
+ set(:user) { create(:user) }
+ let!(:project) { create(:project, :wiki_repo) }
let(:export_path) { "#{Dir.tmpdir}/project_tree_saver_spec" }
let(:shared) { project.import_export_shared }
let(:wiki_bundler) { described_class.new(project: project, shared: shared) }
@@ -23,5 +23,13 @@ describe Gitlab::ImportExport::WikiRepoSaver do
it 'bundles the repo successfully' do
expect(wiki_bundler.save).to be true
end
+
+ context 'when the repo is empty' do
+ let!(:project) { create(:project) }
+
+ it 'bundles the repo successfully' do
+ expect(wiki_bundler.save).to be true
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/kubernetes/helm/delete_command_spec.rb b/spec/lib/gitlab/kubernetes/helm/delete_command_spec.rb
index 39a46f9bc6d..7e9853cf9ea 100644
--- a/spec/lib/gitlab/kubernetes/helm/delete_command_spec.rb
+++ b/spec/lib/gitlab/kubernetes/helm/delete_command_spec.rb
@@ -14,7 +14,7 @@ describe Gitlab::Kubernetes::Helm::DeleteCommand do
let(:commands) do
<<~EOS
helm init --upgrade
- for i in $(seq 1 30); do helm version && break; sleep 1s; echo "Retrying ($i)..."; done
+ for i in $(seq 1 30); do helm version && s=0 && break || s=$?; sleep 1s; echo \"Retrying ($i)...\"; done; (exit $s)
helm delete --purge app-name
EOS
end
@@ -36,7 +36,7 @@ describe Gitlab::Kubernetes::Helm::DeleteCommand do
let(:commands) do
<<~EOS
helm init --upgrade
- for i in $(seq 1 30); do helm version #{tls_flags} && break; sleep 1s; echo "Retrying ($i)..."; done
+ for i in $(seq 1 30); do helm version #{tls_flags} && s=0 && break || s=$?; sleep 1s; echo \"Retrying ($i)...\"; done; (exit $s)
#{helm_delete_command}
EOS
end
diff --git a/spec/lib/gitlab/kubernetes/helm/install_command_spec.rb b/spec/lib/gitlab/kubernetes/helm/install_command_spec.rb
index f7f510f01db..9eb3322f1a6 100644
--- a/spec/lib/gitlab/kubernetes/helm/install_command_spec.rb
+++ b/spec/lib/gitlab/kubernetes/helm/install_command_spec.rb
@@ -36,7 +36,7 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do
let(:commands) do
<<~EOS
helm init --upgrade
- for i in $(seq 1 30); do helm version #{tls_flags} && break; sleep 1s; echo "Retrying ($i)..."; done
+ for i in $(seq 1 30); do helm version #{tls_flags} && s=0 && break || s=$?; sleep 1s; echo \"Retrying ($i)...\"; done; (exit $s)
helm repo add app-name https://repository.example.com
helm repo update
#{helm_install_comand}
@@ -64,7 +64,7 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do
let(:commands) do
<<~EOS
helm init --upgrade
- for i in $(seq 1 30); do helm version #{tls_flags} && break; sleep 1s; echo "Retrying ($i)..."; done
+ for i in $(seq 1 30); do helm version #{tls_flags} && s=0 && break || s=$?; sleep 1s; echo \"Retrying ($i)...\"; done; (exit $s)
helm repo add app-name https://repository.example.com
helm repo update
#{helm_install_command}
@@ -93,7 +93,7 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do
let(:commands) do
<<~EOS
helm init --upgrade
- for i in $(seq 1 30); do helm version #{tls_flags} && break; sleep 1s; echo "Retrying ($i)..."; done
+ for i in $(seq 1 30); do helm version #{tls_flags} && s=0 && break || s=$?; sleep 1s; echo \"Retrying ($i)...\"; done; (exit $s)
#{helm_install_command}
EOS
end
@@ -120,7 +120,7 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do
let(:commands) do
<<~EOS
helm init --upgrade
- for i in $(seq 1 30); do helm version #{tls_flags} && break; sleep 1s; echo "Retrying ($i)..."; done
+ for i in $(seq 1 30); do helm version #{tls_flags} && s=0 && break || s=$?; sleep 1s; echo \"Retrying ($i)...\"; done; (exit $s)
helm repo add app-name https://repository.example.com
helm repo update
/bin/date
@@ -151,7 +151,7 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do
let(:commands) do
<<~EOS
helm init --upgrade
- for i in $(seq 1 30); do helm version #{tls_flags} && break; sleep 1s; echo "Retrying ($i)..."; done
+ for i in $(seq 1 30); do helm version #{tls_flags} && s=0 && break || s=$?; sleep 1s; echo \"Retrying ($i)...\"; done; (exit $s)
helm repo add app-name https://repository.example.com
helm repo update
#{helm_install_command}
@@ -182,7 +182,7 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do
let(:commands) do
<<~EOS
helm init --upgrade
- for i in $(seq 1 30); do helm version && break; sleep 1s; echo "Retrying ($i)..."; done
+ for i in $(seq 1 30); do helm version && s=0 && break || s=$?; sleep 1s; echo \"Retrying ($i)...\"; done; (exit $s)
helm repo add app-name https://repository.example.com
helm repo update
#{helm_install_command}
@@ -210,7 +210,7 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do
let(:commands) do
<<~EOS
helm init --upgrade
- for i in $(seq 1 30); do helm version #{tls_flags} && break; sleep 1s; echo "Retrying ($i)..."; done
+ for i in $(seq 1 30); do helm version #{tls_flags} && s=0 && break || s=$?; sleep 1s; echo \"Retrying ($i)...\"; done; (exit $s)
helm repo add app-name https://repository.example.com
helm repo update
#{helm_install_command}
diff --git a/spec/lib/gitlab/kubernetes/helm/reset_command_spec.rb b/spec/lib/gitlab/kubernetes/helm/reset_command_spec.rb
index d49d4779735..2a89b04723d 100644
--- a/spec/lib/gitlab/kubernetes/helm/reset_command_spec.rb
+++ b/spec/lib/gitlab/kubernetes/helm/reset_command_spec.rb
@@ -15,6 +15,7 @@ describe Gitlab::Kubernetes::Helm::ResetCommand do
<<~EOS
helm reset
kubectl delete replicaset -n gitlab-managed-apps -l name\\=tiller
+ kubectl delete clusterrolebinding tiller-admin
EOS
end
end
@@ -32,6 +33,7 @@ describe Gitlab::Kubernetes::Helm::ResetCommand do
--tls-key /data/helm/helm/config/key.pem
EOS1
kubectl delete replicaset -n gitlab-managed-apps -l name\\=tiller
+ kubectl delete clusterrolebinding tiller-admin
EOS2
end
end
diff --git a/spec/lib/gitlab/kubernetes/kube_client_spec.rb b/spec/lib/gitlab/kubernetes/kube_client_spec.rb
index e5d688aa391..59e81d89a50 100644
--- a/spec/lib/gitlab/kubernetes/kube_client_spec.rb
+++ b/spec/lib/gitlab/kubernetes/kube_client_spec.rb
@@ -162,7 +162,9 @@ describe Gitlab::Kubernetes::KubeClient do
:get_secret,
:get_service,
:get_service_account,
+ :delete_namespace,
:delete_pod,
+ :delete_service_account,
:create_config_map,
:create_namespace,
:create_pod,
diff --git a/spec/lib/gitlab/legacy_github_import/release_formatter_spec.rb b/spec/lib/gitlab/legacy_github_import/release_formatter_spec.rb
index 2cf4b367c0b..554be57fbec 100644
--- a/spec/lib/gitlab/legacy_github_import/release_formatter_spec.rb
+++ b/spec/lib/gitlab/legacy_github_import/release_formatter_spec.rb
@@ -37,6 +37,14 @@ describe Gitlab::LegacyGithubImport::ReleaseFormatter do
expect(release.attributes).to eq(expected)
end
+
+ context 'with a nil published_at date' do
+ let(:published_at) { nil }
+
+ it 'inserts a timestamp for released_at' do
+ expect(release.attributes[:released_at]).to be_a(Time)
+ end
+ end
end
describe '#valid' do
diff --git a/spec/lib/gitlab/lets_encrypt_spec.rb b/spec/lib/gitlab/lets_encrypt_spec.rb
index 65aea0937f1..2229393fb32 100644
--- a/spec/lib/gitlab/lets_encrypt_spec.rb
+++ b/spec/lib/gitlab/lets_encrypt_spec.rb
@@ -24,4 +24,16 @@ describe ::Gitlab::LetsEncrypt do
it { is_expected.to eq(false) }
end
end
+
+ describe '.terms_of_service_url' do
+ before do
+ stub_lets_encrypt_client
+ end
+
+ subject { described_class.terms_of_service_url }
+
+ it 'returns the url' do
+ is_expected.to eq("https://letsencrypt.org/documents/LE-SA-v1.2-November-15-2017.pdf")
+ end
+ end
end
diff --git a/spec/lib/gitlab/lfs_token_spec.rb b/spec/lib/gitlab/lfs_token_spec.rb
index 701ed1f3a1b..b2fd7bdd307 100644
--- a/spec/lib/gitlab/lfs_token_spec.rb
+++ b/spec/lib/gitlab/lfs_token_spec.rb
@@ -115,6 +115,46 @@ describe Gitlab::LfsToken, :clean_gitlab_redis_shared_state do
expect(lfs_token.token_valid?(lfs_token.token)).to be_truthy
end
end
+
+ context 'when the actor is a regular user' do
+ context 'when the user is blocked' do
+ let(:actor) { create(:user, :blocked) }
+
+ it 'returns false' do
+ expect(lfs_token.token_valid?(lfs_token.token)).to be_falsey
+ end
+ end
+
+ context 'when the user password is expired' do
+ let(:actor) { create(:user, password_expires_at: 1.minute.ago) }
+
+ it 'returns false' do
+ expect(lfs_token.token_valid?(lfs_token.token)).to be_falsey
+ end
+ end
+ end
+
+ context 'when the actor is an ldap user' do
+ before do
+ allow(actor).to receive(:ldap_user?).and_return(true)
+ end
+
+ context 'when the user is blocked' do
+ let(:actor) { create(:user, :blocked) }
+
+ it 'returns false' do
+ expect(lfs_token.token_valid?(lfs_token.token)).to be_falsey
+ end
+ end
+
+ context 'when the user password is expired' do
+ let(:actor) { create(:user, password_expires_at: 1.minute.ago) }
+
+ it 'returns true' do
+ expect(lfs_token.token_valid?(lfs_token.token)).to be_truthy
+ end
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/metrics/exporter/base_exporter_spec.rb b/spec/lib/gitlab/metrics/exporter/base_exporter_spec.rb
new file mode 100644
index 00000000000..47ec69e2f45
--- /dev/null
+++ b/spec/lib/gitlab/metrics/exporter/base_exporter_spec.rb
@@ -0,0 +1,180 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Metrics::Exporter::BaseExporter do
+ let(:exporter) { described_class.new }
+ let(:log_filename) { File.join(Rails.root, 'log', 'sidekiq_exporter.log') }
+ let(:settings) { double('settings') }
+
+ before do
+ allow_any_instance_of(described_class).to receive(:log_filename).and_return(log_filename)
+ allow_any_instance_of(described_class).to receive(:settings).and_return(settings)
+ end
+
+ describe 'when exporter is enabled' do
+ before do
+ allow(::WEBrick::HTTPServer).to receive(:new).with(
+ Port: anything,
+ BindAddress: anything,
+ Logger: anything,
+ AccessLog: anything
+ ).and_call_original
+
+ allow(settings).to receive(:enabled).and_return(true)
+ allow(settings).to receive(:port).and_return(0)
+ allow(settings).to receive(:address).and_return('127.0.0.1')
+ end
+
+ after do
+ exporter.stop
+ end
+
+ describe 'when exporter is stopped' do
+ describe '#start' do
+ it 'starts the exporter' do
+ expect_any_instance_of(::WEBrick::HTTPServer).to receive(:start)
+
+ expect { exporter.start.join }.to change { exporter.thread? }.from(false).to(true)
+ end
+
+ describe 'with custom settings' do
+ let(:port) { 99999 }
+ let(:address) { 'sidekiq_exporter_address' }
+
+ before do
+ allow(settings).to receive(:port).and_return(port)
+ allow(settings).to receive(:address).and_return(address)
+ end
+
+ it 'starts server with port and address from settings' do
+ expect(::WEBrick::HTTPServer).to receive(:new).with(
+ Port: port,
+ BindAddress: address,
+ Logger: anything,
+ AccessLog: anything
+ ).and_wrap_original do |m, *args|
+ m.call(DoNotListen: true, Logger: args.first[:Logger])
+ end
+
+ allow_any_instance_of(::WEBrick::HTTPServer).to receive(:start)
+
+ exporter.start.join
+ end
+ end
+
+ describe 'when thread is not alive' do
+ it 'does close listeners' do
+ expect_any_instance_of(::WEBrick::HTTPServer).to receive(:start)
+ expect_any_instance_of(::WEBrick::HTTPServer).to receive(:listeners)
+ .and_call_original
+
+ expect { exporter.start.join }.to change { exporter.thread? }.from(false).to(true)
+
+ exporter.stop
+ end
+ end
+ end
+
+ describe '#stop' do
+ it "doesn't shutdown stopped server" do
+ expect_any_instance_of(::WEBrick::HTTPServer).not_to receive(:shutdown)
+
+ expect { exporter.stop }.not_to change { exporter.thread? }
+ end
+ end
+ end
+
+ describe 'when exporter is running' do
+ before do
+ exporter.start
+ end
+
+ describe '#start' do
+ it "doesn't start running server" do
+ expect_any_instance_of(::WEBrick::HTTPServer).not_to receive(:start)
+
+ expect { exporter.start }.not_to change { exporter.thread? }
+ end
+ end
+
+ describe '#stop' do
+ it 'shutdowns server' do
+ expect_any_instance_of(::WEBrick::HTTPServer).to receive(:shutdown)
+
+ expect { exporter.stop }.to change { exporter.thread? }.from(true).to(false)
+ end
+ end
+ end
+ end
+
+ describe 'request handling' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:method_class, :path, :http_status) do
+ Net::HTTP::Get | '/metrics' | 200
+ Net::HTTP::Get | '/liveness' | 200
+ Net::HTTP::Get | '/readiness' | 200
+ Net::HTTP::Get | '/' | 404
+ end
+
+ before do
+ allow(settings).to receive(:enabled).and_return(true)
+ allow(settings).to receive(:port).and_return(0)
+ allow(settings).to receive(:address).and_return('127.0.0.1')
+
+ # We want to wrap original method
+ # and run handling of requests
+ # in separate thread
+ allow_any_instance_of(::WEBrick::HTTPServer)
+ .to receive(:start).and_wrap_original do |m, *args|
+ Thread.new do
+ m.call(*args)
+ rescue IOError
+ # is raised as we close listeners
+ end
+ end
+
+ exporter.start.join
+ end
+
+ after do
+ exporter.stop
+ end
+
+ with_them do
+ let(:config) { exporter.server.config }
+ let(:request) { method_class.new(path) }
+
+ it 'responds with proper http_status' do
+ http = Net::HTTP.new(config[:BindAddress], config[:Port])
+ response = http.request(request)
+
+ expect(response.code).to eq(http_status.to_s)
+ end
+ end
+ end
+
+ describe 'when exporter is disabled' do
+ before do
+ allow(settings).to receive(:enabled).and_return(false)
+ end
+
+ describe '#start' do
+ it "doesn't start" do
+ expect_any_instance_of(::WEBrick::HTTPServer).not_to receive(:start)
+
+ expect(exporter.start).to be_nil
+ expect { exporter.start }.not_to change { exporter.thread? }
+ end
+ end
+
+ describe '#stop' do
+ it "doesn't shutdown" do
+ expect_any_instance_of(::WEBrick::HTTPServer).not_to receive(:shutdown)
+
+ expect { exporter.stop }.not_to change { exporter.thread? }
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/metrics/exporter/sidekiq_exporter_spec.rb b/spec/lib/gitlab/metrics/exporter/sidekiq_exporter_spec.rb
new file mode 100644
index 00000000000..a415b6407d5
--- /dev/null
+++ b/spec/lib/gitlab/metrics/exporter/sidekiq_exporter_spec.rb
@@ -0,0 +1,65 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Metrics::Exporter::SidekiqExporter do
+ let(:exporter) { described_class.new }
+
+ after do
+ exporter.stop
+ end
+
+ context 'with valid config' do
+ before do
+ stub_config(
+ monitoring: {
+ sidekiq_exporter: {
+ enabled: true,
+ port: 0,
+ address: '127.0.0.1'
+ }
+ }
+ )
+ end
+
+ it 'does start thread' do
+ expect(exporter.start).not_to be_nil
+ end
+ end
+
+ context 'when port is already taken' do
+ let(:first_exporter) { described_class.new }
+
+ before do
+ stub_config(
+ monitoring: {
+ sidekiq_exporter: {
+ enabled: true,
+ port: 9992,
+ address: '127.0.0.1'
+ }
+ }
+ )
+
+ first_exporter.start
+ end
+
+ after do
+ first_exporter.stop
+ end
+
+ it 'does print error message' do
+ expect(Sidekiq.logger).to receive(:error)
+ .with(
+ class: described_class.to_s,
+ message: 'Cannot start sidekiq_exporter',
+ exception: anything)
+
+ exporter.start
+ end
+
+ it 'does not start thread' do
+ expect(exporter.start).to be_nil
+ end
+ end
+end
diff --git a/spec/lib/gitlab/metrics/exporter/web_exporter_spec.rb b/spec/lib/gitlab/metrics/exporter/web_exporter_spec.rb
new file mode 100644
index 00000000000..99349934e63
--- /dev/null
+++ b/spec/lib/gitlab/metrics/exporter/web_exporter_spec.rb
@@ -0,0 +1,64 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Metrics::Exporter::WebExporter do
+ let(:exporter) { described_class.new }
+
+ context 'when blackout seconds is used' do
+ let(:blackout_seconds) { 0 }
+ let(:readiness_probe) { exporter.send(:readiness_probe).execute }
+
+ before do
+ stub_config(
+ monitoring: {
+ web_exporter: {
+ enabled: true,
+ port: 0,
+ address: '127.0.0.1',
+ blackout_seconds: blackout_seconds
+ }
+ }
+ )
+
+ exporter.start
+ end
+
+ after do
+ exporter.stop
+ end
+
+ context 'when running server' do
+ it 'readiness probe returns succesful status' do
+ expect(readiness_probe.http_status).to eq(200)
+ expect(readiness_probe.json).to include(status: 'ok')
+ expect(readiness_probe.json).to include('web_exporter' => [{ 'status': 'ok' }])
+ end
+ end
+
+ context 'when blackout seconds is 10s' do
+ let(:blackout_seconds) { 10 }
+
+ it 'readiness probe returns a failure status' do
+ # during sleep we check the status of readiness probe
+ expect(exporter).to receive(:sleep).with(10) do
+ expect(readiness_probe.http_status).to eq(503)
+ expect(readiness_probe.json).to include(status: 'failed')
+ expect(readiness_probe.json).to include('web_exporter' => [{ 'status': 'failed' }])
+ end
+
+ exporter.stop
+ end
+ end
+
+ context 'when blackout is disabled' do
+ let(:blackout_seconds) { 0 }
+
+ it 'readiness probe returns a failure status' do
+ expect(exporter).not_to receive(:sleep)
+
+ exporter.stop
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/metrics/requests_rack_middleware_spec.rb b/spec/lib/gitlab/metrics/requests_rack_middleware_spec.rb
index c29db3a93ec..66ea390a2bf 100644
--- a/spec/lib/gitlab/metrics/requests_rack_middleware_spec.rb
+++ b/spec/lib/gitlab/metrics/requests_rack_middleware_spec.rb
@@ -63,5 +63,19 @@ describe Gitlab::Metrics::RequestsRackMiddleware do
expect { subject.call(env) }.to raise_error(StandardError)
end
end
+
+ describe '.initialize_http_request_duration_seconds' do
+ it "sets labels" do
+ expected_labels = []
+ described_class::HTTP_METHODS.each do |method, statuses|
+ statuses.each do |status|
+ expected_labels << { method: method, status: status }
+ end
+ end
+
+ described_class.initialize_http_request_duration_seconds
+ expect(described_class.http_request_duration_seconds.values.keys).to include(*expected_labels)
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/metrics/samplers/puma_sampler_spec.rb b/spec/lib/gitlab/metrics/samplers/puma_sampler_spec.rb
index b8add3c1324..1097d26c320 100644
--- a/spec/lib/gitlab/metrics/samplers/puma_sampler_spec.rb
+++ b/spec/lib/gitlab/metrics/samplers/puma_sampler_spec.rb
@@ -4,6 +4,7 @@ require 'spec_helper'
describe Gitlab::Metrics::Samplers::PumaSampler do
subject { described_class.new(5) }
+
let(:null_metric) { double('null_metric', set: nil, observe: nil) }
before do
diff --git a/spec/lib/gitlab/metrics/sidekiq_metrics_exporter_spec.rb b/spec/lib/gitlab/metrics/sidekiq_metrics_exporter_spec.rb
deleted file mode 100644
index 9eea3eb79dc..00000000000
--- a/spec/lib/gitlab/metrics/sidekiq_metrics_exporter_spec.rb
+++ /dev/null
@@ -1,105 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-describe Gitlab::Metrics::SidekiqMetricsExporter do
- let(:exporter) { described_class.new }
- let(:server) { double('server') }
-
- before do
- allow(::WEBrick::HTTPServer).to receive(:new).and_return(server)
- allow(server).to receive(:mount)
- allow(server).to receive(:start)
- allow(server).to receive(:shutdown)
- end
-
- describe 'when exporter is enabled' do
- before do
- allow(Settings.monitoring.sidekiq_exporter).to receive(:enabled).and_return(true)
- end
-
- describe 'when exporter is stopped' do
- describe '#start' do
- it 'starts the exporter' do
- expect { exporter.start.join }.to change { exporter.thread? }.from(false).to(true)
-
- expect(server).to have_received(:start)
- end
-
- describe 'with custom settings' do
- let(:port) { 99999 }
- let(:address) { 'sidekiq_exporter_address' }
-
- before do
- allow(Settings.monitoring.sidekiq_exporter).to receive(:port).and_return(port)
- allow(Settings.monitoring.sidekiq_exporter).to receive(:address).and_return(address)
- end
-
- it 'starts server with port and address from settings' do
- exporter.start.join
-
- expect(::WEBrick::HTTPServer).to have_received(:new).with(
- Port: port,
- BindAddress: address,
- Logger: anything,
- AccessLog: anything
- )
- end
- end
- end
-
- describe '#stop' do
- it "doesn't shutdown stopped server" do
- expect { exporter.stop }.not_to change { exporter.thread? }
-
- expect(server).not_to have_received(:shutdown)
- end
- end
- end
-
- describe 'when exporter is running' do
- before do
- exporter.start.join
- end
-
- describe '#start' do
- it "doesn't start running server" do
- expect { exporter.start.join }.not_to change { exporter.thread? }
-
- expect(server).to have_received(:start).once
- end
- end
-
- describe '#stop' do
- it 'shutdowns server' do
- expect { exporter.stop }.to change { exporter.thread? }.from(true).to(false)
-
- expect(server).to have_received(:shutdown)
- end
- end
- end
- end
-
- describe 'when exporter is disabled' do
- before do
- allow(Settings.monitoring.sidekiq_exporter).to receive(:enabled).and_return(false)
- end
-
- describe '#start' do
- it "doesn't start" do
- expect(exporter.start).to be_nil
- expect { exporter.start }.not_to change { exporter.thread? }
-
- expect(server).not_to have_received(:start)
- end
- end
-
- describe '#stop' do
- it "doesn't shutdown" do
- expect { exporter.stop }.not_to change { exporter.thread? }
-
- expect(server).not_to have_received(:shutdown)
- end
- end
- end
-end
diff --git a/spec/lib/gitlab/metrics/system_spec.rb b/spec/lib/gitlab/metrics/system_spec.rb
index 6d2764a06f2..a5aa80686fd 100644
--- a/spec/lib/gitlab/metrics/system_spec.rb
+++ b/spec/lib/gitlab/metrics/system_spec.rb
@@ -58,4 +58,44 @@ describe Gitlab::Metrics::System do
expect(described_class.monotonic_time).to be_an(Float)
end
end
+
+ describe '.thread_cpu_time' do
+ it 'returns cpu_time on supported platform' do
+ stub_const("Process::CLOCK_THREAD_CPUTIME_ID", 16)
+
+ expect(Process).to receive(:clock_gettime)
+ .with(16, kind_of(Symbol)) { 0.111222333 }
+
+ expect(described_class.thread_cpu_time).to eq(0.111222333)
+ end
+
+ it 'returns nil on unsupported platform' do
+ hide_const("Process::CLOCK_THREAD_CPUTIME_ID")
+
+ expect(described_class.thread_cpu_time).to be_nil
+ end
+ end
+
+ describe '.thread_cpu_duration' do
+ let(:start_time) { described_class.thread_cpu_time }
+
+ it 'returns difference between start and current time' do
+ stub_const("Process::CLOCK_THREAD_CPUTIME_ID", 16)
+
+ expect(Process).to receive(:clock_gettime)
+ .with(16, kind_of(Symbol))
+ .and_return(
+ 0.111222333,
+ 0.222333833
+ )
+
+ expect(described_class.thread_cpu_duration(start_time)).to eq(0.1111115)
+ end
+
+ it 'returns nil on unsupported platform' do
+ hide_const("Process::CLOCK_THREAD_CPUTIME_ID")
+
+ expect(described_class.thread_cpu_duration(start_time)).to be_nil
+ end
+ end
end
diff --git a/spec/lib/gitlab/metrics/transaction_spec.rb b/spec/lib/gitlab/metrics/transaction_spec.rb
index 45e74597a2e..08de2426c5a 100644
--- a/spec/lib/gitlab/metrics/transaction_spec.rb
+++ b/spec/lib/gitlab/metrics/transaction_spec.rb
@@ -27,6 +27,14 @@ describe Gitlab::Metrics::Transaction do
end
end
+ describe '#thread_cpu_duration' do
+ it 'returns the duration of a transaction in seconds' do
+ transaction.run { }
+
+ expect(transaction.thread_cpu_duration).to be > 0
+ end
+ end
+
describe '#allocated_memory' do
it 'returns the allocated memory in bytes' do
transaction.run { 'a' * 32 }
diff --git a/spec/lib/gitlab/middleware/read_only_spec.rb b/spec/lib/gitlab/middleware/read_only_spec.rb
index d2c8f4ab0bd..c7e9b38e3ca 100644
--- a/spec/lib/gitlab/middleware/read_only_spec.rb
+++ b/spec/lib/gitlab/middleware/read_only_spec.rb
@@ -103,6 +103,13 @@ describe Gitlab::Middleware::ReadOnly do
expect(subject).not_to disallow_request
end
+ it 'expects a graphql request to be allowed' do
+ response = request.post("/api/graphql")
+
+ expect(response).not_to be_redirect
+ expect(subject).not_to disallow_request
+ end
+
context 'sidekiq admin requests' do
where(:mounted_at) do
[
diff --git a/spec/lib/gitlab/pages_client_spec.rb b/spec/lib/gitlab/pages_client_spec.rb
deleted file mode 100644
index 84381843221..00000000000
--- a/spec/lib/gitlab/pages_client_spec.rb
+++ /dev/null
@@ -1,174 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-describe Gitlab::PagesClient do
- subject { described_class }
-
- describe '.token' do
- it 'returns the token as it is on disk' do
- pending 'add omnibus support for generating the secret file https://gitlab.com/gitlab-org/omnibus-gitlab/merge_requests/2466'
- expect(subject.token).to eq(File.read('.gitlab_pages_secret'))
- end
- end
-
- describe '.read_or_create_token' do
- subject { described_class.read_or_create_token }
- let(:token_path) { 'tmp/tests/gitlab-pages-secret' }
- before do
- allow(described_class).to receive(:token_path).and_return(token_path)
- FileUtils.rm_f(token_path)
- end
-
- it 'uses the existing token file if it exists' do
- secret = 'existing secret'
- File.write(token_path, secret)
-
- subject
- expect(described_class.token).to eq(secret)
- end
-
- it 'creates one if none exists' do
- pending 'add omnibus support for generating the secret file https://gitlab.com/gitlab-org/omnibus-gitlab/merge_requests/2466'
-
- old_token = described_class.token
- # sanity check
- expect(File.exist?(token_path)).to eq(false)
-
- subject
- expect(described_class.token.bytesize).to eq(64)
- expect(described_class.token).not_to eq(old_token)
- end
- end
-
- describe '.write_token' do
- let(:token_path) { 'tmp/tests/gitlab-pages-secret' }
- before do
- allow(described_class).to receive(:token_path).and_return(token_path)
- FileUtils.rm_f(token_path)
- end
-
- it 'writes the secret' do
- new_secret = 'hello new secret'
- expect(File.exist?(token_path)).to eq(false)
-
- described_class.send(:write_token, new_secret)
-
- expect(File.read(token_path)).to eq(new_secret)
- end
-
- it 'does nothing if the file already exists' do
- existing_secret = 'hello secret'
- File.write(token_path, existing_secret)
-
- described_class.send(:write_token, 'new secret')
-
- expect(File.read(token_path)).to eq(existing_secret)
- end
- end
-
- describe '.load_certificate' do
- subject { described_class.load_certificate }
- before do
- allow(described_class).to receive(:config).and_return(config)
- end
-
- context 'with no certificate in the config' do
- let(:config) { double(:config, certificate: '') }
-
- it 'does not set @certificate' do
- subject
-
- expect(described_class.certificate).to be_nil
- end
- end
-
- context 'with a certificate path in the config' do
- let(:certificate_path) { 'tmp/tests/fake-certificate' }
- let(:config) { double(:config, certificate: certificate_path) }
-
- it 'sets @certificate' do
- certificate_data = "--- BEGIN CERTIFICATE ---\nbla\n--- END CERTIFICATE ---\n"
- File.write(certificate_path, certificate_data)
- subject
-
- expect(described_class.certificate).to eq(certificate_data)
- end
- end
- end
-
- describe '.request_kwargs' do
- let(:token) { 'secret token' }
- let(:auth_header) { 'Bearer c2VjcmV0IHRva2Vu' }
- before do
- allow(described_class).to receive(:token).and_return(token)
- end
-
- context 'without timeout' do
- it { expect(subject.send(:request_kwargs, nil)[:metadata]['authorization']).to eq(auth_header) }
- end
-
- context 'with timeout' do
- let(:timeout) { 1.second }
-
- it 'still sets the authorization header' do
- expect(subject.send(:request_kwargs, timeout)[:metadata]['authorization']).to eq(auth_header)
- end
-
- it 'sets a deadline value' do
- now = Time.now
- deadline = subject.send(:request_kwargs, timeout)[:deadline]
-
- expect(deadline).to be_between(now, now + 2 * timeout)
- end
- end
- end
-
- describe '.stub' do
- before do
- allow(described_class).to receive(:address).and_return('unix:/foo/bar')
- end
-
- it { expect(subject.send(:stub, :health_check)).to be_a(Grpc::Health::V1::Health::Stub) }
- end
-
- describe '.address' do
- subject { described_class.send(:address) }
-
- before do
- allow(described_class).to receive(:config).and_return(config)
- end
-
- context 'with a unix: address' do
- let(:config) { double(:config, address: 'unix:/foo/bar') }
-
- it { expect(subject).to eq('unix:/foo/bar') }
- end
-
- context 'with a tcp:// address' do
- let(:config) { double(:config, address: 'tcp://localhost:1234') }
-
- it { expect(subject).to eq('localhost:1234') }
- end
- end
-
- describe '.grpc_creds' do
- subject { described_class.send(:grpc_creds) }
-
- before do
- allow(described_class).to receive(:config).and_return(config)
- end
-
- context 'with a unix: address' do
- let(:config) { double(:config, address: 'unix:/foo/bar') }
-
- it { expect(subject).to eq(:this_channel_is_insecure) }
- end
-
- context 'with a tcp:// address' do
- let(:config) { double(:config, address: 'tcp://localhost:1234') }
-
- it { expect(subject).to be_a(GRPC::Core::ChannelCredentials) }
- end
- end
-end
diff --git a/spec/lib/gitlab/patch/prependable_spec.rb b/spec/lib/gitlab/patch/prependable_spec.rb
index 725d733d176..255324f89d5 100644
--- a/spec/lib/gitlab/patch/prependable_spec.rb
+++ b/spec/lib/gitlab/patch/prependable_spec.rb
@@ -72,8 +72,8 @@ describe Gitlab::Patch::Prependable do
expect(subject.ancestors.take(3)).to eq([subject, ee, ce])
expect(subject.singleton_class.ancestors.take(3))
.to eq([subject.singleton_class,
- ee.const_get(:ClassMethods),
- ce.const_get(:ClassMethods)])
+ ee.const_get(:ClassMethods, false),
+ ce.const_get(:ClassMethods, false)])
end
it 'prepends only once even if called twice' do
@@ -115,8 +115,8 @@ describe Gitlab::Patch::Prependable do
it 'has the expected ancestors' do
expect(subject.ancestors.take(3)).to eq([ee, ce, subject])
expect(subject.singleton_class.ancestors.take(3))
- .to eq([ee.const_get(:ClassMethods),
- ce.const_get(:ClassMethods),
+ .to eq([ee.const_get(:ClassMethods, false),
+ ce.const_get(:ClassMethods, false),
subject.singleton_class])
end
@@ -152,7 +152,7 @@ describe Gitlab::Patch::Prependable do
it 'has the expected ancestors' do
expect(subject.ancestors.take(2)).to eq([ee, subject])
expect(subject.singleton_class.ancestors.take(2))
- .to eq([ee.const_get(:ClassMethods),
+ .to eq([ee.const_get(:ClassMethods, false),
subject.singleton_class])
end
diff --git a/spec/lib/gitlab/path_regex_spec.rb b/spec/lib/gitlab/path_regex_spec.rb
index 0829a2b4334..3cbcae4cdeb 100644
--- a/spec/lib/gitlab/path_regex_spec.rb
+++ b/spec/lib/gitlab/path_regex_spec.rb
@@ -108,7 +108,7 @@ describe Gitlab::PathRegex do
git = Gitlab.config.git.bin_path
tracked = `cd #{Rails.root} && #{git} ls-files public`
.split("\n")
- .map { |entry| entry.gsub('public/', '') }
+ .map { |entry| entry.start_with?('public/-/') ? '-' : entry.gsub('public/', '') }
.uniq
tracked + %w(assets uploads)
end
diff --git a/spec/lib/gitlab/phabricator_import/worker_state_spec.rb b/spec/lib/gitlab/phabricator_import/worker_state_spec.rb
index b6f2524a9d0..51514dd0ffd 100644
--- a/spec/lib/gitlab/phabricator_import/worker_state_spec.rb
+++ b/spec/lib/gitlab/phabricator_import/worker_state_spec.rb
@@ -4,6 +4,7 @@ require 'spec_helper'
describe Gitlab::PhabricatorImport::WorkerState, :clean_gitlab_redis_shared_state do
subject(:state) { described_class.new('weird-project-id') }
+
let(:key) { 'phabricator-import/jobs/project-weird-project-id/job-count' }
describe '#add_job' do
diff --git a/spec/lib/gitlab/reference_extractor_spec.rb b/spec/lib/gitlab/reference_extractor_spec.rb
index 7513dbeeb6f..6bc9b6365d1 100644
--- a/spec/lib/gitlab/reference_extractor_spec.rb
+++ b/spec/lib/gitlab/reference_extractor_spec.rb
@@ -259,13 +259,15 @@ describe Gitlab::ReferenceExtractor do
describe '.references_pattern' do
subject { described_class.references_pattern }
+
it { is_expected.to be_kind_of Regexp }
end
describe 'referables prefixes' do
def prefixes
described_class::REFERABLES.each_with_object({}) do |referable, result|
- klass = referable.to_s.camelize.constantize
+ class_name = referable.to_s.camelize
+ klass = class_name.constantize if Object.const_defined?(class_name)
next unless klass.respond_to?(:reference_prefix)
diff --git a/spec/lib/gitlab/regex_spec.rb b/spec/lib/gitlab/regex_spec.rb
index 3036e3a9754..b557baed258 100644
--- a/spec/lib/gitlab/regex_spec.rb
+++ b/spec/lib/gitlab/regex_spec.rb
@@ -64,4 +64,15 @@ describe Gitlab::Regex do
it { is_expected.not_to match('.my/image') }
it { is_expected.not_to match('my/image.') }
end
+
+ describe '.aws_account_id_regex' do
+ subject { described_class.aws_arn_regex }
+
+ it { is_expected.to match('arn:aws:iam::123456789012:role/role-name') }
+ it { is_expected.to match('arn:aws:s3:::bucket/key') }
+ it { is_expected.to match('arn:aws:ec2:us-east-1:123456789012:volume/vol-1') }
+ it { is_expected.to match('arn:aws:rds:us-east-1:123456789012:pg:prod') }
+ it { is_expected.not_to match('123456789012') }
+ it { is_expected.not_to match('role/role-name') }
+ end
end
diff --git a/spec/lib/gitlab/request_context_spec.rb b/spec/lib/gitlab/request_context_spec.rb
index a744f48da1f..cde12d4b310 100644
--- a/spec/lib/gitlab/request_context_spec.rb
+++ b/spec/lib/gitlab/request_context_spec.rb
@@ -5,6 +5,7 @@ require 'spec_helper'
describe Gitlab::RequestContext do
describe '#client_ip' do
subject { described_class.client_ip }
+
let(:app) { -> (env) {} }
let(:env) { Hash.new }
diff --git a/spec/lib/gitlab/sanitizers/exif_spec.rb b/spec/lib/gitlab/sanitizers/exif_spec.rb
index f882dbbdb5c..11e430e0be4 100644
--- a/spec/lib/gitlab/sanitizers/exif_spec.rb
+++ b/spec/lib/gitlab/sanitizers/exif_spec.rb
@@ -58,7 +58,7 @@ describe Gitlab::Sanitizers::Exif do
end
describe '#clean' do
- let(:uploader) { create(:upload, :with_file, :issuable_upload).build_uploader }
+ let(:uploader) { create(:upload, :with_file, :issuable_upload).retrieve_uploader }
context "no dry run" do
it "removes exif from the image" do
diff --git a/spec/lib/gitlab/search_results_spec.rb b/spec/lib/gitlab/search_results_spec.rb
index 26cba53502d..86dde15cc8a 100644
--- a/spec/lib/gitlab/search_results_spec.rb
+++ b/spec/lib/gitlab/search_results_spec.rb
@@ -57,8 +57,8 @@ describe Gitlab::SearchResults do
where(:count, :expected) do
23 | '23'
- 100 | '100'
- 101 | max_limited_count
+ 99 | '99'
+ 100 | max_limited_count
1234 | max_limited_count
end
diff --git a/spec/lib/gitlab/shell_spec.rb b/spec/lib/gitlab/shell_spec.rb
index 55d8bac6c03..a17e9a31212 100644
--- a/spec/lib/gitlab/shell_spec.rb
+++ b/spec/lib/gitlab/shell_spec.rb
@@ -396,6 +396,7 @@ describe Gitlab::Shell do
describe 'namespace actions' do
subject { described_class.new }
+
let(:storage) { Gitlab.config.repositories.storages.keys.first }
describe '#add_namespace' do
@@ -422,6 +423,30 @@ describe Gitlab::Shell do
end
end
+ describe '#repository_exists?' do
+ context 'when the storage path does not exist' do
+ subject { described_class.new.repository_exists?(storage, "non-existing.git") }
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'when the repository does not exist' do
+ let(:project) { create(:project, :repository, :legacy_storage) }
+
+ subject { described_class.new.repository_exists?(storage, "#{project.repository.disk_path}-some-other-repo.git") }
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'when the repository exists' do
+ let(:project) { create(:project, :repository, :legacy_storage) }
+
+ subject { described_class.new.repository_exists?(storage, "#{project.repository.disk_path}.git") }
+
+ it { is_expected.to be_truthy }
+ end
+ end
+
describe '#remove' do
it 'removes the namespace' do
subject.add_namespace(storage, "mepmep")
diff --git a/spec/lib/gitlab/sidekiq_daemon/memory_killer_spec.rb b/spec/lib/gitlab/sidekiq_daemon/memory_killer_spec.rb
new file mode 100644
index 00000000000..45bcc71dfcb
--- /dev/null
+++ b/spec/lib/gitlab/sidekiq_daemon/memory_killer_spec.rb
@@ -0,0 +1,501 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::SidekiqDaemon::MemoryKiller do
+ let(:memory_killer) { described_class.new }
+ let(:pid) { 12345 }
+
+ before do
+ allow(Sidekiq.logger).to receive(:info)
+ allow(Sidekiq.logger).to receive(:warn)
+ allow(memory_killer).to receive(:pid).and_return(pid)
+
+ # make sleep no-op
+ allow(memory_killer).to receive(:sleep) {}
+ end
+
+ describe '#run_thread' do
+ subject { memory_killer.send(:run_thread) }
+
+ before do
+ # let enabled? return 3 times: true, true, false
+ allow(memory_killer).to receive(:enabled?).and_return(true, true, false)
+ end
+
+ context 'when structured logging is used' do
+ it 'logs start message once' do
+ expect(Sidekiq.logger).to receive(:info).once
+ .with(
+ class: described_class.to_s,
+ action: 'start',
+ pid: pid,
+ message: 'Starting Gitlab::SidekiqDaemon::MemoryKiller Daemon')
+
+ subject
+ end
+
+ it 'logs StandardError message twice' do
+ expect(Sidekiq.logger).to receive(:warn).twice
+ .with(
+ class: described_class.to_s,
+ pid: pid,
+ message: "Exception from run_thread: My Exception")
+
+ expect(memory_killer).to receive(:rss_within_range?)
+ .twice
+ .and_raise(StandardError, 'My Exception')
+
+ expect { subject }.not_to raise_exception
+ end
+
+ it 'logs exception message once and raise execption and log stop message' do
+ expect(Sidekiq.logger).to receive(:warn).once
+ .with(
+ class: described_class.to_s,
+ pid: pid,
+ message: "Exception from run_thread: My Exception")
+
+ expect(memory_killer).to receive(:rss_within_range?)
+ .once
+ .and_raise(Exception, 'My Exception')
+
+ expect(memory_killer).to receive(:sleep).with(Gitlab::SidekiqDaemon::MemoryKiller::CHECK_INTERVAL_SECONDS)
+ expect(Sidekiq.logger).to receive(:warn).once
+ .with(
+ class: described_class.to_s,
+ action: 'stop',
+ pid: pid,
+ message: 'Stopping Gitlab::SidekiqDaemon::MemoryKiller Daemon')
+
+ expect { subject }.to raise_exception
+ end
+
+ it 'logs stop message once' do
+ expect(Sidekiq.logger).to receive(:warn).once
+ .with(
+ class: described_class.to_s,
+ action: 'stop',
+ pid: pid,
+ message: 'Stopping Gitlab::SidekiqDaemon::MemoryKiller Daemon')
+
+ subject
+ end
+ end
+
+ it 'not invoke restart_sidekiq when rss in range' do
+ expect(memory_killer).to receive(:rss_within_range?)
+ .twice
+ .and_return(true)
+
+ expect(memory_killer).not_to receive(:restart_sidekiq)
+
+ subject
+ end
+
+ it 'invoke restart_sidekiq when rss not in range' do
+ expect(memory_killer).to receive(:rss_within_range?)
+ .at_least(:once)
+ .and_return(false)
+
+ expect(memory_killer).to receive(:restart_sidekiq)
+ .at_least(:once)
+
+ subject
+ end
+ end
+
+ describe '#stop_working' do
+ subject { memory_killer.send(:stop_working)}
+
+ it 'changes enable? to false' do
+ expect { subject }.to change { memory_killer.send(:enabled?) }
+ .from(true).to(false)
+ end
+ end
+
+ describe '#rss_within_range?' do
+ let(:shutdown_timeout_seconds) { 7 }
+ let(:check_interval_seconds) { 2 }
+ let(:grace_balloon_seconds) { 5 }
+
+ subject { memory_killer.send(:rss_within_range?) }
+
+ before do
+ stub_const("#{described_class}::SHUTDOWN_TIMEOUT_SECONDS", shutdown_timeout_seconds)
+ stub_const("#{described_class}::CHECK_INTERVAL_SECONDS", check_interval_seconds)
+ stub_const("#{described_class}::GRACE_BALLOON_SECONDS", grace_balloon_seconds)
+ allow(Process).to receive(:getpgrp).and_return(pid)
+ allow(Sidekiq).to receive(:options).and_return(timeout: 9)
+ end
+
+ it 'return true when everything is within limit' do
+ expect(memory_killer).to receive(:get_rss).and_return(100)
+ expect(memory_killer).to receive(:get_soft_limit_rss).and_return(200)
+ expect(memory_killer).to receive(:get_hard_limit_rss).and_return(300)
+
+ expect(memory_killer).to receive(:refresh_state)
+ .with(:running)
+ .and_call_original
+
+ expect(Gitlab::Metrics::System).to receive(:monotonic_time).and_call_original
+ expect(memory_killer).not_to receive(:log_rss_out_of_range)
+
+ expect(subject).to be true
+ end
+
+ it 'return false when rss exceeds hard_limit_rss' do
+ expect(memory_killer).to receive(:get_rss).at_least(:once).and_return(400)
+ expect(memory_killer).to receive(:get_soft_limit_rss).at_least(:once).and_return(200)
+ expect(memory_killer).to receive(:get_hard_limit_rss).at_least(:once).and_return(300)
+
+ expect(memory_killer).to receive(:refresh_state)
+ .with(:running)
+ .and_call_original
+
+ expect(memory_killer).to receive(:refresh_state)
+ .with(:above_soft_limit)
+ .and_call_original
+
+ expect(Gitlab::Metrics::System).to receive(:monotonic_time).and_call_original
+
+ expect(memory_killer).to receive(:log_rss_out_of_range).with(400, 300, 200)
+
+ expect(subject).to be false
+ end
+
+ it 'return false when rss exceed hard_limit_rss after a while' do
+ expect(memory_killer).to receive(:get_rss).and_return(250, 400, 400)
+ expect(memory_killer).to receive(:get_soft_limit_rss).at_least(:once).and_return(200)
+ expect(memory_killer).to receive(:get_hard_limit_rss).at_least(:once).and_return(300)
+
+ expect(memory_killer).to receive(:refresh_state)
+ .with(:running)
+ .and_call_original
+
+ expect(memory_killer).to receive(:refresh_state)
+ .at_least(:once)
+ .with(:above_soft_limit)
+ .and_call_original
+
+ expect(Gitlab::Metrics::System).to receive(:monotonic_time).twice.and_call_original
+ expect(memory_killer).to receive(:sleep).with(check_interval_seconds)
+ expect(memory_killer).to receive(:log_rss_out_of_range).with(400, 300, 200)
+
+ expect(subject).to be false
+ end
+
+ it 'return true when rss below soft_limit_rss after a while within GRACE_BALLOON_SECONDS' do
+ expect(memory_killer).to receive(:get_rss).and_return(250, 100)
+ expect(memory_killer).to receive(:get_soft_limit_rss).and_return(200, 200)
+ expect(memory_killer).to receive(:get_hard_limit_rss).and_return(300, 300)
+
+ expect(memory_killer).to receive(:refresh_state)
+ .with(:running)
+ .and_call_original
+
+ expect(memory_killer).to receive(:refresh_state)
+ .with(:above_soft_limit)
+ .and_call_original
+
+ expect(Gitlab::Metrics::System).to receive(:monotonic_time).twice.and_call_original
+ expect(memory_killer).to receive(:sleep).with(check_interval_seconds)
+
+ expect(memory_killer).not_to receive(:log_rss_out_of_range)
+
+ expect(subject).to be true
+ end
+
+ context 'when exceeding GRACE_BALLOON_SECONDS' do
+ let(:grace_balloon_seconds) { 0 }
+
+ it 'return false when rss exceed soft_limit_rss' do
+ allow(memory_killer).to receive(:get_rss).and_return(250)
+ allow(memory_killer).to receive(:get_soft_limit_rss).and_return(200)
+ allow(memory_killer).to receive(:get_hard_limit_rss).and_return(300)
+
+ expect(memory_killer).to receive(:refresh_state)
+ .with(:running)
+ .and_call_original
+
+ expect(memory_killer).to receive(:refresh_state)
+ .with(:above_soft_limit)
+ .and_call_original
+
+ expect(memory_killer).to receive(:log_rss_out_of_range)
+ .with(250, 300, 200)
+
+ expect(subject).to be false
+ end
+ end
+ end
+
+ describe '#restart_sidekiq' do
+ let(:shutdown_timeout_seconds) { 7 }
+
+ subject { memory_killer.send(:restart_sidekiq) }
+
+ before do
+ stub_const("#{described_class}::SHUTDOWN_TIMEOUT_SECONDS", shutdown_timeout_seconds)
+ allow(Sidekiq).to receive(:options).and_return(timeout: 9)
+ allow(memory_killer).to receive(:get_rss).and_return(100)
+ allow(memory_killer).to receive(:get_soft_limit_rss).and_return(200)
+ allow(memory_killer).to receive(:get_hard_limit_rss).and_return(300)
+ end
+
+ it 'send signal' do
+ expect(memory_killer).to receive(:refresh_state)
+ .with(:stop_fetching_new_jobs)
+ .ordered
+ .and_call_original
+ expect(memory_killer).to receive(:signal_and_wait)
+ .with(shutdown_timeout_seconds, 'SIGTSTP', 'stop fetching new jobs')
+ .ordered
+
+ expect(memory_killer).to receive(:refresh_state)
+ .with(:shutting_down)
+ .ordered
+ .and_call_original
+ expect(memory_killer).to receive(:signal_and_wait)
+ .with(11, 'SIGTERM', 'gracefully shut down')
+ .ordered
+
+ expect(memory_killer).to receive(:refresh_state)
+ .with(:killing_sidekiq)
+ .ordered
+ .and_call_original
+ expect(memory_killer).to receive(:signal_pgroup)
+ .with('SIGKILL', 'die')
+ .ordered
+
+ subject
+ end
+ end
+
+ describe '#signal_and_wait' do
+ let(:time) { 0 }
+ let(:signal) { 'my-signal' }
+ let(:explanation) { 'my-explanation' }
+ let(:check_interval_seconds) { 2 }
+
+ subject { memory_killer.send(:signal_and_wait, time, signal, explanation) }
+
+ before do
+ stub_const("#{described_class}::CHECK_INTERVAL_SECONDS", check_interval_seconds)
+ end
+
+ it 'send signal and return when all jobs finished' do
+ expect(Process).to receive(:kill).with(signal, pid).ordered
+ expect(Gitlab::Metrics::System).to receive(:monotonic_time).and_call_original
+
+ expect(memory_killer).to receive(:enabled?).and_return(true)
+ expect(memory_killer).to receive(:any_jobs?).and_return(false)
+
+ expect(memory_killer).not_to receive(:sleep)
+
+ subject
+ end
+
+ it 'send signal and wait till deadline if any job not finished' do
+ expect(Process).to receive(:kill)
+ .with(signal, pid)
+ .ordered
+
+ expect(Gitlab::Metrics::System).to receive(:monotonic_time)
+ .and_call_original
+ .at_least(:once)
+
+ expect(memory_killer).to receive(:enabled?).and_return(true).at_least(:once)
+ expect(memory_killer).to receive(:any_jobs?).and_return(true).at_least(:once)
+
+ subject
+ end
+ end
+
+ describe '#signal_pgroup' do
+ let(:signal) { 'my-signal' }
+ let(:explanation) { 'my-explanation' }
+
+ subject { memory_killer.send(:signal_pgroup, signal, explanation) }
+
+ it 'send signal to this proces if it is not group leader' do
+ expect(Process).to receive(:getpgrp).and_return(pid + 1)
+
+ expect(Sidekiq.logger).to receive(:warn).once
+ .with(
+ class: described_class.to_s,
+ signal: signal,
+ pid: pid,
+ message: "sending Sidekiq worker PID-#{pid} #{signal} (#{explanation})")
+ expect(Process).to receive(:kill).with(signal, pid).ordered
+
+ subject
+ end
+
+ it 'send signal to whole process group as group leader' do
+ expect(Process).to receive(:getpgrp).and_return(pid)
+
+ expect(Sidekiq.logger).to receive(:warn).once
+ .with(
+ class: described_class.to_s,
+ signal: signal,
+ pid: pid,
+ message: "sending Sidekiq worker PGRP-#{pid} #{signal} (#{explanation})")
+ expect(Process).to receive(:kill).with(signal, 0).ordered
+
+ subject
+ end
+ end
+
+ describe '#log_rss_out_of_range' do
+ let(:current_rss) { 100 }
+ let(:soft_limit_rss) { 200 }
+ let(:hard_limit_rss) { 300 }
+ let(:reason) { 'rss out of range reason description' }
+
+ subject { memory_killer.send(:log_rss_out_of_range, current_rss, hard_limit_rss, soft_limit_rss) }
+
+ it 'invoke sidekiq logger warn' do
+ expect(memory_killer).to receive(:out_of_range_description).with(current_rss, hard_limit_rss, soft_limit_rss).and_return(reason)
+ expect(Sidekiq.logger).to receive(:warn)
+ .with(
+ class: described_class.to_s,
+ pid: pid,
+ message: 'Sidekiq worker RSS out of range',
+ current_rss: current_rss,
+ hard_limit_rss: hard_limit_rss,
+ soft_limit_rss: soft_limit_rss,
+ reason: reason)
+
+ subject
+ end
+ end
+
+ describe '#out_of_range_description' do
+ let(:hard_limit) { 300 }
+ let(:soft_limit) { 200 }
+ let(:grace_balloon_seconds) { 12 }
+
+ subject { memory_killer.send(:out_of_range_description, rss, hard_limit, soft_limit) }
+
+ context 'when rss > hard_limit' do
+ let(:rss) { 400 }
+
+ it 'tells reason' do
+ expect(subject).to eq("current_rss(#{rss}) > hard_limit_rss(#{hard_limit})")
+ end
+ end
+
+ context 'when rss <= hard_limit' do
+ let(:rss) { 300 }
+
+ it 'tells reason' do
+ stub_const("#{described_class}::GRACE_BALLOON_SECONDS", grace_balloon_seconds)
+ expect(subject).to eq("current_rss(#{rss}) > soft_limit_rss(#{soft_limit}) longer than GRACE_BALLOON_SECONDS(#{grace_balloon_seconds})")
+ end
+ end
+ end
+
+ describe '#rss_increase_by_jobs' do
+ let(:running_jobs) { { id1: 'job1', id2: 'job2' } }
+
+ subject { memory_killer.send(:rss_increase_by_jobs) }
+
+ it 'adds up individual rss_increase_by_job' do
+ expect(Gitlab::SidekiqDaemon::Monitor).to receive_message_chain(:instance, :jobs).and_return(running_jobs)
+ expect(memory_killer).to receive(:rss_increase_by_job).and_return(11, 22)
+ expect(subject).to eq(33)
+ end
+
+ it 'return 0 if no job' do
+ expect(Gitlab::SidekiqDaemon::Monitor).to receive_message_chain(:instance, :jobs).and_return({})
+ expect(subject).to eq(0)
+ end
+ end
+
+ describe '#rss_increase_by_job' do
+ let(:worker_class) { Chaos::SleepWorker }
+ let(:job) { { worker_class: worker_class, started_at: 321 } }
+ let(:max_memory_kb) { 100000 }
+
+ subject { memory_killer.send(:rss_increase_by_job, job) }
+
+ before do
+ stub_const("#{described_class}::DEFAULT_MAX_MEMORY_GROWTH_KB", max_memory_kb)
+ end
+
+ it 'return 0 if memory_growth_kb return 0' do
+ expect(memory_killer).to receive(:get_job_options).with(job, 'memory_killer_memory_growth_kb', 0).and_return(0)
+ expect(memory_killer).to receive(:get_job_options).with(job, 'memory_killer_max_memory_growth_kb', max_memory_kb).and_return(0)
+
+ expect(Time).not_to receive(:now)
+ expect(subject).to eq(0)
+ end
+
+ it 'return time factored growth value when it does not exceed max growth limit for whilited job' do
+ expect(memory_killer).to receive(:get_job_options).with(job, 'memory_killer_memory_growth_kb', 0).and_return(10)
+ expect(memory_killer).to receive(:get_job_options).with(job, 'memory_killer_max_memory_growth_kb', max_memory_kb).and_return(100)
+
+ expect(Gitlab::Metrics::System).to receive(:monotonic_time).and_return(323)
+ expect(subject).to eq(20)
+ end
+
+ it 'return max growth limit when time factored growth value exceed max growth limit for whilited job' do
+ expect(memory_killer).to receive(:get_job_options).with(job, 'memory_killer_memory_growth_kb', 0).and_return(10)
+ expect(memory_killer).to receive(:get_job_options).with(job, 'memory_killer_max_memory_growth_kb', max_memory_kb).and_return(100)
+
+ expect(Gitlab::Metrics::System).to receive(:monotonic_time).and_return(332)
+ expect(subject).to eq(100)
+ end
+ end
+
+ describe '#get_job_options' do
+ let(:worker_class) { Chaos::SleepWorker }
+ let(:job) { { worker_class: worker_class, started_at: 321 } }
+ let(:key) { 'my-key' }
+ let(:default) { 'my-default' }
+
+ subject { memory_killer.send(:get_job_options, job, key, default) }
+
+ it 'return default if key is not defined' do
+ expect(worker_class).to receive(:sidekiq_options).and_return({ "retry" => 5 })
+
+ expect(subject).to eq(default)
+ end
+
+ it 'return default if get StandardError when retrieve sidekiq_options' do
+ expect(worker_class).to receive(:sidekiq_options).and_raise(StandardError)
+
+ expect(subject).to eq(default)
+ end
+
+ it 'return right value if sidekiq_options has the key' do
+ expect(worker_class).to receive(:sidekiq_options).and_return({ key => 10 })
+
+ expect(subject).to eq(10)
+ end
+ end
+
+ describe '#refresh_state' do
+ let(:metrics) { memory_killer.instance_variable_get(:@metrics) }
+
+ subject { memory_killer.send(:refresh_state, :shutting_down) }
+
+ it 'calls gitlab metrics gauge set methods' do
+ expect(memory_killer).to receive(:get_rss) { 1010 }
+ expect(memory_killer).to receive(:get_soft_limit_rss) { 1020 }
+ expect(memory_killer).to receive(:get_hard_limit_rss) { 1040 }
+
+ expect(metrics[:sidekiq_memory_killer_phase]).to receive(:set)
+ .with({}, described_class::PHASE[:shutting_down])
+ expect(metrics[:sidekiq_current_rss]).to receive(:set)
+ .with({}, 1010)
+ expect(metrics[:sidekiq_memory_killer_soft_limit_rss]).to receive(:set)
+ .with({}, 1020)
+ expect(metrics[:sidekiq_memory_killer_hard_limit_rss]).to receive(:set)
+ .with({}, 1040)
+
+ subject
+ end
+ end
+end
diff --git a/spec/lib/gitlab/sidekiq_daemon/monitor_spec.rb b/spec/lib/gitlab/sidekiq_daemon/monitor_spec.rb
index acbb09e3542..3f49ef0e9a7 100644
--- a/spec/lib/gitlab/sidekiq_daemon/monitor_spec.rb
+++ b/spec/lib/gitlab/sidekiq_daemon/monitor_spec.rb
@@ -8,12 +8,12 @@ describe Gitlab::SidekiqDaemon::Monitor do
describe '#within_job' do
it 'tracks thread' do
blk = proc do
- expect(monitor.jobs_thread['jid']).not_to be_nil
+ expect(monitor.jobs.dig('jid', :thread)).not_to be_nil
"OK"
end
- expect(monitor.within_job('jid', 'queue', &blk)).to eq("OK")
+ expect(monitor.within_job('worker_class', 'jid', 'queue', &blk)).to eq("OK")
end
context 'when job is canceled' do
@@ -25,26 +25,42 @@ describe Gitlab::SidekiqDaemon::Monitor do
it 'does not execute a block' do
expect do |blk|
- monitor.within_job(jid, 'queue', &blk)
+ monitor.within_job('worker_class', jid, 'queue', &blk)
rescue described_class::CancelledError
end.not_to yield_control
end
it 'raises exception' do
- expect { monitor.within_job(jid, 'queue') }.to raise_error(
+ expect { monitor.within_job('worker_class', jid, 'queue') }.to raise_error(
described_class::CancelledError)
end
end
end
- describe '#start_working' do
- subject { monitor.send(:start_working) }
+ describe '#run_thread when notification channel not enabled' do
+ subject { monitor.send(:run_thread) }
+
+ it 'return directly' do
+ allow(monitor).to receive(:notification_channel_enabled?).and_return(nil)
+
+ expect(Sidekiq.logger).not_to receive(:info)
+ expect(Sidekiq.logger).not_to receive(:warn)
+ expect(monitor).not_to receive(:enabled?)
+ expect(monitor).not_to receive(:process_messages)
+
+ subject
+ end
+ end
+
+ describe '#run_thread when notification channel enabled' do
+ subject { monitor.send(:run_thread) }
before do
# we want to run at most once cycle
# we toggle `enabled?` flag after the first call
stub_const('Gitlab::SidekiqDaemon::Monitor::RECONNECT_TIME', 0)
allow(monitor).to receive(:enabled?).and_return(true, false)
+ allow(monitor).to receive(:notification_channel_enabled?).and_return(1)
allow(Sidekiq.logger).to receive(:info)
allow(Sidekiq.logger).to receive(:warn)
@@ -204,7 +220,7 @@ describe Gitlab::SidekiqDaemon::Monitor do
let(:thread) { Thread.new { sleep 1000 } }
before do
- monitor.jobs_thread[jid] = thread
+ monitor.jobs[jid] = { worker_class: 'worker_class', thread: thread, started_at: Time.now.to_i }
end
after do
@@ -258,4 +274,24 @@ describe Gitlab::SidekiqDaemon::Monitor do
subject
end
end
+
+ describe '#notification_channel_enabled?' do
+ subject { monitor.send(:notification_channel_enabled?) }
+
+ it 'return nil when SIDEKIQ_MONITOR_WORKER is not set' do
+ expect(subject).to be nil
+ end
+
+ it 'return nil when SIDEKIQ_MONITOR_WORKER set to 0' do
+ allow(ENV).to receive(:fetch).with('SIDEKIQ_MONITOR_WORKER', 0).and_return("0")
+
+ expect(subject).to be nil
+ end
+
+ it 'return 1 when SIDEKIQ_MONITOR_WORKER set to 1' do
+ allow(ENV).to receive(:fetch).with('SIDEKIQ_MONITOR_WORKER', 0).and_return("1")
+
+ expect(subject).to be 1
+ end
+ end
end
diff --git a/spec/lib/gitlab/sidekiq_logging/exception_handler_spec.rb b/spec/lib/gitlab/sidekiq_logging/exception_handler_spec.rb
new file mode 100644
index 00000000000..24b6090cb19
--- /dev/null
+++ b/spec/lib/gitlab/sidekiq_logging/exception_handler_spec.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::SidekiqLogging::ExceptionHandler do
+ describe '#call' do
+ let(:job) do
+ {
+ "class" => "TestWorker",
+ "args" => [1234, 'hello'],
+ "retry" => false,
+ "queue" => "cronjob:test_queue",
+ "queue_namespace" => "cronjob",
+ "jid" => "da883554ee4fe414012f5f42",
+ "correlation_id" => 'cid'
+ }
+ end
+
+ let(:exception_message) { 'An error was thrown' }
+ let(:backtrace) { caller }
+ let(:exception) { RuntimeError.new(exception_message) }
+ let(:logger) { double }
+
+ before do
+ allow(Sidekiq).to receive(:logger).and_return(logger)
+ allow(exception).to receive(:backtrace).and_return(backtrace)
+ end
+
+ subject { described_class.new.call(exception, { context: 'Test', job: job }) }
+
+ it 'logs job data into root tree' do
+ expected_data = job.merge(
+ error_class: 'RuntimeError',
+ error_message: exception_message,
+ context: 'Test',
+ error_backtrace: Gitlab::Profiler.clean_backtrace(backtrace)
+ )
+
+ expect(logger).to receive(:warn).with(expected_data)
+
+ subject
+ end
+ end
+end
diff --git a/spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb b/spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb
index 1b89c094a6b..46fbc069efb 100644
--- a/spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb
+++ b/spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb
@@ -23,13 +23,15 @@ describe Gitlab::SidekiqLogging::StructuredLogger do
end
let(:logger) { double }
+ let(:clock_thread_cputime_start) { 0.222222299 }
+ let(:clock_thread_cputime_end) { 1.333333799 }
let(:start_payload) do
job.merge(
'message' => 'TestWorker JID-da883554ee4fe414012f5f42: start',
'job_status' => 'start',
'pid' => Process.pid,
- 'created_at' => created_at.iso8601(3),
- 'enqueued_at' => created_at.iso8601(3),
+ 'created_at' => created_at.iso8601(6),
+ 'enqueued_at' => created_at.iso8601(6),
'scheduling_latency_s' => scheduling_latency_s
)
end
@@ -38,16 +40,15 @@ describe Gitlab::SidekiqLogging::StructuredLogger do
'message' => 'TestWorker JID-da883554ee4fe414012f5f42: done: 0.0 sec',
'job_status' => 'done',
'duration' => 0.0,
- "completed_at" => timestamp.iso8601(3),
- "system_s" => 0.0,
- "user_s" => 0.0
+ "completed_at" => timestamp.iso8601(6),
+ "cpu_s" => 1.111112
)
end
let(:exception_payload) do
end_payload.merge(
'message' => 'TestWorker JID-da883554ee4fe414012f5f42: fail: 0.0 sec',
'job_status' => 'fail',
- 'error' => ArgumentError,
+ 'error_class' => 'ArgumentError',
'error_message' => 'some exception'
)
end
@@ -57,12 +58,7 @@ describe Gitlab::SidekiqLogging::StructuredLogger do
allow(subject).to receive(:current_time).and_return(timestamp.to_f)
- allow(Process).to receive(:times).and_return(
- stime: 0.0,
- utime: 0.0,
- cutime: 0.0,
- cstime: 0.0
- )
+ allow(Process).to receive(:clock_gettime).with(Process::CLOCK_THREAD_CPUTIME_ID).and_return(clock_thread_cputime_start, clock_thread_cputime_end)
end
subject { described_class.new }
@@ -86,7 +82,6 @@ describe Gitlab::SidekiqLogging::StructuredLogger do
it 'logs an exception in job' do
Timecop.freeze(timestamp) do
expect(logger).to receive(:info).with(start_payload)
- # This excludes the exception_backtrace
expect(logger).to receive(:warn).with(hash_including(exception_payload))
expect(subject).to receive(:log_job_start).and_call_original
expect(subject).to receive(:log_job_done).and_call_original
@@ -188,31 +183,22 @@ describe Gitlab::SidekiqLogging::StructuredLogger do
end
end
end
+ end
- def ctime(times)
- times[:cstime] + times[:cutime]
- end
+ describe '#add_time_keys!' do
+ let(:time) { { duration: 0.1231234, cputime: 1.2342345 } }
+ let(:payload) { { 'class' => 'my-class', 'message' => 'my-message', 'job_status' => 'my-job-status' } }
+ let(:current_utc_time) { '2019-09-23 10:00:58 UTC' }
+ let(:payload_with_time_keys) { { 'class' => 'my-class', 'message' => 'my-message', 'job_status' => 'my-job-status', 'duration' => 0.123123, 'cpu_s' => 1.234235, 'completed_at' => current_utc_time } }
- context 'with ctime value greater than 0' do
- let(:times_start) { { stime: 0.04999, utime: 0.0483, cstime: 0.0188, cutime: 0.0188 } }
- let(:times_end) { { stime: 0.0699, utime: 0.0699, cstime: 0.0399, cutime: 0.0399 } }
+ subject { described_class.new }
- before do
- end_payload['system_s'] = 0.02
- end_payload['user_s'] = 0.022
- end_payload['child_s'] = 0.042
+ it 'update payload correctly' do
+ expect(Time).to receive_message_chain(:now, :utc).and_return(current_utc_time)
- allow(Process).to receive(:times).and_return(times_start, times_end)
- end
+ subject.send(:add_time_keys!, time, payload)
- it 'logs with ctime data and other cpu data' do
- Timecop.freeze(timestamp) do
- expect(logger).to receive(:info).with(start_payload.except('args')).ordered
- expect(logger).to receive(:info).with(end_payload.except('args')).ordered
-
- subject.call(job, 'test_queue') { }
- end
- end
+ expect(payload).to eq(payload_with_time_keys)
end
end
end
diff --git a/spec/lib/gitlab/sidekiq_middleware/memory_killer_spec.rb b/spec/lib/gitlab/sidekiq_middleware/memory_killer_spec.rb
index bf3bc8e1add..b5be43ec96c 100644
--- a/spec/lib/gitlab/sidekiq_middleware/memory_killer_spec.rb
+++ b/spec/lib/gitlab/sidekiq_middleware/memory_killer_spec.rb
@@ -4,6 +4,7 @@ require 'spec_helper'
describe Gitlab::SidekiqMiddleware::MemoryKiller do
subject { described_class.new }
+
let(:pid) { 999 }
let(:worker) { double(:worker, class: ProjectCacheWorker) }
diff --git a/spec/lib/gitlab/sidekiq_middleware/metrics_spec.rb b/spec/lib/gitlab/sidekiq_middleware/metrics_spec.rb
index ac97a5ebd15..806112fcb16 100644
--- a/spec/lib/gitlab/sidekiq_middleware/metrics_spec.rb
+++ b/spec/lib/gitlab/sidekiq_middleware/metrics_spec.rb
@@ -8,12 +8,14 @@ describe Gitlab::SidekiqMiddleware::Metrics do
let(:worker) { double(:worker) }
let(:completion_seconds_metric) { double('completion seconds metric') }
+ let(:user_execution_seconds_metric) { double('user execution seconds metric') }
let(:failed_total_metric) { double('failed total metric') }
let(:retried_total_metric) { double('retried total metric') }
let(:running_jobs_metric) { double('running jobs metric') }
before do
allow(Gitlab::Metrics).to receive(:histogram).with(:sidekiq_jobs_completion_seconds, anything, anything, anything).and_return(completion_seconds_metric)
+ allow(Gitlab::Metrics).to receive(:histogram).with(:sidekiq_jobs_cpu_seconds, anything, anything, anything).and_return(user_execution_seconds_metric)
allow(Gitlab::Metrics).to receive(:counter).with(:sidekiq_jobs_failed_total, anything).and_return(failed_total_metric)
allow(Gitlab::Metrics).to receive(:counter).with(:sidekiq_jobs_retried_total, anything).and_return(retried_total_metric)
allow(Gitlab::Metrics).to receive(:gauge).with(:sidekiq_running_jobs, anything, {}, :livesum).and_return(running_jobs_metric)
@@ -23,13 +25,16 @@ describe Gitlab::SidekiqMiddleware::Metrics do
it 'yields block' do
allow(completion_seconds_metric).to receive(:observe)
+ allow(user_execution_seconds_metric).to receive(:observe)
expect { |b| middleware.call(worker, {}, :test, &b) }.to yield_control.once
end
it 'sets metrics' do
labels = { queue: :test }
+ allow(middleware).to receive(:get_thread_cputime).and_return(1, 3)
+ expect(user_execution_seconds_metric).to receive(:observe).with(labels, 2)
expect(running_jobs_metric).to receive(:increment).with(labels, 1)
expect(running_jobs_metric).to receive(:increment).with(labels, -1)
expect(completion_seconds_metric).to receive(:observe).with(labels, kind_of(Numeric))
@@ -37,9 +42,17 @@ describe Gitlab::SidekiqMiddleware::Metrics do
middleware.call(worker, {}, :test) { nil }
end
+ it 'ignore user execution when measured 0' do
+ allow(completion_seconds_metric).to receive(:observe)
+ allow(middleware).to receive(:get_thread_cputime).and_return(0, 0)
+
+ expect(user_execution_seconds_metric).not_to receive(:observe)
+ end
+
context 'when job is retried' do
it 'sets sidekiq_jobs_retried_total metric' do
allow(completion_seconds_metric).to receive(:observe)
+ expect(user_execution_seconds_metric).to receive(:observe)
expect(retried_total_metric).to receive(:increment)
diff --git a/spec/lib/gitlab/sidekiq_middleware/monitor_spec.rb b/spec/lib/gitlab/sidekiq_middleware/monitor_spec.rb
index 023df1a6391..398144025ea 100644
--- a/spec/lib/gitlab/sidekiq_middleware/monitor_spec.rb
+++ b/spec/lib/gitlab/sidekiq_middleware/monitor_spec.rb
@@ -12,7 +12,7 @@ describe Gitlab::SidekiqMiddleware::Monitor do
it 'calls Gitlab::SidekiqDaemon::Monitor' do
expect(Gitlab::SidekiqDaemon::Monitor.instance).to receive(:within_job)
- .with('job-id', 'my-queue')
+ .with(anything, 'job-id', 'my-queue')
.and_call_original
expect { |blk| monitor.call(worker, job, queue, &blk) }.to yield_control
diff --git a/spec/lib/gitlab/slash_commands/presenters/access_spec.rb b/spec/lib/gitlab/slash_commands/presenters/access_spec.rb
index f00039c634f..c7b83467660 100644
--- a/spec/lib/gitlab/slash_commands/presenters/access_spec.rb
+++ b/spec/lib/gitlab/slash_commands/presenters/access_spec.rb
@@ -3,6 +3,13 @@
require 'spec_helper'
describe Gitlab::SlashCommands::Presenters::Access do
+ shared_examples_for 'displays an error message' do
+ it do
+ expect(subject[:text]).to match(error_message)
+ expect(subject[:response_type]).to be(:ephemeral)
+ end
+ end
+
describe '#access_denied' do
let(:project) { build(:project) }
@@ -10,9 +17,18 @@ describe Gitlab::SlashCommands::Presenters::Access do
it { is_expected.to be_a(Hash) }
- it 'displays an error message' do
- expect(subject[:text]).to match('are not allowed')
- expect(subject[:response_type]).to be(:ephemeral)
+ it_behaves_like 'displays an error message' do
+ let(:error_message) { 'you do not have access to the GitLab project' }
+ end
+ end
+
+ describe '#deactivated' do
+ subject { described_class.new.deactivated }
+
+ it { is_expected.to be_a(Hash) }
+
+ it_behaves_like 'displays an error message' do
+ let(:error_message) { 'your account has been deactivated by your administrator' }
end
end
diff --git a/spec/lib/gitlab/snippet_search_results_spec.rb b/spec/lib/gitlab/snippet_search_results_spec.rb
index d3353b76c15..47f26fdebe2 100644
--- a/spec/lib/gitlab/snippet_search_results_spec.rb
+++ b/spec/lib/gitlab/snippet_search_results_spec.rb
@@ -6,18 +6,17 @@ describe Gitlab::SnippetSearchResults do
include SearchHelpers
let!(:snippet) { create(:snippet, content: 'foo', file_name: 'foo') }
-
- let(:results) { described_class.new(Snippet.all, 'foo') }
+ let(:results) { described_class.new(snippet.author, 'foo') }
describe '#snippet_titles_count' do
it 'returns the amount of matched snippet titles' do
- expect(results.snippet_titles_count).to eq(1)
+ expect(results.limited_snippet_titles_count).to eq(1)
end
end
describe '#snippet_blobs_count' do
it 'returns the amount of matched snippet blobs' do
- expect(results.snippet_blobs_count).to eq(1)
+ expect(results.limited_snippet_blobs_count).to eq(1)
end
end
@@ -25,10 +24,10 @@ describe Gitlab::SnippetSearchResults do
using RSpec::Parameterized::TableSyntax
where(:scope, :count_method, :expected) do
- 'snippet_titles' | :snippet_titles_count | '1234'
- 'snippet_blobs' | :snippet_blobs_count | '1234'
- 'projects' | :limited_projects_count | max_limited_count
- 'unknown' | nil | nil
+ 'snippet_titles' | :limited_snippet_titles_count | max_limited_count
+ 'snippet_blobs' | :limited_snippet_blobs_count | max_limited_count
+ 'projects' | :limited_projects_count | max_limited_count
+ 'unknown' | nil | nil
end
with_them do
diff --git a/spec/lib/gitlab/submodule_links_spec.rb b/spec/lib/gitlab/submodule_links_spec.rb
index d4420c5b513..f0c8825de74 100644
--- a/spec/lib/gitlab/submodule_links_spec.rb
+++ b/spec/lib/gitlab/submodule_links_spec.rb
@@ -8,7 +8,9 @@ describe Gitlab::SubmoduleLinks do
let(:links) { described_class.new(repo) }
describe '#for' do
- subject { links.for(submodule_item, 'ref') }
+ let(:ref) { 'ref' }
+
+ subject { links.for(submodule_item, ref) }
context 'when there is no .gitmodules file' do
before do
@@ -35,8 +37,20 @@ describe Gitlab::SubmoduleLinks do
stub_urls({ 'gitlab-foss' => 'git@gitlab.com:gitlab-org/gitlab-foss.git' })
end
- it 'returns links' do
+ it 'returns links and caches the by ref' do
expect(subject).to eq(['https://gitlab.com/gitlab-org/gitlab-foss', 'https://gitlab.com/gitlab-org/gitlab-foss/tree/hash'])
+
+ cache_store = links.instance_variable_get("@cache_store")
+
+ expect(cache_store[ref]).to eq({ "gitlab-foss" => "git@gitlab.com:gitlab-org/gitlab-foss.git" })
+ end
+
+ context 'when ref name contains a dash' do
+ let(:ref) { 'signed-commits' }
+
+ it 'returns links' do
+ expect(subject).to eq(['https://gitlab.com/gitlab-org/gitlab-foss', 'https://gitlab.com/gitlab-org/gitlab-foss/tree/hash'])
+ end
end
end
end
diff --git a/spec/lib/gitlab/tracking/incident_management_spec.rb b/spec/lib/gitlab/tracking/incident_management_spec.rb
new file mode 100644
index 00000000000..6f7e04b7c16
--- /dev/null
+++ b/spec/lib/gitlab/tracking/incident_management_spec.rb
@@ -0,0 +1,78 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Tracking::IncidentManagement do
+ describe '.track_from_params' do
+ shared_examples 'a tracked event' do |label, value = nil|
+ it 'creates the tracking event with the correct details' do
+ expect(::Gitlab::Tracking)
+ .to receive(:event)
+ .with(
+ 'IncidentManagement::Settings',
+ label,
+ value || kind_of(Hash)
+ )
+ end
+ end
+
+ after do
+ described_class.track_from_params(params)
+ end
+
+ context 'known params' do
+ known_params = described_class.tracking_keys
+
+ known_params.each do |key, values|
+ context "param #{key}" do
+ let(:params) { { key => '1' } }
+
+ it_behaves_like 'a tracked event', "enabled_#{known_params[key][:name]}"
+ end
+ end
+
+ context 'different input values' do
+ shared_examples 'the correct prefixed event name' do |input, enabled|
+ let(:params) { { issue_template_key: input } }
+
+ it 'matches' do
+ expect(::Gitlab::Tracking)
+ .to receive(:event)
+ .with(
+ anything,
+ "#{enabled}_issue_template_on_alerts",
+ anything
+ )
+ end
+ end
+
+ it_behaves_like 'the correct prefixed event name', 1, 'enabled'
+ it_behaves_like 'the correct prefixed event name', '1', 'enabled'
+ it_behaves_like 'the correct prefixed event name', 'template', 'enabled'
+ it_behaves_like 'the correct prefixed event name', '', 'disabled'
+ it_behaves_like 'the correct prefixed event name', nil, 'disabled'
+ end
+
+ context 'param with label' do
+ let(:params) { { issue_template_key: '1' } }
+
+ it_behaves_like 'a tracked event', "enabled_issue_template_on_alerts", { label: 'Template name', property: '1' }
+ end
+
+ context 'param without label' do
+ let(:params) { { create_issue: '1' } }
+
+ it_behaves_like 'a tracked event', "enabled_issue_auto_creation_on_alerts", {}
+ end
+ end
+
+ context 'unknown params' do
+ let(:params) { { 'unknown' => '1' } }
+
+ it 'does not create the tracking event' do
+ expect(::Gitlab::Tracking)
+ .not_to receive(:event)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/tracking_spec.rb b/spec/lib/gitlab/tracking_spec.rb
index 3cce82e522b..50488dba48c 100644
--- a/spec/lib/gitlab/tracking_spec.rb
+++ b/spec/lib/gitlab/tracking_spec.rb
@@ -12,10 +12,8 @@ describe Gitlab::Tracking do
end
describe '.snowplow_options' do
- subject(&method(:described_class))
-
it 'returns useful client options' do
- expect(subject.snowplow_options(nil)).to eq(
+ expect(described_class.snowplow_options(nil)).to eq(
namespace: 'gl',
hostname: 'gitfoo.com',
cookieDomain: '.gitfoo.com',
@@ -32,25 +30,37 @@ describe Gitlab::Tracking do
'_group_'
).and_return(false)
- expect(subject.snowplow_options('_group_')).to include(
+ expect(described_class.snowplow_options('_group_')).to include(
formTracking: false,
linkClickTracking: false
)
end
end
- describe '.event' do
- subject(&method(:described_class))
+ describe 'tracking events' do
+ shared_examples 'events not tracked' do
+ it 'does not track events' do
+ stub_application_setting(snowplow_enabled: false)
+ expect(SnowplowTracker::AsyncEmitter).not_to receive(:new)
+ expect(SnowplowTracker::Tracker).not_to receive(:new)
+
+ track_event
+ end
+ end
around do |example|
Timecop.freeze(timestamp) { example.run }
end
- it 'can track events' do
- tracker = double
+ before do
+ described_class.instance_variable_set("@snowplow", nil)
+ end
- expect(SnowplowTracker::Emitter).to receive(:new).with(
- 'gitfoo.com'
+ let(:tracker) { double }
+
+ def receive_events
+ expect(SnowplowTracker::AsyncEmitter).to receive(:new).with(
+ 'gitfoo.com', { protocol: 'https' }
).and_return('_emitter_')
expect(SnowplowTracker::Tracker).to receive(:new).with(
@@ -59,30 +69,67 @@ describe Gitlab::Tracking do
'gl',
'_abc123_'
).and_return(tracker)
+ end
- expect(tracker).to receive(:track_struct_event).with(
- 'category',
- 'action',
- '_label_',
- '_property_',
- '_value_',
- '_context_',
- timestamp.to_i
- )
+ describe '.event' do
+ let(:track_event) do
+ described_class.event('category', 'action',
+ label: '_label_',
+ property: '_property_',
+ value: '_value_',
+ context: nil
+ )
+ end
- subject.event('category', 'action',
- label: '_label_',
- property: '_property_',
- value: '_value_',
- context: '_context_'
- )
+ it_behaves_like 'events not tracked'
+
+ it 'can track events' do
+ receive_events
+ expect(tracker).to receive(:track_struct_event).with(
+ 'category',
+ 'action',
+ '_label_',
+ '_property_',
+ '_value_',
+ nil,
+ timestamp.to_i
+ )
+
+ track_event
+ end
end
- it 'does not track when not enabled' do
- stub_application_setting(snowplow_enabled: false)
- expect(SnowplowTracker::Tracker).not_to receive(:new)
+ describe '.self_describing_event' do
+ let(:track_event) do
+ described_class.self_describing_event('iglu:com.gitlab/example/jsonschema/1-0-2',
+ {
+ foo: 'bar',
+ foo_count: 42
+ },
+ context: nil
+ )
+ end
+
+ it_behaves_like 'events not tracked'
+
+ it 'can track self describing events' do
+ receive_events
+ expect(SnowplowTracker::SelfDescribingJson).to receive(:new).with(
+ 'iglu:com.gitlab/example/jsonschema/1-0-2',
+ {
+ foo: 'bar',
+ foo_count: 42
+ }
+ ).and_return('_event_json_')
+
+ expect(tracker).to receive(:track_self_describing_event).with(
+ '_event_json_',
+ nil,
+ timestamp.to_i
+ )
- subject.event('epics', 'action', property: 'what', value: 'doit')
+ track_event
+ end
end
end
end
diff --git a/spec/lib/gitlab/url_blocker_spec.rb b/spec/lib/gitlab/url_blocker_spec.rb
index 0e66e959b24..a68ba489986 100644
--- a/spec/lib/gitlab/url_blocker_spec.rb
+++ b/spec/lib/gitlab/url_blocker_spec.rb
@@ -62,6 +62,14 @@ describe Gitlab::UrlBlocker do
expect { subject }.to raise_error(described_class::BlockedUrlError)
end
end
+
+ context 'when domain is too long' do
+ let(:import_url) { 'https://example' + 'a' * 1024 + '.com' }
+
+ it 'raises an error' do
+ expect { subject }.to raise_error(described_class::BlockedUrlError)
+ end
+ end
end
context 'when the URL hostname is an IP address' do
diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb
index 62787c5abaf..f2e864472c5 100644
--- a/spec/lib/gitlab/usage_data_spec.rb
+++ b/spec/lib/gitlab/usage_data_spec.rb
@@ -38,7 +38,7 @@ describe Gitlab::UsageData do
subject { described_class.data }
- it 'gathers usage data' do
+ it 'gathers usage data', :aggregate_failures do
expect(subject.keys).to include(*%i(
active_user_count
counts
@@ -55,6 +55,7 @@ describe Gitlab::UsageData do
omniauth_enabled
reply_by_email_enabled
container_registry_enabled
+ dependency_proxy_enabled
gitlab_shared_runners_enabled
gitlab_pages
git
@@ -63,31 +64,29 @@ describe Gitlab::UsageData do
avg_cycle_analytics
influxdb_metrics_enabled
prometheus_metrics_enabled
- cycle_analytics_views
- productivity_analytics_views
))
-
- expect(subject).to include(
- snippet_create: a_kind_of(Integer),
- snippet_update: a_kind_of(Integer),
- snippet_comment: a_kind_of(Integer),
- merge_request_comment: a_kind_of(Integer),
- merge_request_create: a_kind_of(Integer),
- commit_comment: a_kind_of(Integer),
- wiki_pages_create: a_kind_of(Integer),
- wiki_pages_update: a_kind_of(Integer),
- wiki_pages_delete: a_kind_of(Integer),
- web_ide_views: a_kind_of(Integer),
- web_ide_commits: a_kind_of(Integer),
- web_ide_merge_requests: a_kind_of(Integer),
- navbar_searches: a_kind_of(Integer),
- cycle_analytics_views: a_kind_of(Integer),
- productivity_analytics_views: a_kind_of(Integer),
- source_code_pushes: a_kind_of(Integer)
- )
end
it 'gathers usage counts' do
+ smau_keys = %i(
+ snippet_create
+ snippet_update
+ snippet_comment
+ merge_request_comment
+ merge_request_create
+ commit_comment
+ wiki_pages_create
+ wiki_pages_update
+ wiki_pages_delete
+ web_ide_views
+ web_ide_commits
+ web_ide_merge_requests
+ navbar_searches
+ cycle_analytics_views
+ productivity_analytics_views
+ source_code_pushes
+ )
+
expected_keys = %i(
assignee_lists
boards
@@ -152,18 +151,18 @@ describe Gitlab::UsageData do
todos
uploads
web_hooks
- user_preferences
- )
+ ).push(*smau_keys)
count_data = subject[:counts]
expect(count_data[:boards]).to eq(1)
expect(count_data[:projects]).to eq(4)
+ expect(count_data.values_at(*smau_keys)).to all(be_an(Integer))
expect(count_data.keys).to include(*expected_keys)
expect(expected_keys - count_data.keys).to be_empty
end
- it 'gathers projects data correctly' do
+ it 'gathers projects data correctly', :aggregate_failures do
count_data = subject[:counts]
expect(count_data[:projects]).to eq(4)
@@ -209,11 +208,8 @@ describe Gitlab::UsageData do
describe 'the results of calling #totals on all objects in the array' do
subject { described_class.usage_data_counters.map(&:totals) }
- it do
- is_expected
- .to all(be_a Hash)
- .and all(have_attributes(keys: all(be_a Symbol), values: all(be_a Integer)))
- end
+ it { is_expected.to all(be_a Hash) }
+ it { is_expected.to all(have_attributes(keys: all(be_a Symbol), values: all(be_a Integer))) }
end
it 'does not have any conflicts' do
@@ -226,7 +222,7 @@ describe Gitlab::UsageData do
describe '#features_usage_data_ce' do
subject { described_class.features_usage_data_ce }
- it 'gathers feature usage data' do
+ it 'gathers feature usage data', :aggregate_failures do
expect(subject[:mattermost_enabled]).to eq(Gitlab.config.mattermost.enabled)
expect(subject[:signup_enabled]).to eq(Gitlab::CurrentSettings.allow_signup?)
expect(subject[:ldap_enabled]).to eq(Gitlab.config.ldap.enabled)
@@ -234,6 +230,7 @@ describe Gitlab::UsageData do
expect(subject[:omniauth_enabled]).to eq(Gitlab::Auth.omniauth_enabled?)
expect(subject[:reply_by_email_enabled]).to eq(Gitlab::IncomingEmail.enabled?)
expect(subject[:container_registry_enabled]).to eq(Gitlab.config.registry.enabled)
+ expect(subject[:dependency_proxy_enabled]).to eq(Gitlab.config.dependency_proxy.enabled)
expect(subject[:gitlab_shared_runners_enabled]).to eq(Gitlab.config.gitlab_ci.shared_runners_enabled)
end
end
@@ -241,7 +238,7 @@ describe Gitlab::UsageData do
describe '#components_usage_data' do
subject { described_class.components_usage_data }
- it 'gathers components usage data' do
+ it 'gathers components usage data', :aggregate_failures do
expect(subject[:gitlab_pages][:enabled]).to eq(Gitlab.config.pages.enabled)
expect(subject[:gitlab_pages][:version]).to eq(Gitlab::Pages::VERSION)
expect(subject[:git][:version]).to eq(Gitlab::Git.version)
@@ -257,7 +254,7 @@ describe Gitlab::UsageData do
describe '#license_usage_data' do
subject { described_class.license_usage_data }
- it 'gathers license data' do
+ it 'gathers license data', :aggregate_failures do
expect(subject[:uuid]).to eq(Gitlab::CurrentSettings.uuid)
expect(subject[:version]).to eq(Gitlab::VERSION)
expect(subject[:installation_type]).to eq('gitlab-development-kit')
@@ -289,11 +286,11 @@ describe Gitlab::UsageData do
end
describe '#approximate_counts' do
- it 'gets approximate counts for selected models' do
+ it 'gets approximate counts for selected models', :aggregate_failures do
create(:label)
expect(Gitlab::Database::Count).to receive(:approximate_counts)
- .with(described_class::APPROXIMATE_COUNT_MODELS).once.and_call_original
+ .with(described_class::APPROXIMATE_COUNT_MODELS).once.and_call_original
counts = described_class.approximate_counts.values
@@ -301,14 +298,12 @@ describe Gitlab::UsageData do
expect(counts.any? { |count| count < 0 }).to be_falsey
end
- it 'returns default values if counts can not be retrieved' do
+ it 'returns default values if counts can not be retrieved', :aggregate_failures do
described_class::APPROXIMATE_COUNT_MODELS.map do |model|
model.name.underscore.pluralize.to_sym
end
- expect(Gitlab::Database::Count).to receive(:approximate_counts)
- .and_return({})
-
+ expect(Gitlab::Database::Count).to receive(:approximate_counts).and_return({})
expect(described_class.approximate_counts.values.uniq).to eq([-1])
end
end
diff --git a/spec/lib/gitlab/utils/inline_hash_spec.rb b/spec/lib/gitlab/utils/inline_hash_spec.rb
new file mode 100644
index 00000000000..867db0b92a5
--- /dev/null
+++ b/spec/lib/gitlab/utils/inline_hash_spec.rb
@@ -0,0 +1,70 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+
+describe Gitlab::Utils::InlineHash do
+ describe '.merge_keys' do
+ subject { described_class.merge_keys(source) }
+
+ let(:source) do
+ {
+ nested_param: {
+ key: :Value
+ },
+ 'root_param' => 'Root',
+ unnested_symbol_key: :unnested_symbol_value,
+ 12 => 22,
+ 'very' => {
+ 'deep' => {
+ 'nested' => {
+ 'param' => 'Deep nested value'
+ }
+ }
+ }
+ }
+ end
+
+ it 'transforms a nested hash into a one-level hash' do
+ is_expected.to eq(
+ 'nested_param.key' => :Value,
+ 'root_param' => 'Root',
+ :unnested_symbol_key => :unnested_symbol_value,
+ 12 => 22,
+ 'very.deep.nested.param' => 'Deep nested value'
+ )
+ end
+
+ it 'retains key insertion order' do
+ expect(subject.keys)
+ .to eq(['nested_param.key', 'root_param', :unnested_symbol_key, 12, 'very.deep.nested.param'])
+ end
+
+ context 'with a custom connector' do
+ subject { described_class.merge_keys(source, connector: '::') }
+
+ it 'uses the connector to merge keys' do
+ is_expected.to eq(
+ 'nested_param::key' => :Value,
+ 'root_param' => 'Root',
+ :unnested_symbol_key => :unnested_symbol_value,
+ 12 => 22,
+ 'very::deep::nested::param' => 'Deep nested value'
+ )
+ end
+ end
+
+ context 'with a starter prefix' do
+ subject { described_class.merge_keys(source, prefix: 'options') }
+
+ it 'prefixes all the keys' do
+ is_expected.to eq(
+ 'options.nested_param.key' => :Value,
+ 'options.root_param' => 'Root',
+ 'options.unnested_symbol_key' => :unnested_symbol_value,
+ 'options.12' => 22,
+ 'options.very.deep.nested.param' => 'Deep nested value'
+ )
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/utils/override_spec.rb b/spec/lib/gitlab/utils/override_spec.rb
index 5855c4374a9..e2776efac85 100644
--- a/spec/lib/gitlab/utils/override_spec.rb
+++ b/spec/lib/gitlab/utils/override_spec.rb
@@ -151,6 +151,7 @@ describe Gitlab::Utils::Override do
context 'when subject is a module, and class is prepending it' do
subject { extension }
+
let(:klass) { prepending_class }
it_behaves_like 'checking as intended'
@@ -158,6 +159,7 @@ describe Gitlab::Utils::Override do
context 'when subject is a module, and class is including it' do
subject { extension }
+
let(:klass) { including_class }
it_behaves_like 'checking as intended, nothing was overridden'
@@ -177,6 +179,7 @@ describe Gitlab::Utils::Override do
context 'when subject is a module, and class is prepending it' do
subject { extension }
+
let(:klass) { prepending_class }
it_behaves_like 'nothing happened'
@@ -184,6 +187,7 @@ describe Gitlab::Utils::Override do
context 'when subject is a module, and class is including it' do
subject { extension }
+
let(:klass) { including_class }
it 'does not complain when it is overriding something' do
@@ -215,6 +219,7 @@ describe Gitlab::Utils::Override do
context 'when subject is a module, and class is prepending it' do
subject { extension }
+
let(:klass) { prepending_class_methods }
it_behaves_like 'checking as intended'
@@ -222,6 +227,7 @@ describe Gitlab::Utils::Override do
context 'when subject is a module, and class is extending it' do
subject { extension }
+
let(:klass) { extending_class_methods }
it_behaves_like 'checking as intended, nothing was overridden'
diff --git a/spec/lib/gitlab/utils/safe_inline_hash_spec.rb b/spec/lib/gitlab/utils/safe_inline_hash_spec.rb
new file mode 100644
index 00000000000..617845332bc
--- /dev/null
+++ b/spec/lib/gitlab/utils/safe_inline_hash_spec.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+
+describe Gitlab::Utils::SafeInlineHash do
+ describe '.merge_keys!' do
+ let(:source) { { 'foo' => { 'bar' => 'baz' } } }
+ let(:validator) { instance_double(Gitlab::Utils::DeepSize, valid?: valid) }
+
+ subject { described_class.merge_keys!(source, prefix: 'safe', connector: '::') }
+
+ before do
+ allow(Gitlab::Utils::DeepSize)
+ .to receive(:new)
+ .with(source)
+ .and_return(validator)
+ end
+
+ context 'when hash is too big' do
+ let(:valid) { false }
+
+ it 'raises an exception' do
+ expect { subject }.to raise_error ArgumentError, 'The Hash is too big'
+ end
+ end
+
+ context 'when hash has an acceptaable size' do
+ let(:valid) { true }
+
+ it 'returns a result of InlineHash' do
+ is_expected.to eq('safe::foo::bar' => 'baz')
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/utils/sanitize_node_link_spec.rb b/spec/lib/gitlab/utils/sanitize_node_link_spec.rb
index 80b0935a7ed..dd379f2fe1f 100644
--- a/spec/lib/gitlab/utils/sanitize_node_link_spec.rb
+++ b/spec/lib/gitlab/utils/sanitize_node_link_spec.rb
@@ -43,6 +43,11 @@ describe Gitlab::Utils::SanitizeNodeLink do
doc: HTML::Pipeline.parse("<video><source src='#{scheme}alert(1);'></video>"),
attr: "src",
node_to_check: -> (doc) { doc.children.first.children.filter("source").first }
+ },
+ audio: {
+ doc: HTML::Pipeline.parse("<audio><source src='#{scheme}alert(1);'></audio>"),
+ attr: "src",
+ node_to_check: -> (doc) { doc.children.first.children.filter("source").first }
}
}
diff --git a/spec/lib/gitlab_spec.rb b/spec/lib/gitlab_spec.rb
index ccb5cb3aa43..6bf837f1d3f 100644
--- a/spec/lib/gitlab_spec.rb
+++ b/spec/lib/gitlab_spec.rb
@@ -146,6 +146,7 @@ describe Gitlab do
describe '.ee?' do
before do
+ stub_env('FOSS_ONLY', nil) # Make sure the ENV is clean
described_class.instance_variable_set(:@is_ee, nil)
end
@@ -153,42 +154,66 @@ describe Gitlab do
described_class.instance_variable_set(:@is_ee, nil)
end
- it 'returns true when using Enterprise Edition' do
- root = Pathname.new('dummy')
- license_path = double(:path, exist?: true)
+ context 'for EE' do
+ before do
+ root = Pathname.new('dummy')
+ license_path = double(:path, exist?: true)
- allow(described_class)
- .to receive(:root)
- .and_return(root)
+ allow(described_class)
+ .to receive(:root)
+ .and_return(root)
- allow(root)
- .to receive(:join)
- .with('ee/app/models/license.rb')
- .and_return(license_path)
+ allow(root)
+ .to receive(:join)
+ .with('ee/app/models/license.rb')
+ .and_return(license_path)
+ end
- expect(described_class.ee?).to eq(true)
- end
+ context 'when using FOSS_ONLY=1' do
+ before do
+ stub_env('FOSS_ONLY', '1')
+ end
- it 'returns false when using Community Edition' do
- root = double(:path)
- license_path = double(:path, exists?: false)
+ it 'returns not to be EE' do
+ expect(described_class).not_to be_ee
+ end
+ end
- allow(described_class)
- .to receive(:root)
- .and_return(Pathname.new('dummy'))
+ context 'when using FOSS_ONLY=0' do
+ before do
+ stub_env('FOSS_ONLY', '0')
+ end
- allow(root)
- .to receive(:join)
- .with('ee/app/models/license.rb')
- .and_return(license_path)
+ it 'returns to be EE' do
+ expect(described_class).to be_ee
+ end
+ end
- expect(described_class.ee?).to eq(false)
+ context 'when using default FOSS_ONLY' do
+ it 'returns to be EE' do
+ expect(described_class).to be_ee
+ end
+ end
end
- it 'returns true when the IS_GITLAB_EE variable is not empty' do
- stub_env('IS_GITLAB_EE', '1')
+ context 'for CE' do
+ before do
+ root = double(:path)
+ license_path = double(:path, exists?: false)
- expect(described_class.ee?).to eq(true)
+ allow(described_class)
+ .to receive(:root)
+ .and_return(Pathname.new('dummy'))
+
+ allow(root)
+ .to receive(:join)
+ .with('ee/app/models/license.rb')
+ .and_return(license_path)
+ end
+
+ it 'returns not to be EE' do
+ expect(described_class).not_to be_ee
+ end
end
end
diff --git a/spec/lib/google_api/cloud_platform/client_spec.rb b/spec/lib/google_api/cloud_platform/client_spec.rb
index c24998d32f8..0f7f57095df 100644
--- a/spec/lib/google_api/cloud_platform/client_spec.rb
+++ b/spec/lib/google_api/cloud_platform/client_spec.rb
@@ -54,6 +54,7 @@ describe GoogleApi::CloudPlatform::Client do
describe '#projects_zones_clusters_get' do
subject { client.projects_zones_clusters_get(spy, spy, spy) }
+
let(:gke_cluster) { double }
before do
@@ -68,7 +69,7 @@ describe GoogleApi::CloudPlatform::Client do
describe '#projects_zones_clusters_create' do
subject do
client.projects_zones_clusters_create(
- project_id, zone, cluster_name, cluster_size, machine_type: machine_type, legacy_abac: legacy_abac)
+ project_id, zone, cluster_name, cluster_size, machine_type: machine_type, legacy_abac: legacy_abac, enable_addons: enable_addons)
end
let(:project_id) { 'project-123' }
@@ -77,39 +78,54 @@ describe GoogleApi::CloudPlatform::Client do
let(:cluster_size) { 1 }
let(:machine_type) { 'n1-standard-2' }
let(:legacy_abac) { true }
- let(:create_cluster_request_body) { double('Google::Apis::ContainerV1::CreateClusterRequest') }
+ let(:enable_addons) { [] }
+
+ let(:addons_config) do
+ enable_addons.each_with_object({}) do |addon, hash|
+ hash[addon] = { disabled: false }
+ end
+ end
+
+ let(:cluster_options) do
+ {
+ cluster: {
+ name: cluster_name,
+ initial_node_count: cluster_size,
+ node_config: {
+ machine_type: machine_type
+ },
+ master_auth: {
+ username: 'admin',
+ client_certificate_config: {
+ issue_client_certificate: true
+ }
+ },
+ legacy_abac: {
+ enabled: legacy_abac
+ },
+ ip_allocation_policy: {
+ use_ip_aliases: true
+ },
+ addons_config: addons_config
+ }
+ }
+ end
+
+ let(:create_cluster_request_body) { double('Google::Apis::ContainerV1beta1::CreateClusterRequest') }
let(:operation) { double }
before do
- allow_any_instance_of(Google::Apis::ContainerV1::ContainerService)
+ allow_any_instance_of(Google::Apis::ContainerV1beta1::ContainerService)
.to receive(:create_cluster).with(any_args)
.and_return(operation)
end
it 'sets corresponded parameters' do
- expect_any_instance_of(Google::Apis::ContainerV1::ContainerService)
+ expect_any_instance_of(Google::Apis::ContainerV1beta1::ContainerService)
.to receive(:create_cluster).with(project_id, zone, create_cluster_request_body, options: user_agent_options)
- expect(Google::Apis::ContainerV1::CreateClusterRequest)
- .to receive(:new).with(
- {
- "cluster": {
- "name": cluster_name,
- "initial_node_count": cluster_size,
- "node_config": {
- "machine_type": machine_type
- },
- "master_auth": {
- "username": "admin",
- "client_certificate_config": {
- issue_client_certificate: true
- }
- },
- "legacy_abac": {
- "enabled": true
- }
- }
- } ).and_return(create_cluster_request_body)
+ expect(Google::Apis::ContainerV1beta1::CreateClusterRequest)
+ .to receive(:new).with(cluster_options).and_return(create_cluster_request_body)
expect(subject).to eq operation
end
@@ -118,29 +134,25 @@ describe GoogleApi::CloudPlatform::Client do
let(:legacy_abac) { false }
it 'sets corresponded parameters' do
- expect_any_instance_of(Google::Apis::ContainerV1::ContainerService)
+ expect_any_instance_of(Google::Apis::ContainerV1beta1::ContainerService)
+ .to receive(:create_cluster).with(project_id, zone, create_cluster_request_body, options: user_agent_options)
+
+ expect(Google::Apis::ContainerV1beta1::CreateClusterRequest)
+ .to receive(:new).with(cluster_options).and_return(create_cluster_request_body)
+
+ expect(subject).to eq operation
+ end
+ end
+
+ context 'create with enable_addons for cloud_run' do
+ let(:enable_addons) { [:http_load_balancing, :istio_config, :cloud_run_config] }
+
+ it 'sets corresponded parameters' do
+ expect_any_instance_of(Google::Apis::ContainerV1beta1::ContainerService)
.to receive(:create_cluster).with(project_id, zone, create_cluster_request_body, options: user_agent_options)
- expect(Google::Apis::ContainerV1::CreateClusterRequest)
- .to receive(:new).with(
- {
- "cluster": {
- "name": cluster_name,
- "initial_node_count": cluster_size,
- "node_config": {
- "machine_type": machine_type
- },
- "master_auth": {
- "username": "admin",
- "client_certificate_config": {
- issue_client_certificate: true
- }
- },
- "legacy_abac": {
- "enabled": false
- }
- }
- } ).and_return(create_cluster_request_body)
+ expect(Google::Apis::ContainerV1beta1::CreateClusterRequest)
+ .to receive(:new).with(cluster_options).and_return(create_cluster_request_body)
expect(subject).to eq operation
end
@@ -149,6 +161,7 @@ describe GoogleApi::CloudPlatform::Client do
describe '#projects_zones_operations' do
subject { client.projects_zones_operations(spy, spy, spy) }
+
let(:operation) { double }
before do
diff --git a/spec/lib/grafana/client_spec.rb b/spec/lib/grafana/client_spec.rb
new file mode 100644
index 00000000000..bd93a3c59a2
--- /dev/null
+++ b/spec/lib/grafana/client_spec.rb
@@ -0,0 +1,107 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Grafana::Client do
+ let(:grafana_url) { 'https://grafanatest.com/-/grafana-project' }
+ let(:token) { 'test-token' }
+
+ subject(:client) { described_class.new(api_url: grafana_url, token: token) }
+
+ shared_examples 'calls grafana api' do
+ let!(:grafana_api_request) { stub_grafana_request(grafana_api_url) }
+
+ it 'calls grafana api' do
+ subject
+
+ expect(grafana_api_request).to have_been_requested
+ end
+ end
+
+ shared_examples 'no redirects' do
+ let(:redirect_to) { 'https://redirected.example.com' }
+ let(:other_url) { 'https://grafana.example.org' }
+
+ let!(:redirected_req_stub) { stub_grafana_request(other_url) }
+
+ let!(:redirect_req_stub) do
+ stub_grafana_request(
+ grafana_api_url,
+ status: 302,
+ headers: { location: redirect_to }
+ )
+ end
+
+ it 'does not follow redirects' do
+ expect { subject }.to raise_exception(
+ Grafana::Client::Error,
+ 'Grafana response status code: 302'
+ )
+
+ expect(redirect_req_stub).to have_been_requested
+ expect(redirected_req_stub).not_to have_been_requested
+ end
+ end
+
+ shared_examples 'handles exceptions' do
+ exceptions = {
+ Gitlab::HTTP::Error => 'Error when connecting to Grafana',
+ Net::OpenTimeout => 'Connection to Grafana timed out',
+ SocketError => 'Received SocketError when trying to connect to Grafana',
+ OpenSSL::SSL::SSLError => 'Grafana returned invalid SSL data',
+ Errno::ECONNREFUSED => 'Connection refused',
+ StandardError => 'Grafana request failed due to StandardError'
+ }
+
+ exceptions.each do |exception, message|
+ context "#{exception}" do
+ before do
+ stub_request(:get, grafana_api_url).to_raise(exception)
+ end
+
+ it do
+ expect { subject }
+ .to raise_exception(Grafana::Client::Error, message)
+ end
+ end
+ end
+ end
+
+ describe '#proxy_datasource' do
+ let(:grafana_api_url) do
+ 'https://grafanatest.com/-/grafana-project/' \
+ 'api/datasources/proxy/' \
+ '1/api/v1/query_range' \
+ '?query=rate(relevant_metric)' \
+ '&start=1570441248&end=1570444848&step=900'
+ end
+
+ subject do
+ client.proxy_datasource(
+ datasource_id: '1',
+ proxy_path: 'api/v1/query_range',
+ query: {
+ query: 'rate(relevant_metric)',
+ start: 1570441248,
+ end: 1570444848,
+ step: 900
+ }
+ )
+ end
+
+ it_behaves_like 'calls grafana api'
+ it_behaves_like 'no redirects'
+ it_behaves_like 'handles exceptions'
+ end
+
+ private
+
+ def stub_grafana_request(url, body: {}, status: 200, headers: {})
+ stub_request(:get, url)
+ .to_return(
+ status: status,
+ headers: { 'Content-Type' => 'application/json' }.merge(headers),
+ body: body.to_json
+ )
+ end
+end
diff --git a/spec/lib/json_web_token/token_spec.rb b/spec/lib/json_web_token/token_spec.rb
index 916d11ce0ed..ca587a6ebcd 100644
--- a/spec/lib/json_web_token/token_spec.rb
+++ b/spec/lib/json_web_token/token_spec.rb
@@ -16,6 +16,7 @@ describe JSONWebToken::Token do
context 'embeds default payload' do
subject { token.payload }
+
let(:default) { token.send(:default_payload) }
it { is_expected.to include(default) }
diff --git a/spec/lib/omni_auth/strategies/jwt_spec.rb b/spec/lib/omni_auth/strategies/jwt_spec.rb
index bdf3ea6be98..a8c565aa705 100644
--- a/spec/lib/omni_auth/strategies/jwt_spec.rb
+++ b/spec/lib/omni_auth/strategies/jwt_spec.rb
@@ -8,6 +8,7 @@ describe OmniAuth::Strategies::Jwt do
context '#decoded' do
subject { described_class.new({}) }
+
let(:timestamp) { Time.now.to_i }
let(:jwt_config) { Devise.omniauth_configs[:jwt] }
let(:claims) do
diff --git a/spec/lib/quality/test_level_spec.rb b/spec/lib/quality/test_level_spec.rb
index 59870ce44a7..4db188bd8f2 100644
--- a/spec/lib/quality/test_level_spec.rb
+++ b/spec/lib/quality/test_level_spec.rb
@@ -4,6 +4,20 @@ require 'fast_spec_helper'
RSpec.describe Quality::TestLevel do
describe '#pattern' do
+ context 'when level is all' do
+ it 'returns a pattern' do
+ expect(subject.pattern(:all))
+ .to eq("spec/**{,/**/}*_spec.rb")
+ end
+ end
+
+ context 'when level is geo' do
+ it 'returns a pattern' do
+ expect(subject.pattern(:geo))
+ .to eq("spec/**{,/**/}*_spec.rb")
+ end
+ end
+
context 'when level is unit' do
it 'returns a pattern' do
expect(subject.pattern(:unit))
@@ -44,6 +58,20 @@ RSpec.describe Quality::TestLevel do
end
describe '#regexp' do
+ context 'when level is all' do
+ it 'returns a regexp' do
+ expect(subject.regexp(:all))
+ .to eq(%r{spec/})
+ end
+ end
+
+ context 'when level is geo' do
+ it 'returns a regexp' do
+ expect(subject.regexp(:geo))
+ .to eq(%r{spec/})
+ end
+ end
+
context 'when level is unit' do
it 'returns a regexp' do
expect(subject.regexp(:unit))
diff --git a/spec/lib/uploaded_file_spec.rb b/spec/lib/uploaded_file_spec.rb
index 2cb4727bd4b..2bbbd67b13c 100644
--- a/spec/lib/uploaded_file_spec.rb
+++ b/spec/lib/uploaded_file_spec.rb
@@ -72,16 +72,6 @@ describe UploadedFile do
end
end
- context 'when only remote id is specified' do
- let(:params) do
- { 'file.remote_id' => 'remote_id' }
- end
-
- it "raises an error" do
- expect { subject }.to raise_error(UploadedFile::InvalidPathError, /file is invalid/)
- end
- end
-
context 'when verifying allowed paths' do
let(:params) do
{ 'file.path' => temp_file.path }
@@ -120,6 +110,52 @@ describe UploadedFile do
end
end
+ describe '.initialize' do
+ context 'when no size is provided' do
+ it 'determine size from local path' do
+ file = described_class.new(temp_file.path)
+
+ expect(file.size).to eq(temp_file.size)
+ end
+
+ it 'raises an exception if is a remote file' do
+ expect do
+ described_class.new(nil, remote_id: 'id')
+ end.to raise_error(UploadedFile::UnknownSizeError, 'Unable to determine file size')
+ end
+ end
+
+ context 'when size is a number' do
+ let_it_be(:size) { 1.gigabyte }
+
+ it 'is overridden by the size of the local file' do
+ file = described_class.new(temp_file.path, size: size)
+
+ expect(file.size).to eq(temp_file.size)
+ end
+
+ it 'is respected if is a remote file' do
+ file = described_class.new(nil, remote_id: 'id', size: size)
+
+ expect(file.size).to eq(size)
+ end
+ end
+
+ context 'when size is a string' do
+ it 'is converted to a number' do
+ file = described_class.new(nil, remote_id: 'id', size: '1')
+
+ expect(file.size).to eq(1)
+ end
+
+ it 'raises an exception if does not represent a number' do
+ expect do
+ described_class.new(nil, remote_id: 'id', size: 'not a number')
+ end.to raise_error(UploadedFile::UnknownSizeError, 'Unable to determine file size')
+ end
+ end
+ end
+
describe '#sanitize_filename' do
it { expect(described_class.new(temp_file.path).sanitize_filename('spaced name')).to eq('spaced_name') }
it { expect(described_class.new(temp_file.path).sanitize_filename('#$%^&')).to eq('_____') }
diff --git a/spec/mailers/emails/pipelines_spec.rb b/spec/mailers/emails/pipelines_spec.rb
new file mode 100644
index 00000000000..8d4afe9f00f
--- /dev/null
+++ b/spec/mailers/emails/pipelines_spec.rb
@@ -0,0 +1,69 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require 'email_spec'
+
+describe Emails::Pipelines do
+ include EmailSpec::Matchers
+
+ set(:project) { create(:project, :repository) }
+
+ shared_examples_for 'correct pipeline information' do
+ it 'has a correct information' do
+ expect(subject)
+ .to have_subject "#{project.name} | Pipeline ##{pipeline.id} has " \
+ "#{status} for #{pipeline.source_ref} | " \
+ "#{pipeline.short_sha}".to_s
+
+ expect(subject).to have_body_text pipeline.source_ref
+ expect(subject).to have_body_text status_text
+ end
+
+ context 'when pipeline for merge requests' do
+ let(:pipeline) { merge_request.all_pipelines.first }
+
+ let(:merge_request) do
+ create(:merge_request, :with_detached_merge_request_pipeline,
+ source_project: project,
+ target_project: project)
+ end
+
+ it 'has a correct information with merge request link' do
+ expect(subject)
+ .to have_subject "#{project.name} | Pipeline ##{pipeline.id} has " \
+ "#{status} for #{pipeline.source_ref} | " \
+ "#{pipeline.short_sha} in !#{merge_request.iid}".to_s
+
+ expect(subject).to have_body_text merge_request.to_reference
+ expect(subject).to have_body_text pipeline.source_ref
+ expect(subject).not_to have_body_text pipeline.ref
+ end
+ end
+ end
+
+ describe '#pipeline_success_email' do
+ subject { Notify.pipeline_success_email(pipeline, pipeline.user.try(:email)) }
+
+ let(:pipeline) { create(:ci_pipeline, project: project, ref: ref, sha: sha) }
+ let(:ref) { 'master' }
+ let(:sha) { project.commit(ref).sha }
+
+ it_behaves_like 'correct pipeline information' do
+ let(:status) { 'succeeded' }
+ let(:status_text) { 'Your pipeline has passed.' }
+ end
+ end
+
+ describe '#pipeline_failed_email' do
+ subject { Notify.pipeline_failed_email(pipeline, pipeline.user.try(:email)) }
+
+ let(:pipeline) { create(:ci_pipeline, project: project, ref: ref, sha: sha) }
+ let(:ref) { 'master' }
+ let(:sha) { project.commit(ref).sha }
+
+ it_behaves_like 'correct pipeline information' do
+ let(:status) { 'failed' }
+ let(:status_text) { 'Your pipeline has failed.' }
+ end
+ end
+end
diff --git a/spec/mailers/emails/releases_spec.rb b/spec/mailers/emails/releases_spec.rb
new file mode 100644
index 00000000000..19f404db2a6
--- /dev/null
+++ b/spec/mailers/emails/releases_spec.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require 'email_spec'
+
+describe Emails::Releases do
+ include EmailSpec::Matchers
+ include_context 'gitlab email notification'
+
+ describe '#new_release_email' do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project) }
+ let(:release) { create(:release, project: project) }
+
+ subject { Notify.new_release_email(user.id, release) }
+
+ it_behaves_like 'an email sent from GitLab'
+
+ context 'when the release has a name' do
+ it 'shows the correct subject' do
+ expected_subject = "#{release.project.name} | New release: #{release.name} - #{release.tag}"
+ is_expected.to have_subject(expected_subject)
+ end
+ end
+
+ context 'when the release does not have a name' do
+ it 'shows the correct subject' do
+ release.name = nil
+ expected_subject = "#{release.project.name} | New release: #{release.tag}"
+
+ is_expected.to have_subject(expected_subject)
+ end
+ end
+
+ it 'contains a message with the new release tag' do
+ message = "A new Release #{release.tag} for #{release.project.name} was published."
+ is_expected.to have_body_text(message)
+ end
+
+ it 'contains the release assets' do
+ is_expected.to have_body_text('Assets:')
+ release.sources do |source|
+ is_expected.to have_body_text("Download #{source.format}")
+ end
+ end
+
+ it 'contains the release notes' do
+ is_expected.to have_body_text('Release notes:')
+ is_expected.to have_body_text(release.description)
+ end
+ end
+end
diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb
index 56fa26d5f23..1991bac0229 100644
--- a/spec/mailers/notify_spec.rb
+++ b/spec/mailers/notify_spec.rb
@@ -714,7 +714,7 @@ describe Notify do
describe 'project access requested' do
let(:project) do
- create(:project, :public, :access_requestable) do |project|
+ create(:project, :public) do |project|
project.add_maintainer(project.owner)
end
end
@@ -743,7 +743,7 @@ describe Notify do
end
describe 'project access denied' do
- let(:project) { create(:project, :public, :access_requestable) }
+ let(:project) { create(:project, :public) }
let(:project_member) do
project.request_access(user)
project.requesters.find_by(user_id: user.id)
@@ -765,7 +765,7 @@ describe Notify do
describe 'project access changed' do
let(:owner) { create(:user, name: "Chang O'Keefe") }
- let(:project) { create(:project, :public, :access_requestable, namespace: owner.namespace) }
+ let(:project) { create(:project, :public, namespace: owner.namespace) }
let(:project_member) { create(:project_member, project: project, user: user) }
subject { described_class.member_access_granted_email('project', project_member.id) }
@@ -1167,7 +1167,7 @@ describe Notify do
context 'for a group' do
describe 'group access requested' do
- let(:group) { create(:group, :public, :access_requestable) }
+ let(:group) { create(:group, :public) }
let(:group_member) do
group.request_access(user)
group.requesters.find_by(user_id: user.id)
diff --git a/spec/migrations/README.md b/spec/migrations/README.md
index 4d86d30080a..5065df3fbef 100644
--- a/spec/migrations/README.md
+++ b/spec/migrations/README.md
@@ -1,137 +1 @@
-# Testing migrations
-
-In order to reliably test a migration, we need to test it against a database
-schema that this migration has been written for. In order to achieve that we
-have some _migration helpers_ and RSpec test tag, called `:migration`.
-
-If you want to write a test for a migration consider adding `:migration` tag to
-the test signature, like `describe SomeMigrationClass, :migration`.
-
-## How does it work?
-
-Adding a `:migration` tag to a test signature injects a few before / after
-hooks to the test.
-
-The most important change is that adding a `:migration` tag adds a `before`
-hook that will revert all migrations to the point that a migration under test
-is not yet migrated.
-
-In other words, our custom RSpec hooks will find a previous migration, and
-migrate the database **down** to the previous migration version.
-
-With this approach you can test a migration against a database schema that this
-migration has been written for.
-
-The `after` hook will migrate the database **up** and reinstitutes the latest
-schema version, so that the process does not affect subsequent specs and
-ensures proper isolation.
-
-## Available helpers
-
-Use `table` helper to create a temporary `ActiveRecord::Base` derived model
-for a table.
-
-See `spec/support/helpers/migrations_helpers.rb` for all the available helpers.
-
-## Testing a class that is an ActiveRecord::Migration
-
-In order to test a class that is an `ActiveRecord::Migration`, you will need to
-manually `require` the migration file because it is not autoloaded with Rails.
-
-Use `migrate!` helper to run the migration that is under test. It will not only
-run migration, but will also bump the schema version in the `schema_migrations`
-table. It is necessary because in the `after` hook we trigger the rest of
-the migrations, and we need to know where to start.
-
-### Example
-
-This spec tests the [`db/post_migrate/20170526185842_migrate_pipeline_stages.rb`](https://gitlab.com/gitlab-org/gitlab-foss/blob/v11.6.5/db/post_migrate/20170526185842_migrate_pipeline_stages.rb) migration. You can find the complete spec on [`spec/migrations/migrate_pipeline_stages_spec.rb`](https://gitlab.com/gitlab-org/gitlab-foss/blob/v11.6.5/spec/migrations/migrate_pipeline_stages_spec.rb).
-
-```ruby
-require 'spec_helper'
-require Rails.root.join('db', 'post_migrate', '20170526185842_migrate_pipeline_stages.rb')
-
-describe MigratePipelineStages, :migration do
-
- # Create test data - pipeline and CI/CD jobs.
-
- let(:jobs) { table(:ci_builds) }
- let(:stages) { table(:ci_stages) }
- let(:pipelines) { table(:ci_pipelines) }
- let(:projects) { table(:projects) }
-
- before do
- projects.create!(id: 123, name: 'gitlab1', path: 'gitlab1')
- pipelines.create!(id: 1, project_id: 123, ref: 'master', sha: 'adf43c3a')
- jobs.create!(id: 1, commit_id: 1, project_id: 123, stage_idx: 2, stage: 'build')
- jobs.create!(id: 2, commit_id: 1, project_id: 123, stage_idx: 1, stage: 'test')
- end
-
- # Test the migration.
-
- it 'correctly migrates pipeline stages' do
- expect(stages.count).to be_zero
-
- migrate!
-
- expect(stages.count).to eq 2
- expect(stages.all.pluck(:name)).to match_array %w[test build]
- end
-end
-```
-
-## Testing a class that is not an ActiveRecord::Migration
-
-To test a class that is not an `ActiveRecord::Migration` (a background migration),
-you will need to manually provide a required schema version. Please add a
-schema tag to a context that you want to switch the database schema within.
-
-Example: `describe SomeClass, :migration, schema: 20170608152748`.
-
-### Example
-
-This spec tests the [`lib/gitlab/background_migration/archive_legacy_traces.rb`](https://gitlab.com/gitlab-org/gitlab-foss/blob/v11.6.5/lib/gitlab/background_migration/archive_legacy_traces.rb)
-background migration. You can find the complete spec on
-[`spec/lib/gitlab/background_migration/archive_legacy_traces_spec.rb`](https://gitlab.com/gitlab-org/gitlab-foss/blob/v11.6.5/spec/lib/gitlab/background_migration/archive_legacy_traces_spec.rb)
-
-```ruby
-require 'spec_helper'
-
-describe Gitlab::BackgroundMigration::ArchiveLegacyTraces, :migration, schema: 20180529152628 do
- include TraceHelpers
-
- let(:namespaces) { table(:namespaces) }
- let(:projects) { table(:projects) }
- let(:builds) { table(:ci_builds) }
- let(:job_artifacts) { table(:ci_job_artifacts) }
-
- before do
- namespaces.create!(id: 123, name: 'gitlab1', path: 'gitlab1')
- projects.create!(id: 123, name: 'gitlab1', path: 'gitlab1', namespace_id: 123)
- @build = builds.create!(id: 1, project_id: 123, status: 'success', type: 'Ci::Build')
- end
-
- context 'when trace file exsits at the right place' do
- before do
- create_legacy_trace(@build, 'trace in file')
- end
-
- it 'correctly archive legacy traces' do
- expect(job_artifacts.count).to eq(0)
- expect(File.exist?(legacy_trace_path(@build))).to be_truthy
-
- described_class.new.perform(1, 1)
-
- expect(job_artifacts.count).to eq(1)
- expect(File.exist?(legacy_trace_path(@build))).to be_falsy
- expect(File.read(archived_trace_path(job_artifacts.first))).to eq('trace in file')
- end
- end
-end
-```
-
-## Best practices
-
-1. Note that this type of tests do not run within the transaction, we use
-a deletion database cleanup strategy. Do not depend on transaction being
-present.
+This document was moved to [another location](https://docs.gitlab.com/ee/development/testing_guide/testing_migrations_guide.html).
diff --git a/spec/migrations/backfill_releases_table_updated_at_and_add_not_null_constraints_to_timestamps_spec.rb b/spec/migrations/backfill_releases_table_updated_at_and_add_not_null_constraints_to_timestamps_spec.rb
new file mode 100644
index 00000000000..3ca7af8ea37
--- /dev/null
+++ b/spec/migrations/backfill_releases_table_updated_at_and_add_not_null_constraints_to_timestamps_spec.rb
@@ -0,0 +1,57 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require Rails.root.join('db', 'migrate', '20190920194925_backfill_releases_table_updated_at_and_add_not_null_constraints_to_timestamps.rb')
+
+describe BackfillReleasesTableUpdatedAtAndAddNotNullConstraintsToTimestamps, :migration do
+ let(:releases) { table(:releases) }
+ let(:namespaces) { table(:namespaces) }
+ let(:projects) { table(:projects) }
+
+ subject(:migration) { described_class.new }
+
+ it 'fills null updated_at rows with the value of created_at' do
+ created_at_a = Time.zone.parse('2014-03-11T04:30:00Z')
+ created_at_b = Time.zone.parse('2019-09-10T12:00:00Z')
+ namespace = namespaces.create(name: 'foo', path: 'foo')
+ project = projects.create!(namespace_id: namespace.id)
+ release_a = releases.create!(project_id: project.id,
+ released_at: Time.zone.parse('2014-12-10T06:00:00Z'),
+ created_at: created_at_a)
+ release_b = releases.create!(project_id: project.id,
+ released_at: Time.zone.parse('2019-09-11T06:00:00Z'),
+ created_at: created_at_b)
+ release_a.update!(updated_at: nil)
+ release_b.update!(updated_at: nil)
+
+ disable_migrations_output { migrate! }
+
+ release_a.reload
+ release_b.reload
+ expect(release_a.updated_at).to eq(created_at_a)
+ expect(release_b.updated_at).to eq(created_at_b)
+ end
+
+ it 'does not change updated_at columns with a value' do
+ created_at_a = Time.zone.parse('2014-03-11T04:30:00Z')
+ updated_at_a = Time.zone.parse('2015-01-16T10:00:00Z')
+ created_at_b = Time.zone.parse('2019-09-10T12:00:00Z')
+ namespace = namespaces.create(name: 'foo', path: 'foo')
+ project = projects.create!(namespace_id: namespace.id)
+ release_a = releases.create!(project_id: project.id,
+ released_at: Time.zone.parse('2014-12-10T06:00:00Z'),
+ created_at: created_at_a,
+ updated_at: updated_at_a)
+ release_b = releases.create!(project_id: project.id,
+ released_at: Time.zone.parse('2019-09-11T06:00:00Z'),
+ created_at: created_at_b)
+ release_b.update!(updated_at: nil)
+
+ disable_migrations_output { migrate! }
+
+ release_a.reload
+ release_b.reload
+ expect(release_a.updated_at).to eq(updated_at_a)
+ expect(release_b.updated_at).to eq(created_at_b)
+ end
+end
diff --git a/spec/migrations/migrate_code_owner_approval_status_to_protected_branches_in_batches_spec.rb b/spec/migrations/migrate_code_owner_approval_status_to_protected_branches_in_batches_spec.rb
new file mode 100644
index 00000000000..67ac40d4d39
--- /dev/null
+++ b/spec/migrations/migrate_code_owner_approval_status_to_protected_branches_in_batches_spec.rb
@@ -0,0 +1,63 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require Rails.root.join('db', 'post_migrate', '20190827102026_migrate_code_owner_approval_status_to_protected_branches_in_batches.rb')
+
+describe MigrateCodeOwnerApprovalStatusToProtectedBranchesInBatches, :migration do
+ let(:namespaces) { table(:namespaces) }
+ let(:projects) { table(:projects) }
+ let(:protected_branches) { table(:protected_branches) }
+
+ let(:namespace) do
+ namespaces.create!(
+ path: 'gitlab-instance-administrators',
+ name: 'GitLab Instance Administrators'
+ )
+ end
+
+ let(:project) do
+ projects.create!(
+ namespace_id: namespace.id,
+ name: 'GitLab Instance Administration'
+ )
+ end
+
+ let!(:protected_branch_1) do
+ protected_branches.create!(
+ name: "branch name",
+ project_id: project.id
+ )
+ end
+
+ describe '#up' do
+ context "when there's no projects needing approval" do
+ it "doesn't change any protected branch records" do
+ expect { migrate! }
+ .not_to change { ProtectedBranch.where(code_owner_approval_required: true).count }
+ end
+ end
+
+ context "when there's a project needing approval" do
+ let!(:project_needing_approval) do
+ projects.create!(
+ namespace_id: namespace.id,
+ name: 'GitLab Instance Administration',
+ merge_requests_require_code_owner_approval: true
+ )
+ end
+
+ let!(:protected_branch_2) do
+ protected_branches.create!(
+ name: "branch name",
+ project_id: project_needing_approval.id
+ )
+ end
+
+ it "changes N protected branch records" do
+ expect { migrate! }
+ .to change { ProtectedBranch.where(code_owner_approval_required: true).count }
+ .by(1)
+ end
+ end
+ end
+end
diff --git a/spec/migrations/schedule_pages_metadata_migration_spec.rb b/spec/migrations/schedule_pages_metadata_migration_spec.rb
new file mode 100644
index 00000000000..100ed520a32
--- /dev/null
+++ b/spec/migrations/schedule_pages_metadata_migration_spec.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require Rails.root.join('db', 'post_migrate', '20191002031332_schedule_pages_metadata_migration')
+
+describe SchedulePagesMetadataMigration, :migration, :sidekiq do
+ let(:namespaces) { table(:namespaces) }
+ let(:projects) { table(:projects) }
+
+ before do
+ stub_const("#{described_class.name}::BATCH_SIZE", 1)
+
+ namespaces.create!(id: 11, name: 'gitlab', path: 'gitlab-org')
+ projects.create!(id: 111, namespace_id: 11, name: 'Project 111')
+ projects.create!(id: 114, namespace_id: 11, name: 'Project 114')
+ end
+
+ it 'schedules pages metadata migration' do
+ Sidekiq::Testing.fake! do
+ Timecop.freeze do
+ migrate!
+
+ expect(described_class::MIGRATION).to be_scheduled_delayed_migration(2.minutes, 111, 111)
+ expect(described_class::MIGRATION).to be_scheduled_delayed_migration(4.minutes, 114, 114)
+ expect(BackgroundMigrationWorker.jobs.size).to eq(2)
+ end
+ end
+ end
+end
diff --git a/spec/migrations/sync_issuables_state_id_spec.rb b/spec/migrations/sync_issuables_state_id_spec.rb
new file mode 100644
index 00000000000..8d1f8a36ac3
--- /dev/null
+++ b/spec/migrations/sync_issuables_state_id_spec.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require Rails.root.join('db', 'post_migrate', '20190911251732_sync_issuables_state_id')
+
+describe SyncIssuablesStateId, :migration, :sidekiq do
+ let(:migration) { described_class.new }
+
+ describe '#up' do
+ let(:issues) { table(:issues) }
+ let(:namespaces) { table(:namespaces) }
+ let(:projects) { table(:projects) }
+ let(:merge_requests) { table(:merge_requests) }
+ let(:group) { namespaces.create!(name: 'gitlab', path: 'gitlab') }
+ let(:project) { projects.create!(namespace_id: group.id) }
+ # These state_ids should be the same defined on Issue/MergeRequest models
+ let(:state_ids) { { opened: 1, closed: 2, merged: 3, locked: 4 } }
+
+ it 'migrates state column to state_id as integer' do
+ opened_issue = issues.create!(description: 'first', state: 'opened')
+ closed_issue = issues.create!(description: 'second', state: 'closed')
+ opened_merge_request = merge_requests.create!(state: 'opened', target_project_id: project.id, target_branch: 'feature1', source_branch: 'master')
+ closed_merge_request = merge_requests.create!(state: 'closed', target_project_id: project.id, target_branch: 'feature2', source_branch: 'master')
+ merged_merge_request = merge_requests.create!(state: 'merged', target_project_id: project.id, target_branch: 'feature3', source_branch: 'master')
+ locked_merge_request = merge_requests.create!(state: 'locked', target_project_id: project.id, target_branch: 'feature4', source_branch: 'master')
+
+ migrate!
+
+ expect(opened_issue.reload.state_id).to eq(state_ids[:opened])
+ expect(closed_issue.reload.state_id).to eq(state_ids[:closed])
+ expect(opened_merge_request.reload.state_id).to eq(state_ids[:opened])
+ expect(closed_merge_request.reload.state_id).to eq(state_ids[:closed])
+ expect(merged_merge_request.reload.state_id).to eq(state_ids[:merged])
+ expect(locked_merge_request.reload.state_id).to eq(state_ids[:locked])
+ end
+ end
+end
diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb
index d12f9b9100a..7bef3d30064 100644
--- a/spec/models/application_setting_spec.rb
+++ b/spec/models/application_setting_spec.rb
@@ -51,6 +51,18 @@ describe ApplicationSetting do
it { is_expected.to allow_value(nil).for(:static_objects_external_storage_url) }
it { is_expected.to allow_value(http).for(:static_objects_external_storage_url) }
it { is_expected.to allow_value(https).for(:static_objects_external_storage_url) }
+ it { is_expected.to allow_value(['/example'] * 100).for(:protected_paths) }
+ it { is_expected.not_to allow_value(['/example'] * 101).for(:protected_paths) }
+ it { is_expected.not_to allow_value(nil).for(:protected_paths) }
+ it { is_expected.to allow_value([]).for(:protected_paths) }
+
+ it { is_expected.to allow_value(3).for(:push_event_hooks_limit) }
+ it { is_expected.not_to allow_value('three').for(:push_event_hooks_limit) }
+ it { is_expected.not_to allow_value(nil).for(:push_event_hooks_limit) }
+
+ it { is_expected.to allow_value(3).for(:push_event_activities_limit) }
+ it { is_expected.not_to allow_value('three').for(:push_event_activities_limit) }
+ it { is_expected.not_to allow_value(nil).for(:push_event_activities_limit) }
context "when user accepted let's encrypt terms of service" do
before do
diff --git a/spec/models/aws/role_spec.rb b/spec/models/aws/role_spec.rb
new file mode 100644
index 00000000000..c40752e40a6
--- /dev/null
+++ b/spec/models/aws/role_spec.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Aws::Role do
+ it { is_expected.to belong_to(:user) }
+ it { is_expected.to validate_length_of(:role_external_id).is_at_least(1).is_at_most(64) }
+
+ describe 'custom validations' do
+ subject { role.valid? }
+
+ context ':role_arn' do
+ let(:role) { build(:aws_role, role_arn: role_arn) }
+
+ context 'length is zero' do
+ let(:role_arn) { '' }
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'length is longer than 2048' do
+ let(:role_arn) { '1' * 2049 }
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'ARN is valid' do
+ let(:role_arn) { 'arn:aws:iam::123456789012:role/test-role' }
+
+ it { is_expected.to be_truthy }
+ end
+ end
+ end
+end
diff --git a/spec/models/blob_spec.rb b/spec/models/blob_spec.rb
index 2efab3076d8..9e55fbcce20 100644
--- a/spec/models/blob_spec.rb
+++ b/spec/models/blob_spec.rb
@@ -320,6 +320,22 @@ describe Blob do
expect(blob.rich_viewer).to be_a(BlobViewer::Markup)
end
end
+
+ context 'when the blob is video' do
+ it 'returns a video viewer' do
+ blob = fake_blob(path: 'file.mp4', binary: true)
+
+ expect(blob.rich_viewer).to be_a(BlobViewer::Video)
+ end
+ end
+
+ context 'when the blob is audio' do
+ it 'returns an audio viewer' do
+ blob = fake_blob(path: 'file.wav', binary: true)
+
+ expect(blob.rich_viewer).to be_a(BlobViewer::Audio)
+ end
+ end
end
describe '#auxiliary_viewer' do
diff --git a/spec/models/ci/build_metadata_spec.rb b/spec/models/ci/build_metadata_spec.rb
index 67cd939b4c6..da95a2d30f5 100644
--- a/spec/models/ci/build_metadata_spec.rb
+++ b/spec/models/ci/build_metadata_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
describe Ci::BuildMetadata do
set(:user) { create(:user) }
- set(:group) { create(:group, :access_requestable) }
+ set(:group) { create(:group) }
set(:project) { create(:project, :repository, group: group, build_timeout: 2000) }
set(:pipeline) do
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
index 5c0f8bd392a..058305bc04e 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/ci/build_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
describe Ci::Build do
set(:user) { create(:user) }
- set(:group) { create(:group, :access_requestable) }
+ set(:group) { create(:group) }
set(:project) { create(:project, :repository, group: group) }
set(:pipeline) do
@@ -19,17 +19,24 @@ describe Ci::Build do
it { is_expected.to belong_to(:runner) }
it { is_expected.to belong_to(:trigger_request) }
it { is_expected.to belong_to(:erased_by) }
+
it { is_expected.to have_many(:trace_sections) }
it { is_expected.to have_many(:needs) }
+ it { is_expected.to have_many(:sourced_pipelines) }
+ it { is_expected.to have_many(:job_variables) }
+
it { is_expected.to have_one(:deployment) }
it { is_expected.to have_one(:runner_session) }
- it { is_expected.to have_many(:job_variables) }
+
it { is_expected.to validate_presence_of(:ref) }
+
it { is_expected.to respond_to(:has_trace?) }
it { is_expected.to respond_to(:trace) }
+
it { is_expected.to delegate_method(:merge_request_event?).to(:pipeline) }
it { is_expected.to delegate_method(:merge_request_ref?).to(:pipeline) }
it { is_expected.to delegate_method(:legacy_detached_merge_request_pipeline?).to(:pipeline) }
+
it { is_expected.to include_module(Ci::PipelineDelegator) }
describe 'associations' do
@@ -567,6 +574,7 @@ describe Ci::Build do
describe '#artifacts_metadata?' do
subject { build.artifacts_metadata? }
+
context 'artifacts metadata does not exist' do
it { is_expected.to be_falsy }
end
@@ -579,6 +587,7 @@ describe Ci::Build do
describe '#artifacts_expire_in' do
subject { build.artifacts_expire_in }
+
it { is_expected.to be_nil }
context 'when artifacts_expire_at is specified' do
@@ -957,7 +966,7 @@ describe Ci::Build do
end
describe 'state transition as a deployable' do
- let!(:build) { create(:ci_build, :start_review_app) }
+ let!(:build) { create(:ci_build, :with_deployment, :start_review_app) }
let(:deployment) { build.deployment }
let(:environment) { deployment.environment }
@@ -1043,20 +1052,6 @@ describe Ci::Build do
end
describe 'deployment' do
- describe '#has_deployment?' do
- subject { build.has_deployment? }
-
- context 'when build has a deployment' do
- let!(:deployment) { create(:deployment, deployable: build) }
-
- it { is_expected.to be_truthy }
- end
-
- context 'when build does not have a deployment' do
- it { is_expected.to be_falsy }
- end
- end
-
describe '#outdated_deployment?' do
subject { build.outdated_deployment? }
@@ -1272,6 +1267,7 @@ describe Ci::Build do
describe '#erasable?' do
subject { build.erasable? }
+
it { is_expected.to be_truthy }
end
@@ -1955,7 +1951,7 @@ describe Ci::Build do
end
context 'when build has a start environment' do
- let(:build) { create(:ci_build, :deploy_to_production, pipeline: pipeline) }
+ let(:build) { create(:ci_build, :with_deployment, :deploy_to_production, pipeline: pipeline) }
it 'does not expand environment name' do
expect(build).not_to receive(:expanded_environment_name)
@@ -2202,6 +2198,7 @@ describe Ci::Build do
{ key: 'CI_COMMIT_REF_NAME', value: build.ref, public: true, masked: false },
{ key: 'CI_COMMIT_REF_SLUG', value: build.ref_slug, public: true, masked: false },
{ key: 'CI_NODE_TOTAL', value: '1', public: true, masked: false },
+ { key: 'CI_DEFAULT_BRANCH', value: project.default_branch, public: true, masked: false },
{ key: 'CI_BUILD_REF', value: build.sha, public: true, masked: false },
{ key: 'CI_BUILD_BEFORE_SHA', value: build.before_sha, public: true, masked: false },
{ key: 'CI_BUILD_REF_NAME', value: build.ref, public: true, masked: false },
@@ -2210,6 +2207,7 @@ describe Ci::Build do
{ key: 'CI_BUILD_STAGE', value: 'test', public: true, masked: false },
{ key: 'CI_PROJECT_ID', value: project.id.to_s, public: true, masked: false },
{ key: 'CI_PROJECT_NAME', value: project.path, public: true, masked: false },
+ { key: 'CI_PROJECT_TITLE', value: project.title, public: true, masked: false },
{ key: 'CI_PROJECT_PATH', value: project.full_path, public: true, masked: false },
{ key: 'CI_PROJECT_PATH_SLUG', value: project.full_path_slug, public: true, masked: false },
{ key: 'CI_PROJECT_NAMESPACE', value: project.namespace.full_path, public: true, masked: false },
@@ -3079,6 +3077,12 @@ describe Ci::Build do
rescue StateMachines::InvalidTransition
end
+ it 'ensures pipeline ref existence' do
+ expect(job.pipeline.persistent_ref).to receive(:create).once
+
+ run_job_without_exception
+ end
+
shared_examples 'saves data on transition' do
it 'saves timeout' do
expect { job.run! }.to change { job.reload.ensure_metadata.timeout }.from(nil).to(expected_timeout)
@@ -3918,4 +3922,14 @@ describe Ci::Build do
end
end
end
+
+ describe '#invalid_dependencies' do
+ let!(:pre_stage_job_valid) { create(:ci_build, :manual, pipeline: pipeline, name: 'test1', stage_idx: 0) }
+ let!(:pre_stage_job_invalid) { create(:ci_build, :success, :expired, pipeline: pipeline, name: 'test2', stage_idx: 1) }
+ let!(:job) { create(:ci_build, :pending, pipeline: pipeline, stage_idx: 2, options: { dependencies: %w(test1 test2) }) }
+
+ it 'returns invalid dependencies' do
+ expect(job.invalid_dependencies).to eq([pre_stage_job_invalid])
+ end
+ end
end
diff --git a/spec/models/ci/build_trace_spec.rb b/spec/models/ci/build_trace_spec.rb
new file mode 100644
index 00000000000..2471a6fa827
--- /dev/null
+++ b/spec/models/ci/build_trace_spec.rb
@@ -0,0 +1,53 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Ci::BuildTrace do
+ let(:build) { build_stubbed(:ci_build) }
+ let(:state) { nil }
+ let(:data) { StringIO.new('the-stream') }
+
+ let(:stream) do
+ Gitlab::Ci::Trace::Stream.new { data }
+ end
+
+ subject { described_class.new(build: build, stream: stream, state: state, content_format: content_format) }
+
+ shared_examples 'delegates methods' do
+ it { is_expected.to delegate_method(:state).to(:trace) }
+ it { is_expected.to delegate_method(:append).to(:trace) }
+ it { is_expected.to delegate_method(:truncated).to(:trace) }
+ it { is_expected.to delegate_method(:offset).to(:trace) }
+ it { is_expected.to delegate_method(:size).to(:trace) }
+ it { is_expected.to delegate_method(:total).to(:trace) }
+ it { is_expected.to delegate_method(:id).to(:build).with_prefix }
+ it { is_expected.to delegate_method(:status).to(:build).with_prefix }
+ it { is_expected.to delegate_method(:complete?).to(:build).with_prefix }
+ end
+
+ context 'with :json content format' do
+ let(:content_format) { :json }
+
+ it_behaves_like 'delegates methods'
+
+ it { is_expected.to be_json }
+
+ it 'returns formatted trace' do
+ expect(subject.trace.lines).to eq([
+ { offset: 0, content: [{ text: 'the-stream' }] }
+ ])
+ end
+ end
+
+ context 'with :html content format' do
+ let(:content_format) { :html }
+
+ it_behaves_like 'delegates methods'
+
+ it { is_expected.to be_html }
+
+ it 'returns formatted trace' do
+ expect(subject.trace.html).to eq('<span>the-stream</span>')
+ end
+ end
+end
diff --git a/spec/models/ci/group_spec.rb b/spec/models/ci/group_spec.rb
index 36c65d92840..b3b158a111e 100644
--- a/spec/models/ci/group_spec.rb
+++ b/spec/models/ci/group_spec.rb
@@ -22,6 +22,32 @@ describe Ci::Group do
end
end
+ describe '#status' do
+ let(:jobs) do
+ [create(:ci_build, :failed)]
+ end
+
+ context 'when ci_composite_status is enabled' do
+ before do
+ stub_feature_flags(ci_composite_status: true)
+ end
+
+ it 'returns a failed status' do
+ expect(subject.status).to eq('failed')
+ end
+ end
+
+ context 'when ci_composite_status is disabled' do
+ before do
+ stub_feature_flags(ci_composite_status: false)
+ end
+
+ it 'returns a failed status' do
+ expect(subject.status).to eq('failed')
+ end
+ 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
diff --git a/spec/models/ci/legacy_stage_spec.rb b/spec/models/ci/legacy_stage_spec.rb
index bb812cc0533..477f4036218 100644
--- a/spec/models/ci/legacy_stage_spec.rb
+++ b/spec/models/ci/legacy_stage_spec.rb
@@ -216,7 +216,7 @@ describe Ci::LegacyStage do
context 'when stage has warnings' do
context 'when using memoized warnings flag' do
context 'when there are warnings' do
- let(:stage) { build(:ci_stage, warnings: 2) }
+ let(:stage) { build(:ci_stage, warnings: true) }
it 'returns true using memoized value' do
expect(stage).not_to receive(:statuses)
@@ -225,22 +225,13 @@ describe Ci::LegacyStage do
end
context 'when there are no warnings' do
- let(:stage) { build(:ci_stage, warnings: 0) }
+ let(:stage) { build(:ci_stage, warnings: false) }
it 'returns false using memoized value' do
expect(stage).not_to receive(:statuses)
expect(stage).not_to have_warnings
end
end
-
- context 'when number of warnings is not a valid value' do
- let(:stage) { build(:ci_stage, warnings: true) }
-
- it 'calculates statuses using database queries' do
- expect(stage).to receive(:statuses).and_call_original
- expect(stage).not_to have_warnings
- end
- end
end
context 'when calculating warnings from statuses' do
diff --git a/spec/models/ci/persistent_ref_spec.rb b/spec/models/ci/persistent_ref_spec.rb
new file mode 100644
index 00000000000..be447476e2c
--- /dev/null
+++ b/spec/models/ci/persistent_ref_spec.rb
@@ -0,0 +1,96 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Ci::PersistentRef do
+ it 'cleans up persistent refs after pipeline finished' do
+ pipeline = create(:ci_pipeline, :running)
+
+ expect(pipeline.persistent_ref).to receive(:delete).once
+
+ pipeline.succeed!
+ end
+
+ context '#exist?' do
+ subject { pipeline.persistent_ref.exist? }
+
+ let(:pipeline) { create(:ci_pipeline, sha: sha, project: project) }
+ let(:project) { create(:project, :repository) }
+ let(:sha) { project.repository.commit.sha }
+
+ context 'when a persistent ref does not exist' do
+ it { is_expected.to eq(false) }
+ end
+
+ context 'when a persistent ref exists' do
+ before do
+ pipeline.persistent_ref.create
+ end
+
+ it { is_expected.to eq(true) }
+ end
+ end
+
+ context '#create' do
+ subject { pipeline.persistent_ref.create }
+
+ let(:pipeline) { create(:ci_pipeline, sha: sha, project: project) }
+ let(:project) { create(:project, :repository) }
+ let(:sha) { project.repository.commit.sha }
+
+ context 'when a persistent ref does not exist' do
+ it 'creates a persistent ref' do
+ subject
+
+ expect(pipeline.persistent_ref).to be_exist
+ end
+
+ context 'when sha does not exist in the repository' do
+ let(:sha) { 'not-exist' }
+
+ it 'fails to create a persistent ref' do
+ subject
+
+ expect(pipeline.persistent_ref).not_to be_exist
+ end
+ end
+ end
+
+ context 'when a persistent ref already exists' do
+ before do
+ pipeline.persistent_ref.create
+ end
+
+ it 'does not create a persistent ref' do
+ expect(project.repository).not_to receive(:create_ref)
+
+ subject
+ end
+ end
+ end
+
+ context '#delete' do
+ subject { pipeline.persistent_ref.delete }
+
+ let(:pipeline) { create(:ci_pipeline, sha: sha, project: project) }
+ let(:project) { create(:project, :repository) }
+ let(:sha) { project.repository.commit.sha }
+
+ context 'when a persistent ref exists' do
+ before do
+ pipeline.persistent_ref.create
+ end
+
+ it 'deletes the ref' do
+ expect { subject }.to change { pipeline.persistent_ref.exist? }
+ .from(true).to(false)
+ end
+ end
+
+ context 'when a persistent ref does not exist' do
+ it 'does not raise an error' do
+ expect { subject }.not_to raise_error
+ end
+ end
+ end
+end
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index d5ad70194cb..de0ce9932e8 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -28,7 +28,13 @@ describe Ci::Pipeline, :mailer do
it { is_expected.to have_many(:builds) }
it { is_expected.to have_many(:auto_canceled_pipelines) }
it { is_expected.to have_many(:auto_canceled_jobs) }
+ it { is_expected.to have_many(:sourced_pipelines) }
+ it { is_expected.to have_many(:triggered_pipelines) }
+
it { is_expected.to have_one(:chat_data) }
+ it { is_expected.to have_one(:source_pipeline) }
+ it { is_expected.to have_one(:triggered_by_pipeline) }
+ it { is_expected.to have_one(:source_job) }
it { is_expected.to validate_presence_of(:sha) }
it { is_expected.to validate_presence_of(:status) }
@@ -1136,59 +1142,71 @@ describe Ci::Pipeline, :mailer do
end
describe '#legacy_stages' do
+ using RSpec::Parameterized::TableSyntax
+
subject { pipeline.legacy_stages }
- context 'stages list' do
- it 'returns ordered list of stages' do
- expect(subject.map(&:name)).to eq(%w[build test deploy])
- end
+ where(:ci_composite_status) do
+ [false, true]
end
- context 'stages with statuses' do
- let(:statuses) do
- subject.map { |stage| [stage.name, stage.status] }
+ with_them do
+ before do
+ stub_feature_flags(ci_composite_status: ci_composite_status)
end
- it 'returns list of stages with correct statuses' do
- expect(statuses).to eq([%w(build failed),
- %w(test success),
- %w(deploy running)])
+ context 'stages list' do
+ it 'returns ordered list of stages' do
+ expect(subject.map(&:name)).to eq(%w[build test deploy])
+ end
end
- context 'when commit status is retried' do
- before do
- create(:commit_status, pipeline: pipeline,
- stage: 'build',
- name: 'mac',
- stage_idx: 0,
- status: 'success')
-
- pipeline.process!
+ context 'stages with statuses' do
+ let(:statuses) do
+ subject.map { |stage| [stage.name, stage.status] }
end
- it 'ignores the previous state' do
- expect(statuses).to eq([%w(build success),
+ it 'returns list of stages with correct statuses' do
+ expect(statuses).to eq([%w(build failed),
%w(test success),
%w(deploy running)])
end
- end
- end
- context 'when there is a stage with warnings' do
- before do
- create(:commit_status, pipeline: pipeline,
- stage: 'deploy',
- name: 'prod:2',
- stage_idx: 2,
- status: 'failed',
- allow_failure: true)
+ context 'when commit status is retried' do
+ before do
+ create(:commit_status, pipeline: pipeline,
+ stage: 'build',
+ name: 'mac',
+ stage_idx: 0,
+ status: 'success')
+
+ pipeline.process!
+ end
+
+ it 'ignores the previous state' do
+ expect(statuses).to eq([%w(build success),
+ %w(test success),
+ %w(deploy running)])
+ end
+ end
end
- it 'populates stage with correct number of warnings' do
- deploy_stage = pipeline.legacy_stages.third
+ context 'when there is a stage with warnings' do
+ before do
+ create(:commit_status, pipeline: pipeline,
+ stage: 'deploy',
+ name: 'prod:2',
+ stage_idx: 2,
+ status: 'failed',
+ allow_failure: true)
+ end
- expect(deploy_stage).not_to receive(:statuses)
- expect(deploy_stage).to have_warnings
+ it 'populates stage with correct number of warnings' do
+ deploy_stage = pipeline.legacy_stages.third
+
+ expect(deploy_stage).not_to receive(:statuses)
+ expect(deploy_stage).to have_warnings
+ end
end
end
end
@@ -1318,6 +1336,16 @@ describe Ci::Pipeline, :mailer do
let(:build_b) { create_build('build2', queued_at: 0) }
let(:build_c) { create_build('build3', queued_at: 0) }
+ %w[succeed! drop! cancel! skip!].each do |action|
+ context "when the pipeline recieved #{action} event" do
+ it 'deletes a persistent ref' do
+ expect(pipeline.persistent_ref).to receive(:delete).once
+
+ pipeline.public_send(action)
+ end
+ end
+ end
+
describe '#duration' do
context 'when multiple builds are finished' do
before do
@@ -1755,6 +1783,30 @@ describe Ci::Pipeline, :mailer do
end
end
+ describe '#all_worktree_paths' do
+ let(:files) { { 'main.go' => '', 'mocks/mocks.go' => '' } }
+ let(:project) { create(:project, :custom_repo, files: files) }
+ let(:pipeline) { build(:ci_pipeline, project: project, sha: project.repository.head_commit.sha) }
+
+ it 'returns all file paths cached' do
+ expect(project.repository).to receive(:ls_files).with(pipeline.sha).once.and_call_original
+ expect(pipeline.all_worktree_paths).to eq(files.keys)
+ expect(pipeline.all_worktree_paths).to eq(files.keys)
+ end
+ end
+
+ describe '#top_level_worktree_paths' do
+ let(:files) { { 'main.go' => '', 'mocks/mocks.go' => '' } }
+ let(:project) { create(:project, :custom_repo, files: files) }
+ let(:pipeline) { build(:ci_pipeline, project: project, sha: project.repository.head_commit.sha) }
+
+ it 'returns top-level file paths cached' do
+ expect(project.repository).to receive(:tree).with(pipeline.sha).once.and_call_original
+ expect(pipeline.top_level_worktree_paths).to eq(['main.go'])
+ expect(pipeline.top_level_worktree_paths).to eq(['main.go'])
+ end
+ end
+
describe '#has_kubernetes_active?' do
context 'when kubernetes is active' do
context 'when user configured kubernetes from CI/CD > Clusters' do
@@ -1932,40 +1984,57 @@ describe Ci::Pipeline, :mailer do
end
end
- describe '.latest_status_per_commit' do
+ describe '.latest_pipeline_per_commit' do
let(:project) { create(:project) }
- before do
- pairs = [
- %w[success ref1 123],
- %w[manual master 123],
- %w[failed ref 456]
- ]
-
- pairs.each do |(status, ref, sha)|
- create(
- :ci_empty_pipeline,
- status: status,
- ref: ref,
- sha: sha,
- project: project
- )
- end
+ let!(:commit_123_ref_master) do
+ create(
+ :ci_empty_pipeline,
+ status: 'success',
+ ref: 'master',
+ sha: '123',
+ project: project
+ )
+ end
+ let!(:commit_123_ref_develop) do
+ create(
+ :ci_empty_pipeline,
+ status: 'success',
+ ref: 'develop',
+ sha: '123',
+ project: project
+ )
+ end
+ let!(:commit_456_ref_test) do
+ create(
+ :ci_empty_pipeline,
+ status: 'success',
+ ref: 'test',
+ sha: '456',
+ project: project
+ )
end
context 'without a ref' do
- it 'returns a Hash containing the latest status per commit for all refs' do
- expect(described_class.latest_status_per_commit(%w[123 456]))
- .to eq({ '123' => 'manual', '456' => 'failed' })
+ it 'returns a Hash containing the latest pipeline per commit for all refs' do
+ result = described_class.latest_pipeline_per_commit(%w[123 456])
+
+ expect(result).to match(
+ '123' => commit_123_ref_develop,
+ '456' => commit_456_ref_test
+ )
end
- it 'only includes the status of the given commit SHAs' do
- expect(described_class.latest_status_per_commit(%w[123]))
- .to eq({ '123' => 'manual' })
+ it 'only includes the latest pipeline of the given commit SHAs' do
+ result = described_class.latest_pipeline_per_commit(%w[123])
+
+ expect(result).to match(
+ '123' => commit_123_ref_develop
+ )
end
context 'when there are two pipelines for a ref and SHA' do
- it 'returns the status of the latest pipeline' do
+ let!(:commit_123_ref_master_latest) do
create(
:ci_empty_pipeline,
status: 'failed',
@@ -1973,17 +2042,25 @@ describe Ci::Pipeline, :mailer do
sha: '123',
project: project
)
+ end
+
+ it 'returns the latest pipeline' do
+ result = described_class.latest_pipeline_per_commit(%w[123])
- expect(described_class.latest_status_per_commit(%w[123]))
- .to eq({ '123' => 'failed' })
+ expect(result).to match(
+ '123' => commit_123_ref_master_latest
+ )
end
end
end
context 'with a ref' do
it 'only includes the pipelines for the given ref' do
- expect(described_class.latest_status_per_commit(%w[123 456], 'master'))
- .to eq({ '123' => 'manual' })
+ result = described_class.latest_pipeline_per_commit(%w[123 456], 'master')
+
+ expect(result).to match(
+ '123' => commit_123_ref_master
+ )
end
end
end
@@ -2267,36 +2344,38 @@ describe Ci::Pipeline, :mailer do
describe '#update_status' do
context 'when pipeline is empty' do
it 'updates does not change pipeline status' do
- expect(pipeline.statuses.latest.status).to be_nil
+ expect(pipeline.statuses.latest.slow_composite_status).to be_nil
expect { pipeline.update_status }
- .to change { pipeline.reload.status }.to 'skipped'
+ .to change { pipeline.reload.status }
+ .from('created')
+ .to('skipped')
end
end
context 'when updating status to pending' do
before do
- allow(pipeline)
- .to receive_message_chain(:statuses, :latest, :status)
- .and_return(:running)
+ create(:ci_build, pipeline: pipeline, status: :running)
end
it 'updates pipeline status to running' do
expect { pipeline.update_status }
- .to change { pipeline.reload.status }.to 'running'
+ .to change { pipeline.reload.status }
+ .from('created')
+ .to('running')
end
end
context 'when updating status to scheduled' do
before do
- allow(pipeline)
- .to receive_message_chain(:statuses, :latest, :status)
- .and_return(:scheduled)
+ create(:ci_build, pipeline: pipeline, status: :scheduled)
end
it 'updates pipeline status to scheduled' do
expect { pipeline.update_status }
- .to change { pipeline.reload.status }.to 'scheduled'
+ .to change { pipeline.reload.status }
+ .from('created')
+ .to('scheduled')
end
end
diff --git a/spec/models/ci/runner_spec.rb b/spec/models/ci/runner_spec.rb
index 70ff3cf5dc4..ac438f7d473 100644
--- a/spec/models/ci/runner_spec.rb
+++ b/spec/models/ci/runner_spec.rb
@@ -686,11 +686,13 @@ describe Ci::Runner do
describe '#has_tags?' do
context 'when runner has tags' do
subject { create(:ci_runner, tag_list: ['tag']) }
+
it { is_expected.to have_tags }
end
context 'when runner does not have tags' do
subject { create(:ci_runner, tag_list: []) }
+
it { is_expected.not_to have_tags }
end
end
diff --git a/spec/models/ci/sources/pipeline_spec.rb b/spec/models/ci/sources/pipeline_spec.rb
new file mode 100644
index 00000000000..63bee5bfb55
--- /dev/null
+++ b/spec/models/ci/sources/pipeline_spec.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Ci::Sources::Pipeline do
+ it { is_expected.to belong_to(:project) }
+ it { is_expected.to belong_to(:pipeline) }
+
+ it { is_expected.to belong_to(:source_project) }
+ it { is_expected.to belong_to(:source_job) }
+ it { is_expected.to belong_to(:source_pipeline) }
+
+ it { is_expected.to validate_presence_of(:project) }
+ it { is_expected.to validate_presence_of(:pipeline) }
+
+ it { is_expected.to validate_presence_of(:source_project) }
+ it { is_expected.to validate_presence_of(:source_job) }
+ it { is_expected.to validate_presence_of(:source_pipeline) }
+end
diff --git a/spec/models/ci/stage_spec.rb b/spec/models/ci/stage_spec.rb
index 85cd32fb03a..8827509edda 100644
--- a/spec/models/ci/stage_spec.rb
+++ b/spec/models/ci/stage_spec.rb
@@ -130,7 +130,7 @@ describe Ci::Stage, :models do
context 'when statuses status was not recognized' do
before do
allow(stage)
- .to receive_message_chain(:statuses, :latest, :status)
+ .to receive(:latest_stage_status)
.and_return(:unknown)
end
diff --git a/spec/models/clusters/applications/cert_manager_spec.rb b/spec/models/clusters/applications/cert_manager_spec.rb
index bddc09decc3..c1933c578bc 100644
--- a/spec/models/clusters/applications/cert_manager_spec.rb
+++ b/spec/models/clusters/applications/cert_manager_spec.rb
@@ -54,7 +54,7 @@ describe Clusters::Applications::CertManager do
'kubectl label --overwrite namespace gitlab-managed-apps certmanager.k8s.io/disable-validation=true'
])
expect(subject.postinstall).to eq([
- 'for i in $(seq 1 30); do kubectl apply -f /data/helm/certmanager/config/cluster_issuer.yaml && break; sleep 1s; echo "Retrying ($i)..."; done'
+ "for i in $(seq 1 30); do kubectl apply -f /data/helm/certmanager/config/cluster_issuer.yaml && s=0 && break || s=$?; sleep 1s; echo \"Retrying ($i)...\"; done; (exit $s)"
])
end
diff --git a/spec/models/clusters/applications/helm_spec.rb b/spec/models/clusters/applications/helm_spec.rb
index 9672129bb1e..64f58155a66 100644
--- a/spec/models/clusters/applications/helm_spec.rb
+++ b/spec/models/clusters/applications/helm_spec.rb
@@ -134,4 +134,14 @@ describe Clusters::Applications::Helm do
end
end
end
+
+ describe '#post_uninstall' do
+ let(:helm) { create(:clusters_applications_helm, :installed) }
+
+ it do
+ expect(helm.cluster.kubeclient).to receive(:delete_namespace).with('gitlab-managed-apps')
+
+ helm.post_uninstall
+ end
+ end
end
diff --git a/spec/models/clusters/applications/ingress_spec.rb b/spec/models/clusters/applications/ingress_spec.rb
index d9461ee8581..be0c6df7ad6 100644
--- a/spec/models/clusters/applications/ingress_spec.rb
+++ b/spec/models/clusters/applications/ingress_spec.rb
@@ -96,7 +96,7 @@ describe Clusters::Applications::Ingress do
it 'is initialized with ingress arguments' do
expect(subject.name).to eq('ingress')
expect(subject.chart).to eq('stable/nginx-ingress')
- expect(subject.version).to eq('1.1.2')
+ expect(subject.version).to eq('1.22.1')
expect(subject).to be_rbac
expect(subject.files).to eq(ingress.files)
end
@@ -113,7 +113,7 @@ describe Clusters::Applications::Ingress do
let(:ingress) { create(:clusters_applications_ingress, :errored, version: 'nginx') }
it 'is initialized with the locked version' do
- expect(subject.version).to eq('1.1.2')
+ expect(subject.version).to eq('1.22.1')
end
end
end
diff --git a/spec/models/clusters/applications/jupyter_spec.rb b/spec/models/clusters/applications/jupyter_spec.rb
index e1eee014567..0ec9333d6a7 100644
--- a/spec/models/clusters/applications/jupyter_spec.rb
+++ b/spec/models/clusters/applications/jupyter_spec.rb
@@ -91,7 +91,6 @@ describe Clusters::Applications::Jupyter do
it 'includes valid values' do
expect(values).to include('ingress')
expect(values).to include('hub')
- expect(values).to include('rbac')
expect(values).to include('proxy')
expect(values).to include('auth')
expect(values).to include('singleuser')
@@ -111,7 +110,6 @@ describe Clusters::Applications::Jupyter do
it 'includes valid values' do
expect(values).to include('ingress')
expect(values).to include('hub')
- expect(values).to include('rbac')
expect(values).to include('proxy')
expect(values).to include('auth')
expect(values).to include('singleuser')
diff --git a/spec/models/clusters/applications/knative_spec.rb b/spec/models/clusters/applications/knative_spec.rb
index 3825994b733..51c8a6bb68d 100644
--- a/spec/models/clusters/applications/knative_spec.rb
+++ b/spec/models/clusters/applications/knative_spec.rb
@@ -16,6 +16,13 @@ describe Clusters::Applications::Knative do
allow(ClusterWaitForIngressIpAddressWorker).to receive(:perform_async)
end
+ describe 'when cloud run is enabled' do
+ let(:cluster) { create(:cluster, :provided_by_gcp, :cloud_run_enabled) }
+ let(:knative_cloud_run) { create(:clusters_applications_knative, cluster: cluster) }
+
+ it { expect(knative_cloud_run).to be_not_installable }
+ end
+
describe 'when rbac is not enabled' do
let(:cluster) { create(:cluster, :provided_by_gcp, :rbac_disabled) }
let(:knative_no_rbac) { create(:clusters_applications_knative, cluster: cluster) }
@@ -112,7 +119,7 @@ describe Clusters::Applications::Knative do
subject { knative.install_command }
it 'is initialized with latest version' do
- expect(subject.version).to eq('0.6.0')
+ expect(subject.version).to eq('0.7.0')
end
it_behaves_like 'a command'
diff --git a/spec/models/clusters/applications/prometheus_spec.rb b/spec/models/clusters/applications/prometheus_spec.rb
index 8fc3b7e4c40..2aeb7e5a990 100644
--- a/spec/models/clusters/applications/prometheus_spec.rb
+++ b/spec/models/clusters/applications/prometheus_spec.rb
@@ -175,6 +175,18 @@ describe Clusters::Applications::Prometheus do
expect(subject).to be_rbac
end
+ describe '#predelete' do
+ let(:knative) { create(:clusters_applications_knative, :updated ) }
+ let(:prometheus) { create(:clusters_applications_prometheus, cluster: knative.cluster) }
+
+ subject { prometheus.uninstall_command.predelete }
+
+ it 'deletes knative metrics' do
+ metrics_config = Clusters::Applications::Knative::METRICS_CONFIG
+ is_expected.to include("kubectl delete -f #{metrics_config} --ignore-not-found")
+ end
+ end
+
context 'on a non rbac enabled cluster' do
before do
prometheus.cluster.platform_kubernetes.abac!
diff --git a/spec/models/clusters/cluster_spec.rb b/spec/models/clusters/cluster_spec.rb
index 9afbe6328ca..48e3b4d6bae 100644
--- a/spec/models/clusters/cluster_spec.rb
+++ b/spec/models/clusters/cluster_spec.rb
@@ -11,11 +11,13 @@ describe Clusters::Cluster, :use_clean_rails_memory_store_caching do
subject { build(:cluster) }
it { is_expected.to belong_to(:user) }
+ it { is_expected.to belong_to(:management_project).class_name('::Project') }
it { is_expected.to have_many(:cluster_projects) }
it { is_expected.to have_many(:projects) }
it { is_expected.to have_many(:cluster_groups) }
it { is_expected.to have_many(:groups) }
it { is_expected.to have_one(:provider_gcp) }
+ it { is_expected.to have_one(:provider_aws) }
it { is_expected.to have_one(:platform_kubernetes) }
it { is_expected.to have_one(:application_helm) }
it { is_expected.to have_one(:application_ingress) }
@@ -38,6 +40,15 @@ describe Clusters::Cluster, :use_clean_rails_memory_store_caching do
it { is_expected.to respond_to :project }
+ describe 'applications have inverse_of: :cluster option' do
+ let(:cluster) { create(:cluster) }
+ let!(:helm) { create(:clusters_applications_helm, cluster: cluster) }
+
+ it 'does not do a third query when referencing cluster again' do
+ expect { cluster.application_helm.cluster }.not_to exceed_query_limit(2)
+ end
+ end
+
describe '.enabled' do
subject { described_class.enabled }
@@ -98,6 +109,31 @@ describe Clusters::Cluster, :use_clean_rails_memory_store_caching do
it { is_expected.to contain_exactly(cluster) }
end
+ describe '.aws_provided' do
+ subject { described_class.aws_provided }
+
+ let!(:cluster) { create(:cluster, :provided_by_aws) }
+
+ before do
+ create(:cluster, :provided_by_user)
+ end
+
+ it { is_expected.to contain_exactly(cluster) }
+ end
+
+ describe '.aws_installed' do
+ subject { described_class.aws_installed }
+
+ let!(:cluster) { create(:cluster, :provided_by_aws) }
+
+ before do
+ errored_cluster = create(:cluster, :provided_by_aws)
+ errored_cluster.provider.make_errored!("Error message")
+ end
+
+ it { is_expected.to contain_exactly(cluster) }
+ end
+
describe '.managed' do
subject do
described_class.managed
@@ -280,6 +316,20 @@ describe Clusters::Cluster, :use_clean_rails_memory_store_caching do
it { is_expected.to be_valid }
end
end
+
+ describe 'unique scope for management_project' do
+ let(:project) { create(:project) }
+ let!(:cluster_with_management_project) { create(:cluster, management_project: project) }
+
+ context 'duplicate scopes for the same management project' do
+ let(:cluster) { build(:cluster, management_project: project) }
+
+ it 'adds an error on environment_scope' do
+ expect(cluster).not_to be_valid
+ expect(cluster.errors[:environment_scope].first).to eq('cannot add duplicated environment scope')
+ end
+ end
+ end
end
describe '.ancestor_clusters_for_clusterable' do
@@ -374,7 +424,14 @@ describe Clusters::Cluster, :use_clean_rails_memory_store_caching do
it 'returns a provider' do
is_expected.to eq(cluster.provider_gcp)
- expect(subject.class.name.deconstantize).to eq(Clusters::Providers.to_s)
+ end
+ end
+
+ context 'when provider is aws' do
+ let(:cluster) { create(:cluster, :provided_by_aws) }
+
+ it 'returns a provider' do
+ is_expected.to eq(cluster.provider_aws)
end
end
@@ -537,7 +594,7 @@ describe Clusters::Cluster, :use_clean_rails_memory_store_caching do
before do
expect(Clusters::KubernetesNamespaceFinder).to receive(:new)
- .with(cluster, project: environment.project, environment_slug: environment.slug)
+ .with(cluster, project: environment.project, environment_name: environment.name)
.and_return(double(execute: persisted_namespace))
end
@@ -747,4 +804,26 @@ describe Clusters::Cluster, :use_clean_rails_memory_store_caching do
end
end
end
+
+ describe '#knative_pre_installed?' do
+ subject { cluster.knative_pre_installed? }
+
+ context 'with a GCP provider without cloud_run' do
+ let(:cluster) { create(:cluster, :provided_by_gcp) }
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'with a GCP provider with cloud_run' do
+ let(:cluster) { create(:cluster, :provided_by_gcp, :cloud_run_enabled) }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'with a user provider' do
+ let(:cluster) { create(:cluster, :provided_by_user) }
+
+ it { is_expected.to be_falsey }
+ end
+ end
end
diff --git a/spec/models/clusters/clusters_hierarchy_spec.rb b/spec/models/clusters/clusters_hierarchy_spec.rb
index 0470ebe17ea..fc35b8257e9 100644
--- a/spec/models/clusters/clusters_hierarchy_spec.rb
+++ b/spec/models/clusters/clusters_hierarchy_spec.rb
@@ -4,8 +4,8 @@ require 'spec_helper'
describe Clusters::ClustersHierarchy do
describe '#base_and_ancestors' do
- def base_and_ancestors(clusterable)
- described_class.new(clusterable).base_and_ancestors
+ def base_and_ancestors(clusterable, include_management_project: true)
+ described_class.new(clusterable, include_management_project: include_management_project).base_and_ancestors
end
context 'project in nested group with clusters at every level' do
@@ -44,14 +44,44 @@ describe Clusters::ClustersHierarchy do
end
end
+ context 'cluster has management project' do
+ let!(:project_cluster) { create(:cluster, :project, projects: [project]) }
+ let!(:group_cluster) { create(:cluster, :group, groups: [group], management_project: management_project) }
+
+ let(:group) { create(:group) }
+ let(:project) { create(:project, group: group) }
+ let(:management_project) { create(:project) }
+
+ it 'returns clusters for management_project' do
+ expect(base_and_ancestors(management_project)).to eq([group_cluster])
+ end
+
+ it 'returns nothing if include_management_project is false' do
+ expect(base_and_ancestors(management_project, include_management_project: false)).to be_empty
+ end
+
+ it 'returns clusters for project' do
+ expect(base_and_ancestors(project)).to eq([project_cluster, group_cluster])
+ end
+
+ it 'returns clusters for group' do
+ expect(base_and_ancestors(group)).to eq([group_cluster])
+ end
+ end
+
context 'project in nested group with clusters at some levels' do
- let!(:child) { create(:cluster, :group, groups: [child_group]) }
+ let!(:child) { create(:cluster, :group, groups: [child_group], management_project: management_project) }
let!(:ancestor) { create(:cluster, :group, groups: [ancestor_group]) }
let(:ancestor_group) { create(:group) }
let(:parent_group) { create(:group, parent: ancestor_group) }
let(:child_group) { create(:group, parent: parent_group) }
let(:project) { create(:project, group: child_group) }
+ let(:management_project) { create(:project) }
+
+ it 'returns clusters for management_project' do
+ expect(base_and_ancestors(management_project)).to eq([child])
+ end
it 'returns clusters for project' do
expect(base_and_ancestors(project)).to eq([child, ancestor])
diff --git a/spec/models/clusters/kubernetes_namespace_spec.rb b/spec/models/clusters/kubernetes_namespace_spec.rb
index d4e3a0ac84d..2920bbf2b58 100644
--- a/spec/models/clusters/kubernetes_namespace_spec.rb
+++ b/spec/models/clusters/kubernetes_namespace_spec.rb
@@ -24,13 +24,13 @@ RSpec.describe Clusters::KubernetesNamespace, type: :model do
end
end
- describe '.with_environment_slug' do
+ describe '.with_environment_name' do
let(:cluster) { create(:cluster, :group) }
- let(:environment) { create(:environment, slug: slug) }
+ let(:environment) { create(:environment, name: name) }
- let(:slug) { 'production' }
+ let(:name) { 'production' }
- subject { described_class.with_environment_slug(slug) }
+ subject { described_class.with_environment_name(name) }
context 'there is no associated environment' do
let!(:namespace) { create(:cluster_kubernetes_namespace, cluster: cluster, project: environment.project) }
@@ -48,12 +48,12 @@ RSpec.describe Clusters::KubernetesNamespace, type: :model do
)
end
- context 'with a matching slug' do
+ context 'with a matching name' do
it { is_expected.to eq [namespace] }
end
- context 'without a matching slug' do
- let(:environment) { create(:environment, slug: 'staging') }
+ context 'without a matching name' do
+ let(:environment) { create(:environment, name: 'staging') }
it { is_expected.to be_empty }
end
diff --git a/spec/models/clusters/platforms/kubernetes_spec.rb b/spec/models/clusters/platforms/kubernetes_spec.rb
index 0c4cf291d20..d53fc32cfef 100644
--- a/spec/models/clusters/platforms/kubernetes_spec.rb
+++ b/spec/models/clusters/platforms/kubernetes_spec.rb
@@ -19,14 +19,23 @@ describe Clusters::Platforms::Kubernetes do
it_behaves_like 'having unique enum values'
describe 'before_validation' do
+ let(:kubernetes) { create(:cluster_platform_kubernetes, :configured, namespace: namespace) }
+
context 'when namespace includes upper case' do
- let(:kubernetes) { create(:cluster_platform_kubernetes, :configured, namespace: namespace) }
let(:namespace) { 'ABC' }
it 'converts to lower case' do
expect(kubernetes.namespace).to eq('abc')
end
end
+
+ context 'when namespace is blank' do
+ let(:namespace) { '' }
+
+ it 'nullifies the namespace' do
+ expect(kubernetes.namespace).to be_nil
+ end
+ end
end
describe 'validation' do
@@ -35,8 +44,8 @@ describe Clusters::Platforms::Kubernetes do
context 'when validates namespace' do
let(:kubernetes) { build(:cluster_platform_kubernetes, :configured, namespace: namespace) }
- context 'when namespace is blank' do
- let(:namespace) { '' }
+ context 'when namespace is nil' do
+ let(:namespace) { nil }
it { is_expected.to be_truthy }
end
@@ -218,7 +227,7 @@ describe Clusters::Platforms::Kubernetes do
before do
allow(Clusters::KubernetesNamespaceFinder).to receive(:new)
- .with(cluster, project: project, environment_slug: environment_slug)
+ .with(cluster, project: project, environment_name: environment_name)
.and_return(double(execute: persisted_namespace))
end
@@ -232,6 +241,23 @@ describe Clusters::Platforms::Kubernetes do
it { is_expected.to include(key: 'KUBE_CA_PEM_FILE', value: ca_pem, public: true, file: true) }
end
+ context 'cluster is managed by project' do
+ before do
+ allow(Gitlab::Kubernetes::DefaultNamespace).to receive(:new)
+ .with(cluster, project: project).and_return(double(from_environment_name: namespace))
+
+ allow(platform).to receive(:kubeconfig).with(namespace).and_return('kubeconfig')
+ end
+
+ let(:cluster) { create(:cluster, :group, platform_kubernetes: platform, management_project: project) }
+ let(:namespace) { 'kubernetes-namespace' }
+ let(:kubeconfig) { 'kubeconfig' }
+
+ it { is_expected.to include(key: 'KUBE_TOKEN', value: platform.token, public: false, masked: true) }
+ it { is_expected.to include(key: 'KUBE_NAMESPACE', value: namespace) }
+ it { is_expected.to include(key: 'KUBECONFIG', value: kubeconfig, public: false, file: true) }
+ end
+
context 'kubernetes namespace exists' do
let(:variable) { Hash(key: :fake_key, value: 'fake_value') }
let(:namespace_variables) { Gitlab::Ci::Variables::Collection.new([variable]) }
diff --git a/spec/models/clusters/providers/aws_spec.rb b/spec/models/clusters/providers/aws_spec.rb
new file mode 100644
index 00000000000..ec8159a7ee0
--- /dev/null
+++ b/spec/models/clusters/providers/aws_spec.rb
@@ -0,0 +1,76 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Clusters::Providers::Aws do
+ it { is_expected.to belong_to(:cluster) }
+ it { is_expected.to belong_to(:created_by_user) }
+
+ it { is_expected.to validate_length_of(:key_name).is_at_least(1).is_at_most(255) }
+ it { is_expected.to validate_length_of(:region).is_at_least(1).is_at_most(255) }
+ it { is_expected.to validate_length_of(:instance_type).is_at_least(1).is_at_most(255) }
+ it { is_expected.to validate_length_of(:security_group_id).is_at_least(1).is_at_most(255) }
+ it { is_expected.to validate_presence_of(:subnet_ids) }
+
+ include_examples 'provider status', :cluster_provider_aws
+
+ describe 'default_value_for' do
+ let(:provider) { build(:cluster_provider_aws) }
+
+ it "sets default values" do
+ expect(provider.region).to eq('us-east-1')
+ expect(provider.num_nodes).to eq(3)
+ expect(provider.instance_type).to eq('m5.large')
+ end
+ end
+
+ describe 'custom validations' do
+ subject { provider.valid? }
+
+ context ':num_nodes' do
+ let(:provider) { build(:cluster_provider_aws, num_nodes: num_nodes) }
+
+ context 'contains non-digit characters' do
+ let(:num_nodes) { 'A3' }
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'is blank' do
+ let(:num_nodes) { nil }
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'is less than 1' do
+ let(:num_nodes) { 0 }
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'is a positive integer' do
+ let(:num_nodes) { 3 }
+
+ it { is_expected.to be_truthy }
+ end
+ end
+ end
+
+ describe '#nullify_credentials' do
+ let(:provider) { create(:cluster_provider_aws, :scheduled) }
+
+ subject { provider.nullify_credentials }
+
+ before do
+ expect(provider.access_key_id).to be_present
+ expect(provider.secret_access_key).to be_present
+ end
+
+ it 'removes access_key_id and secret_access_key' do
+ subject
+
+ expect(provider.access_key_id).to be_nil
+ expect(provider.secret_access_key).to be_nil
+ end
+ end
+end
diff --git a/spec/models/clusters/providers/gcp_spec.rb b/spec/models/clusters/providers/gcp_spec.rb
index 785db4febe0..15e152519b4 100644
--- a/spec/models/clusters/providers/gcp_spec.rb
+++ b/spec/models/clusters/providers/gcp_spec.rb
@@ -6,6 +6,8 @@ describe Clusters::Providers::Gcp do
it { is_expected.to belong_to(:cluster) }
it { is_expected.to validate_presence_of(:zone) }
+ include_examples 'provider status', :cluster_provider_gcp
+
describe 'default_value_for' do
let(:gcp) { build(:cluster_provider_gcp) }
@@ -84,86 +86,20 @@ describe Clusters::Providers::Gcp do
it { is_expected.not_to be_legacy_abac }
end
- describe '#state_machine' do
- context 'when any => [:created]' do
- let(:gcp) { build(:cluster_provider_gcp, :creating) }
-
- before do
- gcp.make_created
- end
-
- it 'nullify access_token and operation_id' do
- expect(gcp.access_token).to be_nil
- expect(gcp.operation_id).to be_nil
- expect(gcp).to be_created
- end
- end
-
- context 'when any => [:creating]' do
- let(:gcp) { build(:cluster_provider_gcp) }
-
- context 'when operation_id is present' do
- let(:operation_id) { 'operation-xxx' }
-
- before do
- gcp.make_creating(operation_id)
- end
-
- it 'sets operation_id' do
- expect(gcp.operation_id).to eq(operation_id)
- expect(gcp).to be_creating
- end
- end
-
- context 'when operation_id is nil' do
- let(:operation_id) { nil }
-
- it 'raises an error' do
- expect { gcp.make_creating(operation_id) }
- .to raise_error('operation_id is required')
- end
- end
- end
-
- context 'when any => [:errored]' do
- let(:gcp) { build(:cluster_provider_gcp, :creating) }
- let(:status_reason) { 'err msg' }
-
- it 'nullify access_token and operation_id' do
- gcp.make_errored(status_reason)
-
- expect(gcp.access_token).to be_nil
- expect(gcp.operation_id).to be_nil
- expect(gcp.status_reason).to eq(status_reason)
- expect(gcp).to be_errored
- end
-
- context 'when status_reason is nil' do
- let(:gcp) { build(:cluster_provider_gcp, :errored) }
+ describe '#knative_pre_installed?' do
+ subject { gcp.knative_pre_installed? }
- it 'does not set status_reason' do
- gcp.make_errored(nil)
+ context 'when cluster is cloud_run' do
+ let(:gcp) { create(:cluster_provider_gcp) }
- expect(gcp.status_reason).not_to be_nil
- end
- end
+ it { is_expected.to be_falsey }
end
- end
- describe '#on_creation?' do
- subject { gcp.on_creation? }
-
- context 'when status is creating' do
- let(:gcp) { create(:cluster_provider_gcp, :creating) }
+ context 'when cluster is not cloud_run' do
+ let(:gcp) { create(:cluster_provider_gcp, :cloud_run_enabled) }
it { is_expected.to be_truthy }
end
-
- context 'when status is created' do
- let(:gcp) { create(:cluster_provider_gcp, :created) }
-
- it { is_expected.to be_falsey }
- end
end
describe '#api_client' do
@@ -190,4 +126,31 @@ describe Clusters::Providers::Gcp do
it { is_expected.to be_nil }
end
end
+
+ describe '#nullify_credentials' do
+ let(:provider) { create(:cluster_provider_gcp, :creating) }
+
+ before do
+ expect(provider.access_token).to be_present
+ expect(provider.operation_id).to be_present
+ end
+
+ it 'removes access_token and operation_id' do
+ provider.nullify_credentials
+
+ expect(provider.access_token).to be_nil
+ expect(provider.operation_id).to be_nil
+ end
+ end
+
+ describe '#assign_operation_id' do
+ let(:provider) { create(:cluster_provider_gcp, :scheduled) }
+ let(:operation_id) { 'operation-123' }
+
+ it 'sets operation_id' do
+ provider.assign_operation_id(operation_id)
+
+ expect(provider.operation_id).to eq(operation_id)
+ end
+ end
end
diff --git a/spec/models/commit_collection_spec.rb b/spec/models/commit_collection_spec.rb
index 0bdf83fa90f..d49b71db5f8 100644
--- a/spec/models/commit_collection_spec.rb
+++ b/spec/models/commit_collection_spec.rb
@@ -51,6 +51,30 @@ describe CommitCollection do
end
end
+ describe '#with_latest_pipeline' do
+ let!(:pipeline) do
+ create(
+ :ci_empty_pipeline,
+ ref: 'master',
+ sha: commit.id,
+ status: 'success',
+ project: project
+ )
+ end
+ let(:collection) { described_class.new(project, [commit]) }
+
+ it 'sets the latest pipeline for every commit so no additional queries are necessary' do
+ commits = collection.with_latest_pipeline('master')
+
+ recorder = ActiveRecord::QueryRecorder.new do
+ expect(commits.map { |c| c.latest_pipeline('master') })
+ .to eq([pipeline])
+ end
+
+ expect(recorder.count).to be_zero
+ end
+ end
+
describe 'enrichment methods' do
let(:gitaly_commit) { commit }
let(:hash_commit) { Commit.from_hash(gitaly_commit.to_hash, project) }
@@ -125,27 +149,17 @@ describe CommitCollection do
collection.enrich!
end
- end
- end
- describe '#with_pipeline_status' do
- it 'sets the pipeline status for every commit so no additional queries are necessary' do
- create(
- :ci_empty_pipeline,
- ref: 'master',
- sha: commit.id,
- status: 'success',
- project: project
- )
+ it 'returns the original commit if the commit could not be lazy loaded' do
+ collection = described_class.new(project, [hash_commit])
+ unexisting_lazy_commit = Commit.lazy(project, Gitlab::Git::BLANK_SHA)
- collection = described_class.new(project, [commit])
- collection.with_pipeline_status
+ expect(Commit).to receive(:lazy).with(project, hash_commit.id).and_return(unexisting_lazy_commit)
- recorder = ActiveRecord::QueryRecorder.new do
- expect(commit.status).to eq('success')
- end
+ collection.enrich!
- expect(recorder.count).to be_zero
+ expect(collection.commits).to contain_exactly(hash_commit)
+ end
end
end
diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb
index 5ef824b9950..839c4cadb5e 100644
--- a/spec/models/commit_spec.rb
+++ b/spec/models/commit_spec.rb
@@ -462,78 +462,6 @@ eos
end
end
- describe '#last_pipeline' do
- let!(:first_pipeline) do
- create(:ci_empty_pipeline,
- project: project,
- sha: commit.sha,
- status: 'success')
- end
- let!(:second_pipeline) do
- create(:ci_empty_pipeline,
- project: project,
- sha: commit.sha,
- status: 'success')
- end
-
- it 'returns last pipeline' do
- expect(commit.last_pipeline).to eq second_pipeline
- end
- end
-
- describe '#status' do
- context 'without ref argument' do
- before do
- %w[success failed created pending].each do |status|
- create(:ci_empty_pipeline,
- project: project,
- sha: commit.sha,
- status: status)
- end
- end
-
- it 'gives compound status from latest pipelines' do
- expect(commit.status).to eq(Ci::Pipeline.latest_status)
- expect(commit.status).to eq('pending')
- end
- end
-
- context 'when a particular ref is specified' do
- let!(:pipeline_from_master) do
- create(:ci_empty_pipeline,
- project: project,
- sha: commit.sha,
- ref: 'master',
- status: 'failed')
- end
-
- let!(:pipeline_from_fix) do
- create(:ci_empty_pipeline,
- project: project,
- sha: commit.sha,
- ref: 'fix',
- status: 'success')
- end
-
- it 'gives pipelines from a particular branch' do
- expect(commit.status('master')).to eq(pipeline_from_master.status)
- expect(commit.status('fix')).to eq(pipeline_from_fix.status)
- end
-
- it 'gives compound status from latest pipelines if ref is nil' do
- expect(commit.status(nil)).to eq(pipeline_from_fix.status)
- end
- end
- end
-
- describe '#set_status_for_ref' do
- it 'sets the status for a given reference' do
- commit.set_status_for_ref('master', 'failed')
-
- expect(commit.status('master')).to eq('failed')
- end
- end
-
describe '#participants' do
let(:user1) { build(:user) }
let(:user2) { build(:user) }
@@ -575,6 +503,8 @@ eos
expect(commit.uri_type('files/html')).to be(:tree)
expect(commit.uri_type('files/images/logo-black.png')).to be(:raw)
expect(commit.uri_type('files/images/wm.svg')).to be(:raw)
+ expect(project.commit('audio').uri_type('files/audio/clip.mp3')).to be(:raw)
+ expect(project.commit('audio').uri_type('files/audio/sample.wav')).to be(:raw)
expect(project.commit('video').uri_type('files/videos/intro.mp4')).to be(:raw)
expect(commit.uri_type('files/js/application.js')).to be(:blob)
end
diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb
index 017cca0541e..95e9b0d0f92 100644
--- a/spec/models/commit_status_spec.rb
+++ b/spec/models/commit_status_spec.rb
@@ -321,7 +321,7 @@ describe CommitStatus do
end
it 'returns a correct compound status' do
- expect(described_class.all.status).to eq 'running'
+ expect(described_class.all.slow_composite_status).to eq 'running'
end
end
@@ -331,7 +331,7 @@ describe CommitStatus do
end
it 'returns status that indicates success' do
- expect(described_class.all.status).to eq 'success'
+ expect(described_class.all.slow_composite_status).to eq 'success'
end
end
@@ -342,7 +342,7 @@ describe CommitStatus do
end
it 'returns status according to the scope' do
- expect(described_class.latest.status).to eq 'success'
+ expect(described_class.latest.slow_composite_status).to eq 'success'
end
end
end
diff --git a/spec/models/commit_with_pipeline_spec.rb b/spec/models/commit_with_pipeline_spec.rb
new file mode 100644
index 00000000000..e0bb29fec7b
--- /dev/null
+++ b/spec/models/commit_with_pipeline_spec.rb
@@ -0,0 +1,123 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe CommitWithPipeline do
+ let(:project) { create(:project, :public, :repository) }
+ let(:commit) { described_class.new(project.commit) }
+
+ describe '#last_pipeline' do
+ let!(:first_pipeline) do
+ create(:ci_empty_pipeline,
+ project: project,
+ sha: commit.sha,
+ status: 'success')
+ end
+ let!(:second_pipeline) do
+ create(:ci_empty_pipeline,
+ project: project,
+ sha: commit.sha,
+ status: 'success')
+ end
+
+ it 'returns last pipeline' do
+ expect(commit.last_pipeline).to eq second_pipeline
+ end
+ end
+
+ describe '#latest_pipeline' do
+ let(:pipeline) { double }
+
+ shared_examples_for 'fetching latest pipeline' do |ref|
+ it 'returns the latest pipeline for the project' do
+ expect(commit)
+ .to receive(:latest_pipeline_for_project)
+ .with(ref, project)
+ .and_return(pipeline)
+
+ expect(result).to eq(pipeline)
+ end
+
+ it "returns the memoized pipeline for the key of #{ref}" do
+ commit.set_latest_pipeline_for_ref(ref, pipeline)
+
+ expect(commit)
+ .not_to receive(:latest_pipeline_for_project)
+
+ expect(result).to eq(pipeline)
+ end
+ end
+
+ context 'without ref argument' do
+ let(:result) { commit.latest_pipeline }
+
+ it_behaves_like 'fetching latest pipeline', nil
+ end
+
+ context 'when a particular ref is specified' do
+ let(:result) { commit.latest_pipeline('master') }
+
+ it_behaves_like 'fetching latest pipeline', 'master'
+ end
+ end
+
+ describe '#latest_pipeline_for_project' do
+ let(:project_pipelines) { double }
+ let(:pipeline_project) { double }
+ let(:pipeline) { double }
+ let(:ref) { 'master' }
+ let(:result) { commit.latest_pipeline_for_project(ref, pipeline_project) }
+
+ before do
+ allow(pipeline_project).to receive(:ci_pipelines).and_return(project_pipelines)
+ end
+
+ it 'returns the latest pipeline of the commit for the given ref and project' do
+ expect(project_pipelines)
+ .to receive(:latest_pipeline_per_commit)
+ .with(commit.id, ref)
+ .and_return(commit.id => pipeline)
+
+ expect(result).to eq(pipeline)
+ end
+ end
+
+ describe '#set_latest_pipeline_for_ref' do
+ let(:pipeline) { double }
+
+ it 'sets the latest pipeline for a given reference' do
+ commit.set_latest_pipeline_for_ref('master', pipeline)
+
+ expect(commit.latest_pipeline('master')).to eq(pipeline)
+ end
+ end
+
+ describe "#status" do
+ it 'returns the status of the latest pipeline for the given ref' do
+ expect(commit)
+ .to receive(:latest_pipeline)
+ .with('master')
+ .and_return(double(status: 'success'))
+
+ expect(commit.status('master')).to eq('success')
+ end
+
+ it 'returns nil when latest pipeline is not present for the given ref' do
+ expect(commit)
+ .to receive(:latest_pipeline)
+ .with('master')
+ .and_return(nil)
+
+ expect(commit.status('master')).to eq(nil)
+ end
+
+ it 'returns the status of the latest pipeline when no ref is given' do
+ expect(commit)
+ .to receive(:latest_pipeline)
+ .with(nil)
+ .and_return(double(status: 'success'))
+
+ expect(commit.status).to eq('success')
+ end
+ end
+end
diff --git a/spec/models/concerns/access_requestable_spec.rb b/spec/models/concerns/access_requestable_spec.rb
index de2bc3a387b..5c1694e3737 100644
--- a/spec/models/concerns/access_requestable_spec.rb
+++ b/spec/models/concerns/access_requestable_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
describe AccessRequestable do
describe 'Group' do
describe '#request_access' do
- let(:group) { create(:group, :public, :access_requestable) }
+ let(:group) { create(:group, :public) }
let(:user) { create(:user) }
it { expect(group.request_access(user)).to be_a(GroupMember) }
@@ -13,7 +13,7 @@ describe AccessRequestable do
end
describe '#access_requested?' do
- let(:group) { create(:group, :public, :access_requestable) }
+ let(:group) { create(:group, :public) }
let(:user) { create(:user) }
before do
@@ -26,14 +26,14 @@ describe AccessRequestable do
describe 'Project' do
describe '#request_access' do
- let(:project) { create(:project, :public, :access_requestable) }
+ let(:project) { create(:project, :public) }
let(:user) { create(:user) }
it { expect(project.request_access(user)).to be_a(ProjectMember) }
end
describe '#access_requested?' do
- let(:project) { create(:project, :public, :access_requestable) }
+ let(:project) { create(:project, :public) }
let(:user) { create(:user) }
before do
diff --git a/spec/models/concerns/atomic_internal_id_spec.rb b/spec/models/concerns/atomic_internal_id_spec.rb
new file mode 100644
index 00000000000..0605392c0aa
--- /dev/null
+++ b/spec/models/concerns/atomic_internal_id_spec.rb
@@ -0,0 +1,63 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe AtomicInternalId do
+ let(:milestone) { build(:milestone) }
+ let(:iid) { double('iid', to_i: 42) }
+ let(:external_iid) { 100 }
+ let(:scope_attrs) { { project: milestone.project } }
+ let(:usage) { :milestones }
+
+ describe '#track_project_iid!' do
+ subject { milestone.track_project_iid! }
+
+ it 'tracks the present value' do
+ milestone.iid = external_iid
+
+ expect(InternalId).to receive(:track_greatest).once.with(milestone, scope_attrs, usage, external_iid, anything)
+ expect(InternalId).not_to receive(:generate_next)
+
+ subject
+ end
+
+ context 'when value is set by ensure_project_iid!' do
+ it 'does not track the value' do
+ expect(InternalId).not_to receive(:track_greatest)
+
+ milestone.ensure_project_iid!
+ subject
+ end
+
+ it 'tracks the iid for the scope that is actually present' do
+ milestone.iid = external_iid
+
+ expect(InternalId).to receive(:track_greatest).once.with(milestone, scope_attrs, usage, external_iid, anything)
+ expect(InternalId).not_to receive(:generate_next)
+
+ # group scope is not present here, the milestone does not have a group
+ milestone.track_group_iid!
+ subject
+ end
+ end
+ end
+
+ describe '#ensure_project_iid!' do
+ subject { milestone.ensure_project_iid! }
+
+ it 'generates a new value if non is present' do
+ expect(InternalId).to receive(:generate_next).with(milestone, scope_attrs, usage, anything).and_return(iid)
+
+ expect { subject }.to change { milestone.iid }.from(nil).to(iid.to_i)
+ end
+
+ it 'generates a new value if first set with iid= but later set to nil' do
+ expect(InternalId).to receive(:generate_next).with(milestone, scope_attrs, usage, anything).and_return(iid)
+
+ milestone.iid = external_iid
+ milestone.iid = nil
+
+ expect { subject }.to change { milestone.iid }.from(nil).to(iid.to_i)
+ end
+ end
+end
diff --git a/spec/models/concerns/checksummable_spec.rb b/spec/models/concerns/checksummable_spec.rb
new file mode 100644
index 00000000000..017077bd297
--- /dev/null
+++ b/spec/models/concerns/checksummable_spec.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Checksummable do
+ describe ".hexdigest" do
+ let(:fake_class) do
+ Class.new do
+ include Checksummable
+ end
+ end
+
+ it 'returns the SHA256 sum of the file' do
+ expected = Digest::SHA256.file(__FILE__).hexdigest
+
+ expect(fake_class.hexdigest(__FILE__)).to eq(expected)
+ end
+ end
+end
diff --git a/spec/models/concerns/deployable_spec.rb b/spec/models/concerns/deployable_spec.rb
deleted file mode 100644
index ad2c0770a2c..00000000000
--- a/spec/models/concerns/deployable_spec.rb
+++ /dev/null
@@ -1,82 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-describe Deployable do
- describe '#create_deployment' do
- let(:deployment) { job.deployment }
- let(:environment) { deployment&.environment }
-
- context 'when the deployable object will deploy to production' do
- let!(:job) { create(:ci_build, :start_review_app) }
-
- it 'creates a deployment and environment record' do
- expect(deployment.project).to eq(job.project)
- expect(deployment.ref).to eq(job.ref)
- expect(deployment.tag).to eq(job.tag)
- expect(deployment.sha).to eq(job.sha)
- expect(deployment.user).to eq(job.user)
- expect(deployment.deployable).to eq(job)
- expect(deployment.on_stop).to eq('stop_review_app')
- expect(environment.name).to eq('review/master')
- end
- end
-
- context 'when the deployable object will deploy to a cluster' do
- let(:project) { create(:project) }
- let!(:cluster) { create(:cluster, :provided_by_user, projects: [project]) }
- let!(:job) { create(:ci_build, :start_review_app, project: project) }
-
- it 'creates a deployment with cluster association' do
- expect(deployment.cluster).to eq(cluster)
- end
- end
-
- context 'when the deployable object will stop an environment' do
- let!(:job) { create(:ci_build, :stop_review_app) }
-
- it 'does not create a deployment record' do
- expect(deployment).to be_nil
- end
- end
-
- context 'when the deployable object has already had a deployment' do
- let!(:job) { create(:ci_build, :start_review_app, deployment: race_deployment) }
- let!(:race_deployment) { create(:deployment, :success) }
-
- it 'does not create a new deployment' do
- expect(deployment).to eq(race_deployment)
- end
- end
-
- context 'when the deployable object will not deploy' do
- let!(:job) { create(:ci_build) }
-
- it 'does not create a deployment and environment record' do
- expect(deployment).to be_nil
- expect(environment).to be_nil
- end
- end
-
- context 'when environment scope contains invalid character' do
- let(:job) do
- create(
- :ci_build,
- name: 'job:deploy-to-test-site',
- environment: '$CI_JOB_NAME',
- options: {
- environment: {
- name: '$CI_JOB_NAME',
- url: 'http://staging.example.com/$CI_JOB_NAME',
- on_stop: 'stop_review_app'
- }
- })
- end
-
- it 'does not create a deployment and environment record' do
- expect(deployment).to be_nil
- expect(environment).to be_nil
- end
- end
- end
-end
diff --git a/spec/models/concerns/deployment_platform_spec.rb b/spec/models/concerns/deployment_platform_spec.rb
index 220f244ad71..f99bf18768f 100644
--- a/spec/models/concerns/deployment_platform_spec.rb
+++ b/spec/models/concerns/deployment_platform_spec.rb
@@ -12,6 +12,26 @@ describe DeploymentPlatform do
it { is_expected.to be_nil }
end
+ context 'when project is the cluster\'s management project ' do
+ let!(:cluster_with_management_project) { create(:cluster, :provided_by_user, management_project: project) }
+
+ context 'cluster_management_project feature is enabled' do
+ it 'returns the cluster with management project' do
+ is_expected.to eq(cluster_with_management_project.platform_kubernetes)
+ end
+ end
+
+ context 'cluster_management_project feature is disabled' do
+ before do
+ stub_feature_flags(cluster_management_project: false)
+ end
+
+ it 'returns nothing' do
+ is_expected.to be_nil
+ end
+ end
+ end
+
context 'when project has configured kubernetes from CI/CD > Clusters' do
let!(:cluster) { create(:cluster, :provided_by_gcp, projects: [project]) }
let(:platform_kubernetes) { cluster.platform_kubernetes }
@@ -45,6 +65,35 @@ describe DeploymentPlatform do
is_expected.to eq(group_cluster.platform_kubernetes)
end
+ context 'when project is the cluster\'s management project ' do
+ let!(:cluster_with_management_project) { create(:cluster, :provided_by_user, management_project: project) }
+
+ context 'cluster_management_project feature is enabled' do
+ it 'returns the cluster with management project' do
+ is_expected.to eq(cluster_with_management_project.platform_kubernetes)
+ end
+ end
+
+ context 'cluster_management_project feature is disabled' do
+ before do
+ stub_feature_flags(cluster_management_project: false)
+ end
+
+ it 'returns the group cluster' do
+ is_expected.to eq(group_cluster.platform_kubernetes)
+ end
+ end
+ end
+
+ context 'when project is not the cluster\'s management project' do
+ let(:another_project) { create(:project, group: group) }
+ let!(:cluster_with_management_project) { create(:cluster, :provided_by_user, management_project: another_project) }
+
+ it 'returns the group cluster' do
+ is_expected.to eq(group_cluster.platform_kubernetes)
+ end
+ end
+
context 'when child group has configured kubernetes cluster' do
let(:child_group1) { create(:group, parent: group) }
let!(:child_group1_cluster) { create(:cluster_for_group, groups: [child_group1]) }
diff --git a/spec/models/concerns/has_status_spec.rb b/spec/models/concerns/has_status_spec.rb
index 09fb2fff521..21e4dda6dab 100644
--- a/spec/models/concerns/has_status_spec.rb
+++ b/spec/models/concerns/has_status_spec.rb
@@ -3,12 +3,15 @@
require 'spec_helper'
describe HasStatus do
- describe '.status' do
- subject { CommitStatus.status }
+ describe '.slow_composite_status' do
+ using RSpec::Parameterized::TableSyntax
+
+ subject { CommitStatus.slow_composite_status }
shared_examples 'build status summary' do
context 'all successful' do
let!(:statuses) { Array.new(2) { create(type, status: :success) } }
+
it { is_expected.to eq 'success' }
end
@@ -165,16 +168,26 @@ describe HasStatus do
end
end
- context 'ci build statuses' do
- let(:type) { :ci_build }
-
- it_behaves_like 'build status summary'
+ where(:ci_composite_status) do
+ [false, true]
end
- context 'generic commit statuses' do
- let(:type) { :generic_commit_status }
+ with_them do
+ before do
+ stub_feature_flags(ci_composite_status: ci_composite_status)
+ end
+
+ context 'ci build statuses' do
+ let(:type) { :ci_build }
- it_behaves_like 'build status summary'
+ it_behaves_like 'build status summary'
+ end
+
+ context 'generic commit statuses' do
+ let(:type) { :generic_commit_status }
+
+ it_behaves_like 'build status summary'
+ end
end
end
@@ -372,8 +385,8 @@ describe HasStatus do
end
end
- describe '.status_sql' do
- subject { Ci::Build.status_sql }
+ describe '.legacy_status_sql' do
+ subject { Ci::Build.legacy_status_sql }
it 'returns SQL' do
puts subject
diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb
index 65d41edc035..e8116f0a301 100644
--- a/spec/models/concerns/issuable_spec.rb
+++ b/spec/models/concerns/issuable_spec.rb
@@ -45,8 +45,11 @@ describe Issuable do
it { is_expected.to validate_presence_of(:iid) }
it { is_expected.to validate_presence_of(:author) }
it { is_expected.to validate_presence_of(:title) }
- it { is_expected.to validate_length_of(:title).is_at_most(255) }
- it { is_expected.to validate_length_of(:description).is_at_most(1_000_000) }
+ it { is_expected.to validate_length_of(:title).is_at_most(described_class::TITLE_LENGTH_MAX) }
+ it { is_expected.to validate_length_of(:description).is_at_most(described_class::DESCRIPTION_LENGTH_MAX).on(:create) }
+
+ it_behaves_like 'validates description length with custom validation'
+ it_behaves_like 'truncates the description to its allowed maximum length on import'
end
describe 'milestone' do
diff --git a/spec/models/concerns/noteable_spec.rb b/spec/models/concerns/noteable_spec.rb
index 929b5f52c7c..f823ac0165f 100644
--- a/spec/models/concerns/noteable_spec.rb
+++ b/spec/models/concerns/noteable_spec.rb
@@ -6,6 +6,7 @@ describe Noteable do
let!(:active_diff_note1) { create(:diff_note_on_merge_request) }
let(:project) { active_diff_note1.project }
subject { active_diff_note1.noteable }
+
let!(:active_diff_note2) { create(:diff_note_on_merge_request, project: project, noteable: subject, in_reply_to: active_diff_note1) }
let!(:active_diff_note3) { create(:diff_note_on_merge_request, project: project, noteable: subject, position: active_position2) }
let!(:outdated_diff_note1) { create(:diff_note_on_merge_request, project: project, noteable: subject, position: outdated_position) }
diff --git a/spec/models/concerns/stepable_spec.rb b/spec/models/concerns/stepable_spec.rb
index 5685de6a9bf..51356c3eaf6 100644
--- a/spec/models/concerns/stepable_spec.rb
+++ b/spec/models/concerns/stepable_spec.rb
@@ -7,6 +7,8 @@ describe Stepable do
Class.new do
include Stepable
+ attr_writer :return_non_success
+
steps :method1, :method2, :method3
def execute
@@ -15,18 +17,18 @@ describe Stepable do
private
- def method1
+ def method1(_result)
{ status: :success }
end
- def method2
- return { status: :error } unless @pass
+ def method2(result)
+ return { status: :not_a_success } if @return_non_success
- { status: :success, variable1: 'var1' }
+ result.merge({ status: :success, variable1: 'var1', excluded_variable: 'a' })
end
- def method3
- { status: :success, variable2: 'var2' }
+ def method3(result)
+ result.except(:excluded_variable).merge({ status: :success, variable2: 'var2' })
end
end
end
@@ -41,8 +43,8 @@ describe Stepable do
private
- def appended_method1
- { status: :success }
+ def appended_method1(previous_result)
+ previous_result.merge({ status: :success })
end
end
end
@@ -51,21 +53,19 @@ describe Stepable do
described_class.prepend(prepended_module)
end
- it 'stops after the first error' do
+ it 'stops after the first non success status' do
+ subject.return_non_success = true
+
expect(subject).not_to receive(:method3)
expect(subject).not_to receive(:appended_method1)
expect(subject.execute).to eq(
- status: :error,
- failed_step: :method2
+ status: :not_a_success,
+ last_step: :method2
)
end
context 'when all methods return success' do
- before do
- subject.instance_variable_set(:@pass, true)
- end
-
it 'calls all methods in order' do
expect(subject).to receive(:method1).and_call_original.ordered
expect(subject).to receive(:method2).and_call_original.ordered
@@ -82,6 +82,10 @@ describe Stepable do
variable2: 'var2'
)
end
+
+ it 'can modify results of previous steps' do
+ expect(subject.execute).not_to include(excluded_variable: 'a')
+ end
end
context 'with multiple stepable classes' do
diff --git a/spec/models/concerns/token_authenticatable_spec.rb b/spec/models/concerns/token_authenticatable_spec.rb
index 51e28974ae0..43b894b5957 100644
--- a/spec/models/concerns/token_authenticatable_spec.rb
+++ b/spec/models/concerns/token_authenticatable_spec.rb
@@ -17,6 +17,7 @@ describe User, 'TokenAuthenticatable' do
describe 'ensures authentication token' do
subject { create(:user).send(token_field) }
+
it { is_expected.to be_a String }
end
end
diff --git a/spec/models/container_repository_spec.rb b/spec/models/container_repository_spec.rb
index 935838ce294..eea539746a5 100644
--- a/spec/models/container_repository_spec.rb
+++ b/spec/models/container_repository_spec.rb
@@ -78,7 +78,7 @@ describe ContainerRepository do
describe '#delete_tags!' do
let(:repository) do
create(:container_repository, name: 'my_image',
- tags: %w[latest rc1],
+ tags: { latest: '123', rc1: '234' },
project: project)
end
@@ -86,6 +86,7 @@ describe ContainerRepository do
it 'returns status that indicates success' do
expect(repository.client)
.to receive(:delete_repository_tag)
+ .twice
.and_return(true)
expect(repository.delete_tags!).to be_truthy
@@ -96,6 +97,7 @@ describe ContainerRepository do
it 'returns status that indicates failure' do
expect(repository.client)
.to receive(:delete_repository_tag)
+ .twice
.and_return(false)
expect(repository.delete_tags!).to be_falsey
diff --git a/spec/models/deploy_keys_project_spec.rb b/spec/models/deploy_keys_project_spec.rb
index c137444763b..1dbae78a01d 100644
--- a/spec/models/deploy_keys_project_spec.rb
+++ b/spec/models/deploy_keys_project_spec.rb
@@ -16,6 +16,7 @@ describe DeployKeysProject do
describe "Destroying" do
let(:project) { create(:project) }
subject { create(:deploy_keys_project, project: project) }
+
let(:deploy_key) { subject.deploy_key }
context "when the deploy key is only used by this project" do
diff --git a/spec/models/deployment_spec.rb b/spec/models/deployment_spec.rb
index 51ed8e9421b..3a0b3c46ad0 100644
--- a/spec/models/deployment_spec.rb
+++ b/spec/models/deployment_spec.rb
@@ -348,4 +348,17 @@ describe Deployment do
expect(deployment.deployed_by).to eq(build_user)
end
end
+
+ describe '.find_successful_deployment!' do
+ it 'returns a successful deployment' do
+ deploy = create(:deployment, :success)
+
+ expect(described_class.find_successful_deployment!(deploy.iid)).to eq(deploy)
+ end
+
+ it 'raises when no deployment is found' do
+ expect { described_class.find_successful_deployment!(-1) }
+ .to raise_error(ActiveRecord::RecordNotFound)
+ end
+ end
end
diff --git a/spec/models/description_version_spec.rb b/spec/models/description_version_spec.rb
new file mode 100644
index 00000000000..5ec34c0cde4
--- /dev/null
+++ b/spec/models/description_version_spec.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe DescriptionVersion do
+ describe 'associations' do
+ it { is_expected.to belong_to :issue }
+ it { is_expected.to belong_to :merge_request }
+ end
+
+ describe 'validations' do
+ describe 'exactly_one_issuable' do
+ using RSpec::Parameterized::TableSyntax
+
+ subject { described_class.new(issue_id: issue_id, merge_request_id: merge_request_id).valid? }
+
+ where(:issue_id, :merge_request_id, :valid?) do
+ nil | 1 | true
+ 1 | nil | true
+ nil | nil | false
+ 1 | 1 | false
+ end
+
+ with_them do
+ it { is_expected.to eq(valid?) }
+ end
+ end
+ end
+end
diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb
index 521c4704c87..786f3b832c4 100644
--- a/spec/models/environment_spec.rb
+++ b/spec/models/environment_spec.rb
@@ -882,4 +882,19 @@ describe Environment, :use_clean_rails_memory_store_caching do
end
end
end
+
+ describe '.find_or_create_by_name' do
+ it 'finds an existing environment if it exists' do
+ env = create(:environment)
+
+ expect(described_class.find_or_create_by_name(env.name)).to eq(env)
+ end
+
+ it 'creates an environment if it does not exist' do
+ env = project.environments.find_or_create_by_name('kittens')
+
+ expect(env).to be_an_instance_of(described_class)
+ expect(env).to be_persisted
+ end
+ end
end
diff --git a/spec/models/environment_status_spec.rb b/spec/models/environment_status_spec.rb
index e2836420df9..01d331f518b 100644
--- a/spec/models/environment_status_spec.rb
+++ b/spec/models/environment_status_spec.rb
@@ -95,7 +95,7 @@ describe EnvironmentStatus do
describe '.build_environments_status' do
subject { described_class.send(:build_environments_status, merge_request, user, pipeline) }
- let!(:build) { create(:ci_build, :deploy_to_production, pipeline: pipeline) }
+ let!(:build) { create(:ci_build, :with_deployment, :deploy_to_production, pipeline: pipeline) }
let(:environment) { build.deployment.environment }
let(:user) { project.owner }
diff --git a/spec/models/event_collection_spec.rb b/spec/models/event_collection_spec.rb
index efe511042c3..c421ffa000d 100644
--- a/spec/models/event_collection_spec.rb
+++ b/spec/models/event_collection_spec.rb
@@ -4,50 +4,75 @@ require 'spec_helper'
describe EventCollection do
describe '#to_a' do
- let(:project) { create(:project_empty_repo) }
- let(:projects) { Project.where(id: project.id) }
- let(:user) { create(:user) }
+ set(:group) { create(:group) }
+ set(:project) { create(:project_empty_repo, group: group) }
+ set(:projects) { Project.where(id: project.id) }
+ set(:user) { create(:user) }
- before do
- 20.times do
- event = create(:push_event, project: project, author: user)
+ context 'with project events' do
+ before do
+ 20.times do
+ event = create(:push_event, project: project, author: user)
- create(:push_event_payload, event: event)
+ create(:push_event_payload, event: event)
+ end
+
+ create(:closed_issue_event, project: project, author: user)
end
- create(:closed_issue_event, project: project, author: user)
- end
+ it 'returns an Array of events' do
+ events = described_class.new(projects).to_a
- it 'returns an Array of events' do
- events = described_class.new(projects).to_a
+ expect(events).to be_an_instance_of(Array)
+ end
- expect(events).to be_an_instance_of(Array)
- end
+ it 'applies a limit to the number of events' do
+ events = described_class.new(projects).to_a
- it 'applies a limit to the number of events' do
- events = described_class.new(projects).to_a
+ expect(events.length).to eq(20)
+ end
- expect(events.length).to eq(20)
- end
+ it 'can paginate through events' do
+ events = described_class.new(projects, offset: 20).to_a
- it 'can paginate through events' do
- events = described_class.new(projects, offset: 20).to_a
+ expect(events.length).to eq(1)
+ end
- expect(events.length).to eq(1)
- end
+ it 'returns an empty Array when crossing the maximum page number' do
+ events = described_class.new(projects, limit: 1, offset: 15).to_a
- it 'returns an empty Array when crossing the maximum page number' do
- events = described_class.new(projects, limit: 1, offset: 15).to_a
+ expect(events).to be_empty
+ end
+
+ it 'allows filtering of events using an EventFilter' do
+ filter = EventFilter.new(EventFilter::ISSUE)
+ events = described_class.new(projects, filter: filter).to_a
- expect(events).to be_empty
+ expect(events.length).to eq(1)
+ expect(events[0].action).to eq(Event::CLOSED)
+ end
end
- it 'allows filtering of events using an EventFilter' do
- filter = EventFilter.new(EventFilter::ISSUE)
- events = described_class.new(projects, filter: filter).to_a
+ context 'with group events' do
+ let(:groups) { group.self_and_descendants.public_or_visible_to_user(user) }
+ let(:subject) { described_class.new(projects, groups: groups).to_a }
+
+ it 'includes also group events' do
+ subgroup = create(:group, parent: group)
+ event1 = create(:event, project: project, author: user)
+ event2 = create(:event, project: nil, group: group, author: user)
+ event3 = create(:event, project: nil, group: subgroup, author: user)
- expect(events.length).to eq(1)
- expect(events[0].action).to eq(Event::CLOSED)
+ expect(subject).to eq([event3, event2, event1])
+ end
+
+ it 'does not include events from inaccessible groups' do
+ subgroup = create(:group, :private, parent: group)
+ event1 = create(:event, project: nil, group: group, author: user)
+ create(:event, project: nil, group: subgroup, author: user)
+
+ expect(subject).to eq([event1])
+ end
end
end
end
diff --git a/spec/models/event_spec.rb b/spec/models/event_spec.rb
index 62663c247d1..ff2e1aa047e 100644
--- a/spec/models/event_spec.rb
+++ b/spec/models/event_spec.rb
@@ -100,26 +100,31 @@ describe Event do
describe '#membership_changed?' do
context "created" do
subject { build(:event, :created).membership_changed? }
+
it { is_expected.to be_falsey }
end
context "updated" do
subject { build(:event, :updated).membership_changed? }
+
it { is_expected.to be_falsey }
end
context "expired" do
subject { build(:event, :expired).membership_changed? }
+
it { is_expected.to be_truthy }
end
context "left" do
subject { build(:event, :left).membership_changed? }
+
it { is_expected.to be_truthy }
end
context "joined" do
subject { build(:event, :joined).membership_changed? }
+
it { is_expected.to be_truthy }
end
end
diff --git a/spec/models/evidence_spec.rb b/spec/models/evidence_spec.rb
new file mode 100644
index 00000000000..00788c2c391
--- /dev/null
+++ b/spec/models/evidence_spec.rb
@@ -0,0 +1,87 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Evidence do
+ let_it_be(:project) { create(:project) }
+ let(:release) { create(:release, project: project) }
+ let(:schema_file) { 'evidences/evidence' }
+ let(:summary_json) { described_class.last.summary.to_json }
+
+ describe 'associations' do
+ it { is_expected.to belong_to(:release) }
+ end
+
+ describe 'summary_sha' do
+ it 'returns nil if summary is nil' do
+ expect(build(:evidence, summary: nil).summary_sha).to be_nil
+ end
+ end
+
+ describe '#generate_summary_and_sha' do
+ before do
+ described_class.create!(release: release)
+ end
+
+ context 'when a release name is not provided' do
+ let(:release) { create(:release, project: project, name: nil) }
+
+ it 'creates a valid JSON object' do
+ expect(release.name).to be_nil
+ expect(summary_json).to match_schema(schema_file)
+ end
+ end
+
+ context 'when a release is associated to a milestone' do
+ let(:milestone) { create(:milestone, project: project) }
+ let(:release) { create(:release, project: project, milestones: [milestone]) }
+
+ context 'when a milestone has no issue associated with it' do
+ it 'creates a valid JSON object' do
+ expect(milestone.issues).to be_empty
+ expect(summary_json).to match_schema(schema_file)
+ end
+ end
+
+ context 'when a milestone has no description' do
+ let(:milestone) { create(:milestone, project: project, description: nil) }
+
+ it 'creates a valid JSON object' do
+ expect(milestone.description).to be_nil
+ expect(summary_json).to match_schema(schema_file)
+ end
+ end
+
+ context 'when a milestone has no due_date' do
+ let(:milestone) { create(:milestone, project: project, due_date: nil) }
+
+ it 'creates a valid JSON object' do
+ expect(milestone.due_date).to be_nil
+ expect(summary_json).to match_schema(schema_file)
+ end
+ end
+
+ context 'when a milestone has an issue' do
+ context 'when the issue has no description' do
+ let(:issue) { create(:issue, project: project, description: nil, state: 'closed') }
+
+ before do
+ milestone.issues << issue
+ end
+
+ it 'creates a valid JSON object' do
+ expect(milestone.issues.first.description).to be_nil
+ expect(summary_json).to match_schema(schema_file)
+ end
+ end
+ end
+ end
+
+ context 'when a release is not associated to any milestone' do
+ it 'creates a valid JSON object' do
+ expect(release.milestones).to be_empty
+ expect(summary_json).to match_schema(schema_file)
+ end
+ end
+ end
+end
diff --git a/spec/models/gpg_signature_spec.rb b/spec/models/gpg_signature_spec.rb
index 4911375c962..a780b8bfdf5 100644
--- a/spec/models/gpg_signature_spec.rb
+++ b/spec/models/gpg_signature_spec.rb
@@ -20,6 +20,7 @@ RSpec.describe GpgSignature do
describe 'validation' do
subject { described_class.new }
+
it { is_expected.to validate_presence_of(:commit_sha) }
it { is_expected.to validate_presence_of(:project_id) }
it { is_expected.to validate_presence_of(:gpg_key_primary_keyid) }
@@ -60,6 +61,18 @@ RSpec.describe GpgSignature do
end
end
+ describe '.by_commit_sha scope' do
+ let(:gpg_key) { create(:gpg_key, key: GpgHelpers::User2.public_key) }
+ let!(:another_gpg_signature) { create(:gpg_signature, gpg_key: gpg_key) }
+
+ it 'returns all gpg signatures by sha' do
+ expect(described_class.by_commit_sha(commit_sha)).to eq([gpg_signature])
+ expect(
+ described_class.by_commit_sha([commit_sha, another_gpg_signature.commit_sha])
+ ).to contain_exactly(gpg_signature, another_gpg_signature)
+ end
+ end
+
describe '#commit' do
it 'fetches the commit through the project' do
expect_any_instance_of(Project).to receive(:commit).with(commit_sha).and_return(commit)
diff --git a/spec/models/grafana_integration_spec.rb b/spec/models/grafana_integration_spec.rb
new file mode 100644
index 00000000000..f8973097a40
--- /dev/null
+++ b/spec/models/grafana_integration_spec.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe GrafanaIntegration do
+ describe 'associations' do
+ it { is_expected.to belong_to(:project) }
+ end
+
+ describe 'validations' do
+ it { is_expected.to validate_presence_of(:project) }
+ it { is_expected.to validate_presence_of(:token) }
+
+ it 'disallows invalid urls for grafana_url' do
+ unsafe_url = %{https://replaceme.com/'><script>alert(document.cookie)</script>}
+ non_ascii_url = 'http://gitlab.com/api/0/projects/project1/something€'
+ blank_url = ''
+ excessively_long_url = 'https://grafan' + 'a' * 1024 + '.com'
+
+ is_expected.not_to allow_values(
+ unsafe_url,
+ non_ascii_url,
+ blank_url,
+ excessively_long_url
+ ).for(:grafana_url)
+ end
+
+ it 'allows valid urls for grafana_url' do
+ external_url = 'http://grafana.com/'
+ internal_url = 'http://192.168.1.1'
+
+ is_expected.to allow_value(
+ external_url,
+ internal_url
+ ).for(:grafana_url)
+ end
+ end
+end
diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb
index 796b6917fb2..520421ac5e3 100644
--- a/spec/models/group_spec.rb
+++ b/spec/models/group_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
describe Group do
- let!(:group) { create(:group, :access_requestable) }
+ let!(:group) { create(:group) }
describe 'associations' do
it { is_expected.to have_many :projects }
@@ -331,7 +331,7 @@ describe Group do
end
describe '#avatar_url' do
- let!(:group) { create(:group, :access_requestable, :with_avatar) }
+ let!(:group) { create(:group, :with_avatar) }
let(:user) { create(:user) }
context 'when avatar file is uploaded' do
@@ -880,22 +880,6 @@ describe Group do
end
end
- describe '#has_parent?' do
- context 'when the group has a parent' do
- it 'is truthy' do
- group = create(:group, :nested)
- expect(group.has_parent?).to be_truthy
- end
- end
-
- context 'when the group has no parent' do
- it 'is falsy' do
- group = create(:group, parent: nil)
- expect(group.has_parent?).to be_falsy
- end
- end
- end
-
context 'with uploads' do
it_behaves_like 'model with uploads', true do
let(:model_object) { create(:group, :with_avatar) }
@@ -1058,4 +1042,21 @@ describe Group do
expect(group.access_request_approvers_to_be_notified).to eq(active_owners_in_recent_sign_in_desc_order)
end
end
+
+ describe '.groups_including_descendants_by' do
+ it 'returns the expected groups for a group and its descendants' do
+ parent_group1 = create(:group)
+ child_group1 = create(:group, parent: parent_group1)
+ child_group2 = create(:group, parent: parent_group1)
+
+ parent_group2 = create(:group)
+ child_group3 = create(:group, parent: parent_group2)
+
+ create(:group)
+
+ groups = described_class.groups_including_descendants_by([parent_group2.id, parent_group1.id])
+
+ expect(groups).to contain_exactly(parent_group1, parent_group2, child_group1, child_group2, child_group3)
+ end
+ end
end
diff --git a/spec/models/hooks/web_hook_spec.rb b/spec/models/hooks/web_hook_spec.rb
index fe08dc4f5e6..025c11d6407 100644
--- a/spec/models/hooks/web_hook_spec.rb
+++ b/spec/models/hooks/web_hook_spec.rb
@@ -6,7 +6,7 @@ describe WebHook do
let(:hook) { build(:project_hook) }
describe 'associations' do
- it { is_expected.to have_many(:web_hook_logs).dependent(:destroy) }
+ it { is_expected.to have_many(:web_hook_logs) }
end
describe 'validations' do
@@ -85,4 +85,13 @@ describe WebHook do
hook.async_execute(data, hook_name)
end
end
+
+ describe '#destroy' do
+ it 'cascades to web_hook_logs' do
+ web_hook = create(:project_hook)
+ create_list(:web_hook_log, 3, web_hook: web_hook)
+
+ expect { web_hook.destroy }.to change(web_hook.web_hook_logs, :count).by(-3)
+ end
+ end
end
diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb
index 9c58d307c4c..18a1a30eee5 100644
--- a/spec/models/issue_spec.rb
+++ b/spec/models/issue_spec.rb
@@ -138,7 +138,10 @@ describe Issue do
end
it 'changes the state to closed' do
- expect { issue.close }.to change { issue.state }.from('opened').to('closed')
+ open_state = described_class.available_states[:opened]
+ closed_state = described_class.available_states[:closed]
+
+ expect { issue.close }.to change { issue.state_id }.from(open_state).to(closed_state)
end
end
@@ -155,7 +158,7 @@ describe Issue do
end
it 'changes the state to opened' do
- expect { issue.reopen }.to change { issue.state }.from('closed').to('opened')
+ expect { issue.reopen }.to change { issue.state_id }.from(described_class.available_states[:closed]).to(described_class.available_states[:opened])
end
end
@@ -277,6 +280,7 @@ describe Issue do
context 'checking destination project also' do
subject { issue.can_move?(user, to_project) }
+
let(:to_project) { create(:project) }
context 'destination project allowed' do
@@ -899,4 +903,6 @@ describe Issue do
let(:default_params) { { project: project } }
end
end
+
+ it_behaves_like 'versioned description'
end
diff --git a/spec/models/lfs_object_spec.rb b/spec/models/lfs_object_spec.rb
index 85bfc3f1387..47cae5cf197 100644
--- a/spec/models/lfs_object_spec.rb
+++ b/spec/models/lfs_object_spec.rb
@@ -31,6 +31,46 @@ describe LfsObject do
end
end
+ describe '#project_allowed_access?' do
+ set(:lfs_object) { create(:lfs_objects_project).lfs_object }
+ set(:project) { create(:project) }
+
+ it 'returns true when project is linked' do
+ create(:lfs_objects_project, lfs_object: lfs_object, project: project)
+
+ expect(lfs_object.project_allowed_access?(project)).to eq(true)
+ end
+
+ it 'returns false when project is not linked' do
+ expect(lfs_object.project_allowed_access?(project)).to eq(false)
+ end
+
+ context 'when project is a member of a fork network' do
+ set(:fork_network) { create(:fork_network) }
+ set(:fork_network_root_project) { fork_network.root_project }
+ set(:fork_network_membership) { create(:fork_network_member, project: project, fork_network: fork_network) }
+
+ it 'returns true for all members when forked project is linked' do
+ create(:lfs_objects_project, lfs_object: lfs_object, project: project)
+
+ expect(lfs_object.project_allowed_access?(project)).to eq(true)
+ expect(lfs_object.project_allowed_access?(fork_network_root_project)).to eq(true)
+ end
+
+ it 'returns true for all members when root of network is linked' do
+ create(:lfs_objects_project, lfs_object: lfs_object, project: fork_network_root_project)
+
+ expect(lfs_object.project_allowed_access?(project)).to eq(true)
+ expect(lfs_object.project_allowed_access?(fork_network_root_project)).to eq(true)
+ end
+
+ it 'returns false when no member of fork network is linked' do
+ expect(lfs_object.project_allowed_access?(project)).to eq(false)
+ expect(lfs_object.project_allowed_access?(fork_network_root_project)).to eq(false)
+ end
+ end
+ end
+
describe '#schedule_background_upload' do
before do
stub_lfs_setting(enabled: true)
@@ -116,4 +156,15 @@ describe LfsObject do
end
end
end
+
+ describe ".calculate_oid" do
+ let(:lfs_object) { create(:lfs_object, :with_file) }
+
+ it 'returns SHA256 sum of the file' do
+ path = lfs_object.file.path
+ expected = Digest::SHA256.file(path).hexdigest
+
+ expect(described_class.calculate_oid(path)).to eq expected
+ end
+ end
end
diff --git a/spec/models/list_spec.rb b/spec/models/list_spec.rb
index dc28204d7aa..bc9124e73af 100644
--- a/spec/models/list_spec.rb
+++ b/spec/models/list_spec.rb
@@ -136,18 +136,6 @@ describe List do
expect(preferences).to be_persisted
expect(preferences.collapsed).to eq(true)
end
-
- context 'when preferences are already loaded for user' do
- it 'gets preloaded user preferences' do
- fetched_list = described_class.where(id: list.id).with_preferences_for(user).first
-
- expect(fetched_list).to receive(:preloaded_preferences_for).with(user).and_call_original
-
- preferences = fetched_list.preferences_for(user)
-
- expect(preferences.collapsed).to eq(true)
- end
- end
end
context 'when preferences for user does not exist' do
diff --git a/spec/models/member_spec.rb b/spec/models/member_spec.rb
index 2cb4f222ea4..e7f03226826 100644
--- a/spec/models/member_spec.rb
+++ b/spec/models/member_spec.rb
@@ -92,7 +92,7 @@ describe Member do
describe 'Scopes & finders' do
before do
- project = create(:project, :public, :access_requestable)
+ project = create(:project, :public)
group = create(:group)
@owner_user = create(:user).tap { |u| group.add_owner(u) }
@owner = group.members.find_by(user_id: @owner_user.id)
@@ -230,7 +230,7 @@ describe Member do
describe '.add_user' 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!(:source) { create(source_type, :public) }
let!(:user) { create(:user) }
let!(:admin) { create(:admin) }
@@ -437,7 +437,7 @@ describe Member do
describe '.add_users' 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!(:source) { create(source_type, :public) }
let!(:admin) { create(:admin) }
let(:user1) { create(:user) }
let(:user2) { create(:user) }
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index 65cc1a4bd6b..ad79bee8801 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -470,7 +470,7 @@ describe MergeRequest do
commit = double('commit1', safe_message: "Fixes #{issue.to_reference}")
allow(subject).to receive(:commits).and_return([commit])
- allow(subject).to receive(:state).and_return("closed")
+ allow(subject).to receive(:state_id).and_return(described_class.available_states[:closed])
expect { subject.cache_merge_request_closes_issues!(subject.author) }.not_to change(subject.merge_requests_closing_issues, :count)
end
@@ -479,7 +479,7 @@ describe MergeRequest do
issue = create :issue, project: subject.project
commit = double('commit1', safe_message: "Fixes #{issue.to_reference}")
allow(subject).to receive(:commits).and_return([commit])
- allow(subject).to receive(:state).and_return("merged")
+ allow(subject).to receive(:state_id).and_return(described_class.available_states[:merged])
expect { subject.cache_merge_request_closes_issues!(subject.author) }.not_to change(subject.merge_requests_closing_issues, :count)
end
@@ -541,6 +541,7 @@ describe MergeRequest do
context 'with diffs' do
subject { create(:merge_request, :with_diffs) }
+
it 'returns the sha of the source branch last commit' do
expect(subject.source_branch_sha).to eq(last_branch_commit.sha)
end
@@ -548,6 +549,7 @@ describe MergeRequest do
context 'without diffs' do
subject { create(:merge_request, :without_diffs) }
+
it 'returns the sha of the source branch last commit' do
expect(subject.source_branch_sha).to eq(last_branch_commit.sha)
end
@@ -570,6 +572,7 @@ describe MergeRequest do
context 'when the merge request is being created' do
subject { build(:merge_request, source_branch: nil, compare_commits: []) }
+
it 'returns nil' do
expect(subject.source_branch_sha).to be_nil
end
@@ -650,6 +653,46 @@ describe MergeRequest do
end
end
+ describe '#note_positions_for_paths' do
+ let(:merge_request) { create(:merge_request, :with_diffs) }
+ let(:project) { merge_request.project }
+ let!(:diff_note) do
+ create(:diff_note_on_merge_request, project: project, noteable: merge_request)
+ end
+
+ let(:file_paths) { merge_request.diffs.diff_files.map(&:file_path) }
+
+ subject do
+ merge_request.note_positions_for_paths(file_paths)
+ end
+
+ it 'returns a Gitlab::Diff::PositionCollection' do
+ expect(subject).to be_a(Gitlab::Diff::PositionCollection)
+ end
+
+ context 'within all diff files' do
+ it 'returns correct positions' do
+ expect(subject).to match_array([diff_note.position])
+ end
+ end
+
+ context 'within specific diff file' do
+ let(:file_paths) { [diff_note.position.file_path] }
+
+ it 'returns correct positions' do
+ expect(subject).to match_array([diff_note.position])
+ end
+ end
+
+ context 'within no diff files' do
+ let(:file_paths) { [] }
+
+ it 'returns no positions' do
+ expect(subject.to_a).to be_empty
+ end
+ end
+ end
+
describe '#discussions_diffs' do
let(:merge_request) { create(:merge_request) }
@@ -2032,7 +2075,7 @@ describe MergeRequest do
end
it 'refuses to enqueue a job if the MR is not open' do
- merge_request.update_column(:state, 'foo')
+ merge_request.update_column(:state_id, 5)
expect(RebaseWorker).not_to receive(:perform_async)
@@ -2326,7 +2369,7 @@ describe MergeRequest do
merge_requests_as_head_pipeline: [merge_request])
end
- let!(:job) { create(:ci_build, :start_review_app, pipeline: pipeline, project: project) }
+ let!(:job) { create(:ci_build, :with_deployment, :start_review_app, pipeline: pipeline, project: project) }
it 'returns environments' do
is_expected.to eq(pipeline.environments)
@@ -2455,6 +2498,7 @@ describe MergeRequest do
describe "#diff_refs" do
context "with diffs" do
subject { create(:merge_request, :with_diffs) }
+
let(:expected_diff_refs) do
Gitlab::Diff::DiffRefs.new(
base_sha: subject.merge_request_diff.base_commit_sha,
@@ -2527,32 +2571,32 @@ describe MergeRequest do
describe '#merge_ongoing?' do
it 'returns true when the merge request is locked' do
- merge_request = build_stubbed(:merge_request, state: :locked)
+ merge_request = build_stubbed(:merge_request, state_id: described_class.available_states[:locked])
expect(merge_request.merge_ongoing?).to be(true)
end
it 'returns true when merge_id, MR is not merged and it has no running job' do
- merge_request = build_stubbed(:merge_request, state: :open, merge_jid: 'foo')
+ merge_request = build_stubbed(:merge_request, state_id: described_class.available_states[:opened], merge_jid: 'foo')
allow(Gitlab::SidekiqStatus).to receive(:running?).with('foo') { true }
expect(merge_request.merge_ongoing?).to be(true)
end
it 'returns false when merge_jid is nil' do
- merge_request = build_stubbed(:merge_request, state: :open, merge_jid: nil)
+ merge_request = build_stubbed(:merge_request, state_id: described_class.available_states[:opened], merge_jid: nil)
expect(merge_request.merge_ongoing?).to be(false)
end
it 'returns false if MR is merged' do
- merge_request = build_stubbed(:merge_request, state: :merged, merge_jid: 'foo')
+ merge_request = build_stubbed(:merge_request, state_id: described_class.available_states[:merged], merge_jid: 'foo')
expect(merge_request.merge_ongoing?).to be(false)
end
it 'returns false if there is no merge job running' do
- merge_request = build_stubbed(:merge_request, state: :open, merge_jid: 'foo')
+ merge_request = build_stubbed(:merge_request, state_id: described_class.available_states[:opened], merge_jid: 'foo')
allow(Gitlab::SidekiqStatus).to receive(:running?).with('foo') { false }
expect(merge_request.merge_ongoing?).to be(false)
@@ -2686,7 +2730,7 @@ describe MergeRequest do
context 'closed MR' do
before do
- merge_request.update_attribute(:state, :closed)
+ merge_request.update_attribute(:state_id, described_class.available_states[:closed])
end
it 'is not mergeable' do
@@ -2800,6 +2844,7 @@ describe MergeRequest do
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') }
@@ -2830,6 +2875,7 @@ describe MergeRequest do
describe '#version_params_for' do
subject { create(:merge_request, importing: true) }
+
let(:project) { subject.project }
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) }
@@ -3257,4 +3303,40 @@ describe MergeRequest do
end
end
end
+
+ describe '.with_open_merge_when_pipeline_succeeds' do
+ let!(:project) { create(:project) }
+ let!(:fork) { fork_project(project) }
+ let!(:merge_request1) do
+ create(:merge_request,
+ :merge_when_pipeline_succeeds,
+ target_project: project,
+ target_branch: 'master',
+ source_project: project,
+ source_branch: 'feature-1')
+ end
+
+ let!(:merge_request2) do
+ create(:merge_request,
+ :merge_when_pipeline_succeeds,
+ target_project: project,
+ target_branch: 'master',
+ source_project: fork,
+ source_branch: 'fork-feature-1')
+ end
+
+ let!(:merge_request4) do
+ create(:merge_request,
+ target_project: project,
+ target_branch: 'master',
+ source_project: fork,
+ source_branch: 'fork-feature-2')
+ end
+
+ let(:query) { described_class.with_open_merge_when_pipeline_succeeds }
+
+ it { expect(query).to contain_exactly(merge_request1, merge_request2) }
+ end
+
+ it_behaves_like 'versioned description'
end
diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb
index 972f26ac745..1e06d0fd7b9 100644
--- a/spec/models/namespace_spec.rb
+++ b/spec/models/namespace_spec.rb
@@ -191,6 +191,16 @@ describe Namespace do
end
end
+ describe '.find_by_pages_host' do
+ it 'finds namespace by GitLab Pages host and is case-insensitive' do
+ namespace = create(:namespace, name: 'topnamespace')
+ create(:namespace, name: 'annother_namespace')
+ host = "TopNamespace.#{Settings.pages.host.upcase}"
+
+ expect(described_class.find_by_pages_host(host)).to eq(namespace)
+ end
+ end
+
describe '#ancestors_upto' do
let(:parent) { create(:group) }
let(:child) { create(:group, parent: parent) }
@@ -240,7 +250,7 @@ describe Namespace do
it "moves dir if path changed" do
namespace.update(path: namespace.full_path + '_new')
- expect(gitlab_shell.exists?(project.repository_storage, "#{namespace.path}/#{project.path}.git")).to be_truthy
+ expect(gitlab_shell.repository_exists?(project.repository_storage, "#{namespace.path}/#{project.path}.git")).to be_truthy
end
context 'when #write_projects_repository_config raises an error' do
@@ -348,7 +358,7 @@ describe Namespace do
namespace.update(path: namespace.full_path + '_new')
expect(before_disk_path).to eq(project.disk_path)
- expect(gitlab_shell.exists?(project.repository_storage, "#{project.disk_path}.git")).to be_truthy
+ expect(gitlab_shell.repository_exists?(project.repository_storage, "#{project.disk_path}.git")).to be_truthy
end
end
@@ -886,31 +896,110 @@ describe Namespace do
end
end
end
+ end
- context 'when :emails_disabled feature flag is off' do
- before do
- stub_feature_flags(emails_disabled: false)
- end
+ describe '#pages_virtual_domain' do
+ let(:project) { create(:project, namespace: namespace) }
+
+ context 'when there are pages deployed for the project' do
+ context 'but pages metadata is not migrated' do
+ before do
+ generic_commit_status = create(:generic_commit_status, :success, stage: 'deploy', name: 'pages:deploy')
+ generic_commit_status.update!(project: project)
+ project.pages_metadatum.destroy!
+ end
+
+ it 'migrates pages metadata and returns the virual domain' do
+ virtual_domain = namespace.pages_virtual_domain
- context 'when not a subgroup' do
- it 'returns false' do
- group = create(:group, emails_disabled: true)
+ expect(project.reload.pages_metadatum.deployed).to eq(true)
- expect(group.emails_disabled?).to be_falsey
+ expect(virtual_domain).to be_an_instance_of(Pages::VirtualDomain)
+ expect(virtual_domain.lookup_paths).not_to be_empty
end
end
- context 'when a subgroup and ancestor emails are disabled' do
- let(:grandparent) { create(:group) }
- let(:parent) { create(:group, parent: grandparent) }
- let(:group) { create(:group, parent: parent) }
+ context 'and pages metadata is migrated' do
+ before do
+ project.mark_pages_as_deployed
+ end
- it 'returns false' do
- grandparent.update_attribute(:emails_disabled, true)
+ it 'returns the virual domain' do
+ virtual_domain = namespace.pages_virtual_domain
- expect(group.emails_disabled?).to be_falsey
+ expect(virtual_domain).to be_an_instance_of(Pages::VirtualDomain)
+ expect(virtual_domain.lookup_paths).not_to be_empty
end
end
end
end
+
+ describe '#has_parent?' do
+ it 'returns true when the group has a parent' do
+ group = create(:group, :nested)
+
+ expect(group.has_parent?).to be_truthy
+ end
+
+ it 'returns true when the group has an unsaved parent' do
+ parent = build(:group)
+ group = build(:group, parent: parent)
+
+ expect(group.has_parent?).to be_truthy
+ end
+
+ it 'returns false when the group has no parent' do
+ group = create(:group, parent: nil)
+
+ expect(group.has_parent?).to be_falsy
+ end
+ end
+
+ describe '#closest_setting' do
+ using RSpec::Parameterized::TableSyntax
+
+ shared_examples_for 'fetching closest setting' do
+ let!(:root_namespace) { create(:namespace) }
+ let!(:namespace) { create(:namespace, parent: root_namespace) }
+
+ let(:setting) { namespace.closest_setting(setting_name) }
+
+ before do
+ root_namespace.update_attribute(setting_name, root_setting)
+ namespace.update_attribute(setting_name, child_setting)
+ end
+
+ it 'returns closest non-nil value' do
+ expect(setting).to eq(result)
+ end
+ end
+
+ context 'when setting is of non-boolean type' do
+ where(:root_setting, :child_setting, :result) do
+ 100 | 200 | 200
+ 100 | nil | 100
+ nil | nil | nil
+ end
+
+ with_them do
+ let(:setting_name) { :max_artifacts_size }
+
+ it_behaves_like 'fetching closest setting'
+ end
+ end
+
+ context 'when setting is of boolean type' do
+ where(:root_setting, :child_setting, :result) do
+ true | false | false
+ true | nil | true
+ nil | nil | nil
+ end
+
+ with_them do
+ let(:setting_name) { :lfs_enabled }
+
+ it_behaves_like 'fetching closest setting'
+ end
+ end
+ end
end
diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb
index 66e3c6d5e9d..4c320b4b145 100644
--- a/spec/models/note_spec.rb
+++ b/spec/models/note_spec.rb
@@ -55,11 +55,13 @@ describe Note do
context 'when noteable and note project are the same' do
subject { create(:note) }
+
it { is_expected.to be_valid }
end
context 'when project is missing for a project related note' do
subject { build(:note, project: nil, noteable: build_stubbed(:issue)) }
+
it { is_expected.to be_invalid }
end
@@ -70,6 +72,37 @@ describe Note do
is_expected.to be_valid
end
end
+
+ describe 'max notes limit' do
+ let_it_be(:noteable) { create(:issue) }
+ let_it_be(:existing_note) { create(:note, project: noteable.project, noteable: noteable) }
+
+ before do
+ stub_const('Noteable::MAX_NOTES_LIMIT', 1)
+ end
+
+ context 'when creating a system note' do
+ subject { build(:system_note, project: noteable.project, noteable: noteable) }
+
+ it { is_expected.to be_valid }
+ end
+
+ context 'when creating a user note' do
+ subject { build(:note, project: noteable.project, noteable: noteable) }
+
+ it { is_expected.not_to be_valid }
+ end
+
+ context 'when updating an existing note on a noteable that already exceeds the limit' do
+ subject { existing_note }
+
+ before do
+ create(:system_note, project: noteable.project, noteable: noteable)
+ end
+
+ it { is_expected.to be_valid }
+ end
+ end
end
describe "Commit notes" do
@@ -710,6 +743,7 @@ describe Note do
describe '#to_discussion' do
subject { create(:discussion_note_on_merge_request) }
+
let!(:note2) { create(:discussion_note_on_merge_request, project: subject.project, noteable: subject.noteable, in_reply_to: subject) }
it "returns a discussion with just this note" do
@@ -777,6 +811,7 @@ describe Note do
context 'for a note' do
context 'when part of a discussion' do
subject { create(:discussion_note_on_issue) }
+
let(:note) { create(:discussion_note_on_issue, in_reply_to: subject) }
it 'checks if the note is in reply to the other discussion' do
@@ -790,6 +825,7 @@ describe Note do
context 'when not part of a discussion' do
subject { create(:note) }
+
let(:note) { create(:note, in_reply_to: subject) }
it 'checks if the note is in reply to the other noteable' do
@@ -804,6 +840,7 @@ describe Note do
context 'for a discussion' do
context 'when part of the same discussion' do
subject { create(:diff_note_on_merge_request) }
+
let(:note) { create(:diff_note_on_merge_request, in_reply_to: subject) }
it 'returns true' do
@@ -813,6 +850,7 @@ describe Note do
context 'when not part of the same discussion' do
subject { create(:diff_note_on_merge_request) }
+
let(:note) { create(:diff_note_on_merge_request) }
it 'returns false' do
@@ -824,6 +862,7 @@ describe Note do
context 'for a noteable' do
context 'when a comment on the same noteable' do
subject { create(:note) }
+
let(:note) { create(:note, in_reply_to: subject) }
it 'returns true' do
@@ -833,6 +872,7 @@ describe Note do
context 'when not a comment on the same noteable' do
subject { create(:note) }
+
let(:note) { create(:note) }
it 'returns false' do
@@ -856,6 +896,7 @@ describe Note do
context 'when not part of a discussion' do
subject { create(:note) }
+
let(:note) { create(:note, in_reply_to: subject) }
it 'returns the noteable' do
@@ -941,13 +982,64 @@ describe Note do
project = create(:project)
note = create(:note_on_issue, project: project)
- expect(note.parent).to eq(project)
+ expect(note.resource_parent).to eq(project)
end
it 'returns nil for personal snippet note' do
note = create(:note_on_personal_snippet)
- expect(note.parent).to be_nil
+ expect(note.resource_parent).to be_nil
+ end
+ end
+
+ describe 'scopes' do
+ let_it_be(:note1) { create(:note, note: 'Test 345') }
+ let_it_be(:note2) { create(:note, note: 'Test 789') }
+
+ describe '#for_note_or_capitalized_note' do
+ it 'returns the expected matching note' do
+ notes = described_class.for_note_or_capitalized_note('Test 345')
+
+ expect(notes.count).to eq(1)
+ expect(notes.first.id).to eq(note1.id)
+ end
+
+ it 'returns the expected capitalized note' do
+ notes = described_class.for_note_or_capitalized_note('test 345')
+
+ expect(notes.count).to eq(1)
+ expect(notes.first.id).to eq(note1.id)
+ end
+
+ it 'does not support pattern matching' do
+ notes = described_class.for_note_or_capitalized_note('test%')
+
+ expect(notes.count).to eq(0)
+ end
+ end
+
+ describe '#like_note_or_capitalized_note' do
+ it 'returns the expected matching note' do
+ notes = described_class.like_note_or_capitalized_note('Test 345')
+
+ expect(notes.count).to eq(1)
+ expect(notes.first.id).to eq(note1.id)
+ end
+
+ it 'returns the expected capitalized note' do
+ notes = described_class.like_note_or_capitalized_note('test 345')
+
+ expect(notes.count).to eq(1)
+ expect(notes.first.id).to eq(note1.id)
+ end
+
+ it 'supports pattern matching' do
+ notes = described_class.like_note_or_capitalized_note('test%')
+
+ expect(notes.count).to eq(2)
+ expect(notes.first.id).to eq(note1.id)
+ expect(notes.second.id).to eq(note2.id)
+ end
end
end
end
diff --git a/spec/models/notification_setting_spec.rb b/spec/models/notification_setting_spec.rb
index 820d233dbdc..094c60e3e09 100644
--- a/spec/models/notification_setting_spec.rb
+++ b/spec/models/notification_setting_spec.rb
@@ -98,6 +98,7 @@ RSpec.describe NotificationSetting do
it 'returns email events' do
expect(subject).to include(
+ :new_release,
:new_note,
:new_issue,
:reopen_issue,
diff --git a/spec/models/pages/lookup_path_spec.rb b/spec/models/pages/lookup_path_spec.rb
index 2146b0c9abd..c05d4c82634 100644
--- a/spec/models/pages/lookup_path_spec.rb
+++ b/spec/models/pages/lookup_path_spec.rb
@@ -57,8 +57,18 @@ describe Pages::LookupPath do
end
describe '#prefix' do
- it 'returns "/"' do
+ it 'returns "/" for pages group root projects' do
+ project = instance_double(Project, pages_group_root?: true)
+ lookup_path = described_class.new(project, trim_prefix: 'mygroup')
+
expect(lookup_path.prefix).to eq('/')
end
+
+ it 'returns the project full path with the provided prefix removed' do
+ project = instance_double(Project, pages_group_root?: false, full_path: 'mygroup/myproject')
+ lookup_path = described_class.new(project, trim_prefix: 'mygroup')
+
+ expect(lookup_path.prefix).to eq('/myproject/')
+ end
end
end
diff --git a/spec/models/pages/virtual_domain_spec.rb b/spec/models/pages/virtual_domain_spec.rb
index eaa57b7acd6..a5310738482 100644
--- a/spec/models/pages/virtual_domain_spec.rb
+++ b/spec/models/pages/virtual_domain_spec.rb
@@ -25,19 +25,33 @@ describe Pages::VirtualDomain do
end
describe '#lookup_paths' do
- let(:domain) { instance_double(PagesDomain) }
let(:project_a) { instance_double(Project) }
let(:project_z) { instance_double(Project) }
let(:pages_lookup_path_a) { instance_double(Pages::LookupPath, prefix: 'aaa') }
let(:pages_lookup_path_z) { instance_double(Pages::LookupPath, prefix: 'zzz') }
- subject(:virtual_domain) { described_class.new([project_a, project_z], domain: domain) }
+ context 'when there is pages domain provided' do
+ let(:domain) { instance_double(PagesDomain) }
- it 'returns collection of projects pages lookup paths sorted by prefix in reverse' do
- expect(project_a).to receive(:pages_lookup_path).with(domain: domain).and_return(pages_lookup_path_a)
- expect(project_z).to receive(:pages_lookup_path).with(domain: domain).and_return(pages_lookup_path_z)
+ subject(:virtual_domain) { described_class.new([project_a, project_z], domain: domain) }
- expect(virtual_domain.lookup_paths).to eq([pages_lookup_path_z, pages_lookup_path_a])
+ it 'returns collection of projects pages lookup paths sorted by prefix in reverse' do
+ expect(project_a).to receive(:pages_lookup_path).with(domain: domain, trim_prefix: nil).and_return(pages_lookup_path_a)
+ expect(project_z).to receive(:pages_lookup_path).with(domain: domain, trim_prefix: nil).and_return(pages_lookup_path_z)
+
+ expect(virtual_domain.lookup_paths).to eq([pages_lookup_path_z, pages_lookup_path_a])
+ end
+ end
+
+ context 'when there is trim_prefix provided' do
+ subject(:virtual_domain) { described_class.new([project_a, project_z], trim_prefix: 'group/') }
+
+ it 'returns collection of projects pages lookup paths sorted by prefix in reverse' do
+ expect(project_a).to receive(:pages_lookup_path).with(trim_prefix: 'group/', domain: nil).and_return(pages_lookup_path_a)
+ expect(project_z).to receive(:pages_lookup_path).with(trim_prefix: 'group/', domain: nil).and_return(pages_lookup_path_z)
+
+ expect(virtual_domain.lookup_paths).to eq([pages_lookup_path_z, pages_lookup_path_a])
+ end
end
end
end
diff --git a/spec/models/pages_domain_spec.rb b/spec/models/pages_domain_spec.rb
index 89a837dc812..4b65bf032d1 100644
--- a/spec/models/pages_domain_spec.rb
+++ b/spec/models/pages_domain_spec.rb
@@ -160,7 +160,7 @@ describe PagesDomain do
end
context 'when curve is set explicitly by parameters' do
- it 'adds errors to private key', :quarantine do
+ it 'adds errors to private key' do
domain = build(:pages_domain, :explicit_ecdsa)
expect(domain).to be_invalid
@@ -293,11 +293,13 @@ describe PagesDomain do
describe "#https?" do
context "when a certificate is present" do
subject { build(:pages_domain) }
+
it { is_expected.to be_https }
end
context "when no certificate is present" do
subject { build(:pages_domain, :without_certificate) }
+
it { is_expected.not_to be_https }
end
end
@@ -557,15 +559,35 @@ describe PagesDomain do
end
end
- describe '.pages_virtual_domain' do
- let(:project) { build(:project) }
+ describe '#pages_virtual_domain' do
+ let(:project) { create(:project) }
+ let(:pages_domain) { create(:pages_domain, project: project) }
+
+ context 'when there are no pages deployed for the project' do
+ it 'returns nil' do
+ expect(pages_domain.pages_virtual_domain).to be_nil
+ end
+ end
+
+ context 'when there are pages deployed for the project' do
+ before do
+ generic_commit_status = create(:generic_commit_status, :success, stage: 'deploy', name: 'pages:deploy')
+ generic_commit_status.update!(project: project)
+ project.pages_metadatum.destroy!
+ project.reload
+ end
- subject(:pages_domain) { build(:pages_domain, project: project) }
+ it 'returns the virual domain' do
+ expect(Pages::VirtualDomain).to receive(:new).with([project], domain: pages_domain).and_call_original
- it 'returns instance of Pages::VirtualDomain' do
- expect(Pages::VirtualDomain).to receive(:new).with([project], domain: pages_domain).and_call_original
+ expect(pages_domain.pages_virtual_domain).to be_an_instance_of(Pages::VirtualDomain)
+ end
- expect(pages_domain.pages_virtual_domain).to be_a(Pages::VirtualDomain)
+ it 'migrates project pages metadata' do
+ expect { pages_domain.pages_virtual_domain }.to change {
+ project.reload.pages_metadatum&.deployed
+ }.from(nil).to(true)
+ end
end
end
end
diff --git a/spec/models/project_services/bugzilla_service_spec.rb b/spec/models/project_services/bugzilla_service_spec.rb
index 66481a461ca..d0ab5afc765 100644
--- a/spec/models/project_services/bugzilla_service_spec.rb
+++ b/spec/models/project_services/bugzilla_service_spec.rb
@@ -41,7 +41,7 @@ describe BugzillaService do
{ project_url: url, issues_url: url, new_issue_url: url }
end
- # this will be removed as part of https://gitlab.com/gitlab-org/gitlab-foss/issues/63084
+ # this will be removed as part of https://gitlab.com/gitlab-org/gitlab/issues/29404
context 'when data are stored in properties' do
let(:properties) { access_params.merge(title: title, description: description) }
let(:service) do
diff --git a/spec/models/project_services/custom_issue_tracker_service_spec.rb b/spec/models/project_services/custom_issue_tracker_service_spec.rb
index 50bf15cfc8c..e749ea6eacc 100644
--- a/spec/models/project_services/custom_issue_tracker_service_spec.rb
+++ b/spec/models/project_services/custom_issue_tracker_service_spec.rb
@@ -55,7 +55,7 @@ describe CustomIssueTrackerService do
{ project_url: url, issues_url: url, new_issue_url: url }
end
- # this will be removed as part of https://gitlab.com/gitlab-org/gitlab-foss/issues/63084
+ # this will be removed as part of https://gitlab.com/gitlab-org/gitlab/issues/29404
context 'when data are stored in properties' do
let(:properties) { access_params.merge(title: title, description: description) }
let(:service) do
diff --git a/spec/models/project_services/gitlab_issue_tracker_service_spec.rb b/spec/models/project_services/gitlab_issue_tracker_service_spec.rb
index 2dc0b67239c..defebcee9c6 100644
--- a/spec/models/project_services/gitlab_issue_tracker_service_spec.rb
+++ b/spec/models/project_services/gitlab_issue_tracker_service_spec.rb
@@ -58,7 +58,7 @@ describe GitlabIssueTrackerService do
{ project_url: url, issues_url: url, new_issue_url: url }
end
- # this will be removed as part of https://gitlab.com/gitlab-org/gitlab-foss/issues/63084
+ # this will be removed as part of https://gitlab.com/gitlab-org/gitlab/issues/29404
context 'when data are stored in properties' do
let(:properties) { access_params.merge(title: title, description: description) }
let(:service) do
diff --git a/spec/models/project_services/jira_service_spec.rb b/spec/models/project_services/jira_service_spec.rb
index b86ab8959a2..5feb8ca7839 100644
--- a/spec/models/project_services/jira_service_spec.rb
+++ b/spec/models/project_services/jira_service_spec.rb
@@ -15,26 +15,53 @@ describe JiraService do
let(:transition_id) { 'test27' }
describe '#options' do
- let(:service) do
- described_class.create(
+ let(:options) do
+ {
project: create(:project),
active: true,
username: 'username',
password: 'test',
jira_issue_transition_id: 24,
url: 'http://jira.test.com/path/'
- )
+ }
end
+ let(:service) { described_class.create(options) }
+
it 'sets the URL properly' do
- # jira-ruby gem parses the URI and handles trailing slashes
- # fine: https://github.com/sumoheavy/jira-ruby/blob/v1.4.1/lib/jira/http_client.rb#L59
+ # jira-ruby gem parses the URI and handles trailing slashes fine:
+ # https://github.com/sumoheavy/jira-ruby/blob/v1.7.0/lib/jira/http_client.rb#L62
expect(service.options[:site]).to eq('http://jira.test.com/')
end
it 'leaves out trailing slashes in context' do
expect(service.options[:context_path]).to eq('/path')
end
+
+ context 'username with trailing whitespaces' do
+ before do
+ options.merge!(username: 'username ')
+ end
+
+ it 'leaves out trailing whitespaces in username' do
+ expect(service.options[:username]).to eq('username')
+ end
+ end
+
+ it 'provides additional cookies to allow basic auth with oracle webgate' do
+ expect(service.options[:use_cookies]).to eq(true)
+ expect(service.options[:additional_cookies]).to eq(['OBBasicAuth=fromDialog'])
+ end
+
+ context 'using api URL' do
+ before do
+ options.merge!(api_url: 'http://jira.test.com/api_path/')
+ end
+
+ it 'leaves out trailing slashes in context' do
+ expect(service.options[:context_path]).to eq('/api_path')
+ end
+ end
end
describe 'Associations' do
@@ -68,7 +95,7 @@ describe JiraService do
expect(subject.properties).to be_nil
end
- it 'sets title correctly' do
+ it 'sets title correctly' do
service = subject
expect(service.title).to eq('custom title')
@@ -93,7 +120,7 @@ describe JiraService do
end
# we need to make sure we are able to read both from properties and jira_tracker_data table
- # TODO: change this as part of #63084
+ # TODO: change this as part of https://gitlab.com/gitlab-org/gitlab/issues/29404
context 'overriding properties' do
let(:access_params) do
{ url: url, api_url: api_url, username: username, password: password,
@@ -278,11 +305,11 @@ describe JiraService do
end
end
- # this will be removed as part of https://gitlab.com/gitlab-org/gitlab-foss/issues/63084
+ # this will be removed as part of https://gitlab.com/gitlab-org/gitlab/issues/29404
context 'when data are stored in properties' do
let(:properties) { data_params.merge(title: title, description: description) }
let!(:service) do
- create(:jira_service, :without_properties_callback, properties: properties)
+ create(:jira_service, :without_properties_callback, properties: properties.merge(additional: 'something'))
end
it_behaves_like 'issue tracker fields'
@@ -604,26 +631,6 @@ describe JiraService do
end
end
- describe 'additional cookies' do
- let(:project) { create(:project) }
-
- context 'provides additional cookies to allow basic auth with oracle webgate' do
- before do
- @service = project.create_jira_service(
- active: true, properties: { url: 'http://jira.com' })
- end
-
- after do
- @service.destroy!
- end
-
- it 'is initialized' do
- expect(@service.options[:use_cookies]).to eq(true)
- expect(@service.options[:additional_cookies]).to eq(['OBBasicAuth=fromDialog'])
- end
- end
- end
-
describe 'project and issue urls' do
context 'when gitlab.yml was initialized' do
it 'is prepopulated with the settings' do
@@ -650,7 +657,7 @@ describe JiraService do
end
end
- describe 'favicon urls', :request_store do
+ describe 'favicon urls' do
it 'includes the standard favicon' do
props = described_class.new.send(:build_remote_link_props, url: 'http://example.com', title: 'title')
expect(props[:object][:icon][:url16x16]).to match %r{^http://localhost/assets/favicon(?:-\h+).png$}
diff --git a/spec/models/project_services/redmine_service_spec.rb b/spec/models/project_services/redmine_service_spec.rb
index 2339c5a8421..6220d7b1fac 100644
--- a/spec/models/project_services/redmine_service_spec.rb
+++ b/spec/models/project_services/redmine_service_spec.rb
@@ -57,7 +57,7 @@ describe RedmineService do
{ project_url: url, issues_url: url, new_issue_url: url }
end
- # this will be removed as part of https://gitlab.com/gitlab-org/gitlab-foss/issues/63084
+ # this will be removed as part of https://gitlab.com/gitlab-org/gitlab/issues/29404
context 'when data are stored in properties' do
let(:properties) { access_params.merge(title: title, description: description) }
let(:service) do
diff --git a/spec/models/project_services/youtrack_service_spec.rb b/spec/models/project_services/youtrack_service_spec.rb
index fe608baf16b..19d4cb95315 100644
--- a/spec/models/project_services/youtrack_service_spec.rb
+++ b/spec/models/project_services/youtrack_service_spec.rb
@@ -45,7 +45,7 @@ describe YoutrackService do
{ project_url: url, issues_url: url, new_issue_url: url }
end
- # this will be removed as part of https://gitlab.com/gitlab-org/gitlab-foss/issues/63084
+ # this will be removed as part of https://gitlab.com/gitlab-org/gitlab/issues/29404
context 'when data are stored in properties' do
let(:properties) { access_params.merge(title: title, description: description) }
let(:service) do
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 7fe48e66def..9f3313e67b5 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -92,6 +92,7 @@ describe Project do
it { is_expected.to have_many(:pipeline_schedules) }
it { is_expected.to have_many(:members_and_requesters) }
it { is_expected.to have_many(:clusters) }
+ it { is_expected.to have_many(:management_clusters).class_name('Clusters::Cluster') }
it { is_expected.to have_many(:kubernetes_namespaces) }
it { is_expected.to have_many(:custom_attributes).class_name('ProjectCustomAttribute') }
it { is_expected.to have_many(:project_badges).class_name('ProjectBadge') }
@@ -100,6 +101,8 @@ describe Project do
it { is_expected.to have_many(:deploy_tokens).through(:project_deploy_tokens) }
it { is_expected.to have_many(:cycle_analytics_stages) }
it { is_expected.to have_many(:external_pull_requests) }
+ it { is_expected.to have_many(:sourced_pipelines) }
+ it { is_expected.to have_many(:source_pipelines) }
it 'has an inverse relationship with merge requests' do
expect(described_class.reflect_on_association(:merge_requests).has_inverse?).to eq(:target_project)
@@ -132,6 +135,13 @@ describe Project do
expect(project.ci_cd_settings).to be_an_instance_of(ProjectCiCdSetting)
expect(project.ci_cd_settings).to be_persisted
end
+
+ it 'automatically creates a Pages metadata row' do
+ project = create(:project)
+
+ expect(project.pages_metadatum).to be_an_instance_of(ProjectPagesMetadatum)
+ expect(project.pages_metadatum).to be_persisted
+ end
end
context 'updating cd_cd_settings' do
@@ -143,7 +153,7 @@ describe Project do
end
describe '#members & #requesters' do
- let(:project) { create(:project, :public, :access_requestable) }
+ let(:project) { create(:project, :public) }
let(:requester) { create(:user) }
let(:developer) { create(:user) }
before do
@@ -621,8 +631,38 @@ describe Project do
describe "#web_url" do
let(:project) { create(:project, path: "somewhere") }
- it 'returns the full web URL for this repo' do
- expect(project.web_url).to eq("#{Gitlab.config.gitlab.url}/#{project.namespace.full_path}/somewhere")
+ context 'when given the only_path option' do
+ subject { project.web_url(only_path: only_path) }
+
+ context 'when only_path is false' do
+ let(:only_path) { false }
+
+ it 'returns the full web URL for this repo' do
+ expect(subject).to eq("#{Gitlab.config.gitlab.url}/#{project.namespace.full_path}/somewhere")
+ end
+ end
+
+ context 'when only_path is true' do
+ let(:only_path) { true }
+
+ it 'returns the relative web URL for this repo' do
+ expect(subject).to eq("/#{project.namespace.full_path}/somewhere")
+ end
+ end
+
+ context 'when only_path is nil' do
+ let(:only_path) { nil }
+
+ it 'returns the full web URL for this repo' do
+ expect(subject).to eq("#{Gitlab.config.gitlab.url}/#{project.namespace.full_path}/somewhere")
+ end
+ end
+ end
+
+ context 'when not given the only_path option' do
+ it 'returns the full web URL for this repo' do
+ expect(project.web_url).to eq("#{Gitlab.config.gitlab.url}/#{project.namespace.full_path}/somewhere")
+ end
end
end
@@ -2380,29 +2420,6 @@ describe Project do
expect(project.emails_disabled?).to be_truthy
end
end
-
- context 'when :emails_disabled feature flag is off' do
- before do
- stub_feature_flags(emails_disabled: false)
- end
-
- context 'emails disabled in group' do
- it 'returns false' do
- allow(project.namespace).to receive(:emails_disabled?) { true }
-
- expect(project.emails_disabled?).to be_falsey
- end
- end
-
- context 'emails enabled in group' do
- it 'returns false' do
- allow(project.namespace).to receive(:emails_disabled?) { false }
- project.update_attribute(:emails_disabled, true)
-
- expect(project.emails_disabled?).to be_falsey
- end
- end
- end
end
describe '#lfs_enabled?' do
@@ -3239,20 +3256,78 @@ describe Project do
describe '#http_url_to_repo' do
let(:project) { create(:project) }
- it 'returns the url to the repo without a username' do
- expect(project.http_url_to_repo).to eq("#{project.web_url}.git")
- expect(project.http_url_to_repo).not_to include('@')
+ context 'when a custom HTTP clone URL root is not set' do
+ it 'returns the url to the repo without a username' do
+ expect(project.http_url_to_repo).to eq("#{project.web_url}.git")
+ expect(project.http_url_to_repo).not_to include('@')
+ end
+ end
+
+ context 'when a custom HTTP clone URL root is set' do
+ before do
+ stub_application_setting(custom_http_clone_url_root: custom_http_clone_url_root)
+ end
+
+ context 'when custom HTTP clone URL root has a relative URL root' do
+ context 'when custom HTTP clone URL root ends with a slash' do
+ let(:custom_http_clone_url_root) { 'https://git.example.com:51234/mygitlab/' }
+
+ it 'returns the url to the repo, with the root replaced with the custom one' do
+ expect(project.http_url_to_repo).to eq("https://git.example.com:51234/mygitlab/#{project.full_path}.git")
+ end
+ end
+
+ context 'when custom HTTP clone URL root does not end with a slash' do
+ let(:custom_http_clone_url_root) { 'https://git.example.com:51234/mygitlab' }
+
+ it 'returns the url to the repo, with the root replaced with the custom one' do
+ expect(project.http_url_to_repo).to eq("https://git.example.com:51234/mygitlab/#{project.full_path}.git")
+ end
+ end
+ end
+
+ context 'when custom HTTP clone URL root does not have a relative URL root' do
+ context 'when custom HTTP clone URL root ends with a slash' do
+ let(:custom_http_clone_url_root) { 'https://git.example.com:51234/' }
+
+ it 'returns the url to the repo, with the root replaced with the custom one' do
+ expect(project.http_url_to_repo).to eq("https://git.example.com:51234/#{project.full_path}.git")
+ end
+ end
+
+ context 'when custom HTTP clone URL root does not end with a slash' do
+ let(:custom_http_clone_url_root) { 'https://git.example.com:51234' }
+
+ it 'returns the url to the repo, with the root replaced with the custom one' do
+ expect(project.http_url_to_repo).to eq("https://git.example.com:51234/#{project.full_path}.git")
+ end
+ end
+ end
end
end
describe '#lfs_http_url_to_repo' do
let(:project) { create(:project) }
- it 'returns the url to the repo without a username' do
- lfs_http_url_to_repo = project.lfs_http_url_to_repo('operation_that_doesnt_matter')
+ context 'when a custom HTTP clone URL root is not set' do
+ it 'returns the url to the repo without a username' do
+ lfs_http_url_to_repo = project.lfs_http_url_to_repo('operation_that_doesnt_matter')
+
+ expect(lfs_http_url_to_repo).to eq("#{project.web_url}.git")
+ expect(lfs_http_url_to_repo).not_to include('@')
+ end
+ end
+
+ context 'when a custom HTTP clone URL root is set' do
+ before do
+ stub_application_setting(custom_http_clone_url_root: 'https://git.example.com:51234')
+ end
+
+ it 'returns the url to the repo, with the root replaced with the custom one' do
+ lfs_http_url_to_repo = project.lfs_http_url_to_repo('operation_that_doesnt_matter')
- expect(lfs_http_url_to_repo).to eq("#{project.web_url}.git")
- expect(lfs_http_url_to_repo).not_to include('@')
+ expect(lfs_http_url_to_repo).to eq("https://git.example.com:51234/#{project.full_path}.git")
+ end
end
end
@@ -3556,7 +3631,8 @@ describe Project do
end
describe '#remove_pages' do
- let(:project) { create(:project) }
+ let(:project) { create(:project).tap { |project| project.mark_pages_as_deployed } }
+ let(:pages_metadatum) { project.pages_metadatum }
let(:namespace) { project.namespace }
let(:pages_path) { project.pages_path }
@@ -3569,12 +3645,12 @@ describe Project do
end
end
- it 'removes the pages directory' do
+ it 'removes the pages directory and marks the project as not having pages deployed' do
expect_any_instance_of(Projects::UpdatePagesConfigurationService).to receive(:execute)
expect_any_instance_of(Gitlab::PagesTransfer).to receive(:rename_project).and_return(true)
expect(PagesWorker).to receive(:perform_in).with(5.minutes, :remove, namespace.full_path, anything)
- project.remove_pages
+ expect { project.remove_pages }.to change { pages_metadatum.reload.deployed }.from(true).to(false)
end
it 'is a no-op when there is no namespace' do
@@ -3584,13 +3660,13 @@ describe Project do
expect_any_instance_of(Projects::UpdatePagesConfigurationService).not_to receive(:execute)
expect_any_instance_of(Gitlab::PagesTransfer).not_to receive(:rename_project)
- project.remove_pages
+ expect { project.remove_pages }.not_to change { pages_metadatum.reload.deployed }
end
it 'is run when the project is destroyed' do
expect(project).to receive(:remove_pages).and_call_original
- project.destroy
+ expect { project.destroy }.not_to raise_error
end
end
@@ -3663,14 +3739,6 @@ describe Project do
end
end
- describe '#ensure_storage_path_exists' do
- it 'delegates to gitlab_shell to ensure namespace is created' do
- expect(gitlab_shell).to receive(:add_namespace).with(project.repository_storage, project.base_dir)
-
- project.ensure_storage_path_exists
- end
- end
-
describe '#legacy_storage?' do
it 'returns true when storage_version is nil' do
project = build(:project, storage_version: nil)
@@ -3785,16 +3853,6 @@ describe Project do
end
end
- describe '#ensure_storage_path_exists' do
- it 'delegates to gitlab_shell to ensure namespace is created' do
- allow(project).to receive(:gitlab_shell).and_return(gitlab_shell)
-
- expect(gitlab_shell).to receive(:add_namespace).with(project.repository_storage, hashed_prefix)
-
- project.ensure_storage_path_exists
- end
- end
-
describe '#pages_path' do
it 'returns a path where pages are stored' do
expect(project.pages_path).to eq(File.join(Settings.pages.path, project.namespace.full_path, project.path))
@@ -4225,13 +4283,24 @@ describe Project do
end
describe '#check_repository_path_availability' do
- let(:project) { build(:project) }
+ let(:project) { build(:project, :repository, :legacy_storage) }
+ subject { project.check_repository_path_availability }
- it 'skips gitlab-shell exists?' do
- project.skip_disk_validation = true
+ context 'when the repository already exists' do
+ let(:project) { create(:project, :repository, :legacy_storage) }
- expect(project.gitlab_shell).not_to receive(:exists?)
- expect(project.check_repository_path_availability).to be_truthy
+ it { is_expected.to be_falsey }
+ end
+
+ context 'when the repository does not exist' do
+ it { is_expected.to be_truthy }
+
+ it 'skips gitlab-shell exists?' do
+ project.skip_disk_validation = true
+
+ expect(project.gitlab_shell).not_to receive(:repository_exists?)
+ is_expected.to be_truthy
+ end
end
end
@@ -4945,6 +5014,7 @@ describe Project do
describe '#git_objects_poolable?' do
subject { project }
+
context 'when not using hashed storage' do
let(:project) { create(:project, :legacy_storage, :public, :repository) }
@@ -5044,6 +5114,35 @@ describe Project do
end
end
+ context 'pages deployed' do
+ let(:project) { create(:project) }
+
+ {
+ mark_pages_as_deployed: true,
+ mark_pages_as_not_deployed: false
+ }.each do |method_name, flag|
+ describe method_name do
+ it "creates new record and sets deployed to #{flag} if none exists yet" do
+ project.pages_metadatum.destroy!
+ project.reload
+
+ project.send(method_name)
+
+ expect(project.pages_metadatum.reload.deployed).to eq(flag)
+ end
+
+ it "updates the existing record and sets deployed to #{flag}" do
+ pages_metadatum = project.pages_metadatum
+ pages_metadatum.update!(deployed: !flag)
+
+ expect { project.send(method_name) }.to change {
+ pages_metadatum.reload.deployed
+ }.from(!flag).to(flag)
+ end
+ end
+ end
+ end
+
describe '#has_pool_repsitory?' do
it 'returns false when it does not have a pool repository' do
subject = create(:project, :repository)
@@ -5084,9 +5183,146 @@ describe Project do
let(:project) { build(:project) }
it 'returns instance of Pages::LookupPath' do
- expect(Pages::LookupPath).to receive(:new).with(project, domain: pages_domain).and_call_original
+ expect(Pages::LookupPath).to receive(:new).with(project, domain: pages_domain, trim_prefix: 'mygroup').and_call_original
+
+ expect(project.pages_lookup_path(domain: pages_domain, trim_prefix: 'mygroup')).to be_a(Pages::LookupPath)
+ end
+ end
+
+ describe '.with_pages_deployed' do
+ it 'returns only projects that have pages deployed' do
+ _project_without_pages = create(:project)
+ project_with_pages = create(:project)
+ project_with_pages.mark_pages_as_deployed
+
+ expect(described_class.with_pages_deployed).to contain_exactly(project_with_pages)
+ end
+ end
+
+ describe '.pages_metadata_not_migrated' do
+ it 'returns only projects that have pages deployed' do
+ _project_with_pages_metadata_migrated = create(:project)
+ project_with_pages_metadata_not_migrated = create(:project)
+ project_with_pages_metadata_not_migrated.pages_metadatum.destroy!
+
+ expect(described_class.pages_metadata_not_migrated).to contain_exactly(project_with_pages_metadata_not_migrated)
+ end
+ end
+
+ describe '#pages_group_root?' do
+ it 'returns returns true if pages_url is same as pages_group_url' do
+ project = build(:project)
+ expect(project).to receive(:pages_url).and_return(project.pages_group_url)
+
+ expect(project.pages_group_root?).to eq(true)
+ end
+
+ it 'returns returns false if pages_url is different than pages_group_url' do
+ project = build(:project)
+
+ expect(project.pages_group_root?).to eq(false)
+ end
+ end
+
+ describe '#closest_setting' do
+ using RSpec::Parameterized::TableSyntax
+
+ shared_examples_for 'fetching closest setting' do
+ let!(:namespace) { create(:namespace) }
+ let!(:project) { create(:project, namespace: namespace) }
+
+ let(:setting_name) { :some_setting }
+ let(:setting) { project.closest_setting(setting_name) }
- expect(project.pages_lookup_path(domain: pages_domain)).to be_a(Pages::LookupPath)
+ before do
+ allow(project).to receive(:read_attribute).with(setting_name).and_return(project_setting)
+ allow(namespace).to receive(:closest_setting).with(setting_name).and_return(group_setting)
+ allow(Gitlab::CurrentSettings).to receive(setting_name).and_return(global_setting)
+ end
+
+ it 'returns closest non-nil value' do
+ expect(setting).to eq(result)
+ end
+ end
+
+ context 'when setting is of non-boolean type' do
+ where(:global_setting, :group_setting, :project_setting, :result) do
+ 100 | 200 | 300 | 300
+ 100 | 200 | nil | 200
+ 100 | nil | nil | 100
+ nil | nil | nil | nil
+ end
+
+ with_them do
+ it_behaves_like 'fetching closest setting'
+ end
+ end
+
+ context 'when setting is of boolean type' do
+ where(:global_setting, :group_setting, :project_setting, :result) do
+ true | true | false | false
+ true | false | nil | false
+ true | nil | nil | true
+ end
+
+ with_them do
+ it_behaves_like 'fetching closest setting'
+ end
+ end
+ end
+
+ describe '#drop_visibility_level!' do
+ context 'when has a group' do
+ let(:group) { create(:group, visibility_level: group_visibility_level) }
+ let(:project) { build(:project, namespace: group, visibility_level: project_visibility_level) }
+
+ context 'when the group `visibility_level` is more strict' do
+ let(:group_visibility_level) { Gitlab::VisibilityLevel::PRIVATE }
+ let(:project_visibility_level) { Gitlab::VisibilityLevel::INTERNAL }
+
+ it 'sets `visibility_level` value from the group' do
+ expect { project.drop_visibility_level! }
+ .to change { project.visibility_level }
+ .to(Gitlab::VisibilityLevel::PRIVATE)
+ end
+ end
+
+ context 'when the group `visibility_level` is less strict' do
+ let(:group_visibility_level) { Gitlab::VisibilityLevel::INTERNAL }
+ let(:project_visibility_level) { Gitlab::VisibilityLevel::PRIVATE }
+
+ it 'does not change the value of the `visibility_level` field' do
+ expect { project.drop_visibility_level! }
+ .not_to change { project.visibility_level }
+ end
+ end
+ end
+
+ context 'when `restricted_visibility_levels` of the GitLab instance exist' do
+ before do
+ stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::INTERNAL])
+ end
+
+ let(:project) { build(:project, visibility_level: project_visibility_level) }
+
+ context 'when `visibility_level` is included into `restricted_visibility_levels`' do
+ let(:project_visibility_level) { Gitlab::VisibilityLevel::INTERNAL }
+
+ it 'sets `visibility_level` value to `PRIVATE`' do
+ expect { project.drop_visibility_level! }
+ .to change { project.visibility_level }
+ .to(Gitlab::VisibilityLevel::PRIVATE)
+ end
+ end
+
+ context 'when `restricted_visibility_levels` does not include `visibility_level`' do
+ let(:project_visibility_level) { Gitlab::VisibilityLevel::PUBLIC }
+
+ it 'does not change the value of the `visibility_level` field' do
+ expect { project.drop_visibility_level! }
+ .to not_change { project.visibility_level }
+ end
+ end
end
end
diff --git a/spec/models/project_team_spec.rb b/spec/models/project_team_spec.rb
index 77c88a04cde..d62fa58739a 100644
--- a/spec/models/project_team_spec.rb
+++ b/spec/models/project_team_spec.rb
@@ -141,7 +141,7 @@ describe ProjectTeam do
describe '#find_member' do
context 'personal project' do
let(:project) do
- create(:project, :public, :access_requestable)
+ create(:project, :public)
end
let(:requester) { create(:user) }
@@ -161,7 +161,7 @@ describe ProjectTeam do
end
context 'group project' do
- let(:group) { create(:group, :access_requestable) }
+ let(:group) { create(:group) }
let(:project) { create(:project, group: group) }
let(:requester) { create(:user) }
@@ -246,7 +246,7 @@ describe ProjectTeam do
context 'personal project' do
let(:project) do
- create(:project, :public, :access_requestable)
+ create(:project, :public)
end
context 'when project is not shared with group' do
@@ -292,7 +292,7 @@ describe ProjectTeam do
end
context 'group project' do
- let(:group) { create(:group, :access_requestable) }
+ let(:group) { create(:group) }
let!(:project) do
create(:project, group: group)
end
diff --git a/spec/models/project_wiki_spec.rb b/spec/models/project_wiki_spec.rb
index d12dd97bb9e..31d1d1fd7d1 100644
--- a/spec/models/project_wiki_spec.rb
+++ b/spec/models/project_wiki_spec.rb
@@ -47,11 +47,25 @@ describe ProjectWiki do
describe "#http_url_to_repo" do
let(:project) { create :project }
- it 'returns the full http url to the repo' do
- expected_url = "#{Gitlab.config.gitlab.url}/#{subject.full_path}.git"
+ context 'when a custom HTTP clone URL root is not set' do
+ it 'returns the full http url to the repo' do
+ expected_url = "#{Gitlab.config.gitlab.url}/#{subject.full_path}.git"
- expect(project_wiki.http_url_to_repo).to eq(expected_url)
- expect(project_wiki.http_url_to_repo).not_to include('@')
+ expect(project_wiki.http_url_to_repo).to eq(expected_url)
+ expect(project_wiki.http_url_to_repo).not_to include('@')
+ end
+ end
+
+ context 'when a custom HTTP clone URL root is set' do
+ before do
+ stub_application_setting(custom_http_clone_url_root: 'https://git.example.com:51234')
+ end
+
+ it 'returns the full http url to the repo, with the root replaced with the custom one' do
+ expected_url = "https://git.example.com:51234/#{subject.full_path}.git"
+
+ expect(project_wiki.http_url_to_repo).to eq(expected_url)
+ end
end
end
@@ -95,6 +109,7 @@ describe ProjectWiki do
context "when the wiki repository is empty" do
describe '#empty?' do
subject { super().empty? }
+
it { is_expected.to be_truthy }
end
end
@@ -107,6 +122,7 @@ describe ProjectWiki do
describe '#empty?' do
subject { super().empty? }
+
it { is_expected.to be_falsey }
it 'only instantiates a Wiki page once' do
diff --git a/spec/models/release_spec.rb b/spec/models/release_spec.rb
index 8714c67f29d..0aac325c2b2 100644
--- a/spec/models/release_spec.rb
+++ b/spec/models/release_spec.rb
@@ -15,12 +15,13 @@ RSpec.describe Release do
it { is_expected.to have_many(:links).class_name('Releases::Link') }
it { is_expected.to have_many(:milestones) }
it { is_expected.to have_many(:milestone_releases) }
+ it { is_expected.to have_one(:evidence) }
end
describe 'validation' do
it { is_expected.to validate_presence_of(:project) }
it { is_expected.to validate_presence_of(:description) }
- it { is_expected.to validate_presence_of(:name) }
+ it { is_expected.to validate_presence_of(:tag) }
context 'when a release exists in the database without a name' do
it 'does not require name' do
@@ -90,4 +91,42 @@ RSpec.describe Release do
end
end
end
+
+ describe 'evidence' do
+ describe '#create_evidence!' do
+ context 'when a release is created' do
+ it 'creates one Evidence object too' do
+ expect { release }.to change(Evidence, :count).by(1)
+ end
+ end
+ end
+
+ context 'when a release is deleted' do
+ it 'also deletes the associated evidence' do
+ release = create(:release)
+
+ expect { release.destroy }.to change(Evidence, :count).by(-1)
+ end
+ end
+ end
+
+ describe '#notify_new_release' do
+ context 'when a release is created' do
+ it 'instantiates NewReleaseWorker to send notifications' do
+ expect(NewReleaseWorker).to receive(:perform_async)
+
+ create(:release)
+ end
+ end
+
+ context 'when a release is updated' do
+ let!(:release) { create(:release) }
+
+ it 'does not send any new notification' do
+ expect(NewReleaseWorker).not_to receive(:perform_async)
+
+ release.update!(description: 'new description')
+ end
+ end
+ end
end
diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb
index 6dc47e0e501..cf9100eb6cf 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -279,7 +279,7 @@ describe Repository do
describe '#commits' do
context 'when neither the all flag nor a ref are specified' do
it 'returns every commit from default branch' do
- expect(repository.commits(limit: 60).size).to eq(37)
+ expect(repository.commits(nil, limit: 60).size).to eq(37)
end
end
@@ -320,7 +320,7 @@ describe Repository do
context "when 'all' flag is set" do
it 'returns every commit from the repository' do
- expect(repository.commits(all: true, limit: 60).size).to eq(60)
+ expect(repository.commits(nil, all: true, limit: 60).size).to eq(60)
end
end
end
@@ -1075,7 +1075,7 @@ describe Repository do
let(:ref) { 'refs/heads/master' }
it 'returns nil' do
- is_expected.to eq(nil)
+ is_expected.to be_nil
end
end
@@ -1193,66 +1193,84 @@ describe Repository do
end
describe '#has_visible_content?' do
- before do
- # If raw_repository.has_visible_content? gets called more than once then
- # caching is broken. We don't want that.
+ it 'delegates to raw_repository when true' do
expect(repository.raw_repository).to receive(:has_visible_content?)
- .once
- .and_return(result)
- end
-
- context 'when true' do
- let(:result) { true }
+ .and_return(true)
- it 'returns true and caches it' do
- expect(repository.has_visible_content?).to eq(true)
- # Second call hits the cache
- expect(repository.has_visible_content?).to eq(true)
- end
+ expect(repository.has_visible_content?).to eq(true)
end
- context 'when false' do
- let(:result) { false }
+ it 'delegates to raw_repository when false' do
+ expect(repository.raw_repository).to receive(:has_visible_content?)
+ .and_return(false)
- it 'returns false and caches it' do
- expect(repository.has_visible_content?).to eq(false)
- # Second call hits the cache
- expect(repository.has_visible_content?).to eq(false)
- end
+ expect(repository.has_visible_content?).to eq(false)
end
+
+ it_behaves_like 'asymmetric cached method', :has_visible_content?
end
describe '#branch_exists?' do
- it 'uses branch_names' do
- allow(repository).to receive(:branch_names).and_return(['foobar'])
+ let(:branch) { repository.root_ref }
+
+ subject { repository.branch_exists?(branch) }
+
+ it 'delegates to branch_names when the cache is empty' do
+ repository.expire_branches_cache
+
+ expect(repository).to receive(:branch_names).and_call_original
+ is_expected.to eq(true)
+ end
+
+ it 'uses redis set caching when the cache is filled' do
+ repository.branch_names # ensure the branch name cache is filled
+
+ expect(repository)
+ .to receive(:branch_names_include?)
+ .with(branch)
+ .and_call_original
- expect(repository.branch_exists?('foobar')).to eq(true)
- expect(repository.branch_exists?('master')).to eq(false)
+ is_expected.to eq(true)
end
end
describe '#tag_exists?' do
- it 'uses tag_names' do
- allow(repository).to receive(:tag_names).and_return(['foobar'])
+ let(:tag) { repository.tags.first.name }
- expect(repository.tag_exists?('foobar')).to eq(true)
- expect(repository.tag_exists?('master')).to eq(false)
+ subject { repository.tag_exists?(tag) }
+
+ it 'delegates to tag_names when the cache is empty' do
+ repository.expire_tags_cache
+
+ expect(repository).to receive(:tag_names).and_call_original
+ is_expected.to eq(true)
+ end
+
+ it 'uses redis set caching when the cache is filled' do
+ repository.tag_names # ensure the tag name cache is filled
+
+ expect(repository)
+ .to receive(:tag_names_include?)
+ .with(tag)
+ .and_call_original
+
+ is_expected.to eq(true)
end
end
- describe '#branch_names', :use_clean_rails_memory_store_caching do
+ describe '#branch_names', :clean_gitlab_redis_cache do
let(:fake_branch_names) { ['foobar'] }
it 'gets cached across Repository instances' do
allow(repository.raw_repository).to receive(:branch_names).once.and_return(fake_branch_names)
- expect(repository.branch_names).to eq(fake_branch_names)
+ expect(repository.branch_names).to match_array(fake_branch_names)
fresh_repository = Project.find(project.id).repository
expect(fresh_repository.object_id).not_to eq(repository.object_id)
expect(fresh_repository.raw_repository).not_to receive(:branch_names)
- expect(fresh_repository.branch_names).to eq(fake_branch_names)
+ expect(fresh_repository.branch_names).to match_array(fake_branch_names)
end
end
@@ -1972,7 +1990,7 @@ describe Repository do
it 'returns nil if repo does not exist' do
allow(repository).to receive(:root_ref).and_raise(Gitlab::Git::Repository::NoRepository)
- expect(repository.avatar).to eq(nil)
+ expect(repository.avatar).to be_nil
end
it 'returns the first avatar file found in the repository' do
@@ -2574,6 +2592,10 @@ describe Repository do
expect { repository.create_if_not_exists }.to change { repository.exists? }.from(false).to(true)
end
+ it 'returns true' do
+ expect(repository.create_if_not_exists).to eq(true)
+ end
+
it 'calls out to the repository client to create a repo' do
expect(repository.raw.gitaly_repository_client).to receive(:create_repository)
@@ -2588,6 +2610,10 @@ describe Repository do
repository.create_if_not_exists
end
+
+ it 'returns nil' do
+ expect(repository.create_if_not_exists).to be_nil
+ end
end
context 'when the repository exists but the cache is not up to date' do
@@ -2599,6 +2625,10 @@ describe Repository do
expect { repository.create_if_not_exists }.not_to raise_error
end
+
+ it 'returns nil' do
+ expect(repository.create_if_not_exists).to be_nil
+ end
end
end
diff --git a/spec/models/resource_label_event_spec.rb b/spec/models/resource_label_event_spec.rb
index f4023dcb95a..f51041c9ddc 100644
--- a/spec/models/resource_label_event_spec.rb
+++ b/spec/models/resource_label_event_spec.rb
@@ -4,6 +4,7 @@ require 'spec_helper'
RSpec.describe ResourceLabelEvent, type: :model do
subject { build(:resource_label_event, issue: issue) }
+
let(:issue) { create(:issue) }
let(:merge_request) { create(:merge_request) }
diff --git a/spec/models/service_spec.rb b/spec/models/service_spec.rb
index d96e1398677..64077b76f01 100644
--- a/spec/models/service_spec.rb
+++ b/spec/models/service_spec.rb
@@ -78,10 +78,11 @@ describe Service do
end
describe "Template" do
+ let(:project) { create(:project) }
+
describe '.build_from_template' do
context 'when template is invalid' do
it 'sets service template to inactive when template is invalid' do
- project = create(:project)
template = build(:prometheus_service, template: true, active: true, properties: {})
template.save(validate: false)
@@ -91,6 +92,64 @@ describe Service do
expect(service.active).to be false
end
end
+
+ describe 'build issue tracker from a template' do
+ let(:title) { 'custom title' }
+ let(:description) { 'custom description' }
+ let(:url) { 'http://jira.example.com' }
+ let(:api_url) { 'http://api-jira.example.com' }
+ let(:username) { 'jira-username' }
+ let(:password) { 'jira-password' }
+ let(:data_params) do
+ {
+ url: url, api_url: api_url,
+ username: username, password: password
+ }
+ end
+
+ shared_examples 'service creation from a template' do
+ it 'creates a correct service' do
+ service = described_class.build_from_template(project.id, template)
+
+ expect(service).to be_active
+ expect(service.title).to eq(title)
+ expect(service.description).to eq(description)
+ expect(service.url).to eq(url)
+ expect(service.api_url).to eq(api_url)
+ expect(service.username).to eq(username)
+ expect(service.password).to eq(password)
+ end
+ end
+
+ # this will be removed as part of https://gitlab.com/gitlab-org/gitlab/issues/29404
+ context 'when data are stored in properties' do
+ let(:properties) { data_params.merge(title: title, description: description) }
+ let!(:template) do
+ create(:jira_service, :without_properties_callback, template: true, properties: properties.merge(additional: 'something'))
+ end
+
+ it_behaves_like 'service creation from a template'
+ end
+
+ context 'when data are stored in separated fields' do
+ let(:template) do
+ create(:jira_service, data_params.merge(properties: {}, title: title, description: description, template: true))
+ end
+
+ it_behaves_like 'service creation from a template'
+ end
+
+ context 'when data are stored in both properties and separated fields' do
+ let(:properties) { data_params.merge(title: title, description: description) }
+ let(:template) do
+ create(:jira_service, :without_properties_callback, active: true, template: true, properties: properties).tap do |service|
+ create(:jira_tracker_data, data_params.merge(service: service))
+ end
+ end
+
+ it_behaves_like 'service creation from a template'
+ end
+ end
end
describe "for pushover service" do
@@ -104,7 +163,6 @@ describe Service do
api_key: '123456789'
})
end
- let(:project) { create(:project) }
describe 'is prefilled for projects pushover service' do
it "has all fields prefilled" do
diff --git a/spec/models/snippet_spec.rb b/spec/models/snippet_spec.rb
index 3524cdae3b8..f4dcbfbc190 100644
--- a/spec/models/snippet_spec.rb
+++ b/spec/models/snippet_spec.rb
@@ -133,6 +133,32 @@ describe Snippet do
end
end
+ describe 'when default snippet visibility set to internal' do
+ using RSpec::Parameterized::TableSyntax
+
+ before do
+ stub_application_setting(default_snippet_visibility: Gitlab::VisibilityLevel::INTERNAL)
+ end
+
+ where(:attribute_name, :value) do
+ :visibility | 'private'
+ :visibility_level | Gitlab::VisibilityLevel::PRIVATE
+ 'visibility' | 'private'
+ 'visibility_level' | Gitlab::VisibilityLevel::PRIVATE
+ end
+
+ with_them do
+ it 'sets the visibility level' do
+ snippet = described_class.new(attribute_name => value, title: 'test', file_name: 'test.rb', content: 'test data')
+
+ expect(snippet.visibility_level).to eq(Gitlab::VisibilityLevel::PRIVATE)
+ expect(snippet.title).to eq('test')
+ expect(snippet.file_name).to eq('test.rb')
+ expect(snippet.content).to eq('test data')
+ end
+ end
+ end
+
describe '.with_optional_visibility' do
context 'when a visibility level is provided' do
it 'returns snippets with the given visibility' do
@@ -157,12 +183,12 @@ describe Snippet do
end
end
- describe '.only_global_snippets' do
+ describe '.only_personal_snippets' do
it 'returns snippets not associated with any projects' do
create(:project_snippet)
snippet = create(:snippet)
- snippets = described_class.only_global_snippets
+ snippets = described_class.only_personal_snippets
expect(snippets).to eq([snippet])
end
diff --git a/spec/models/suggestion_spec.rb b/spec/models/suggestion_spec.rb
index 8d4e9070b19..2ac3ae0a5ad 100644
--- a/spec/models/suggestion_spec.rb
+++ b/spec/models/suggestion_spec.rb
@@ -38,16 +38,6 @@ describe Suggestion do
end
describe '#appliable?' do
- context 'when note does not support suggestions' do
- it 'returns false' do
- expect_next_instance_of(DiffNote) do |note|
- allow(note).to receive(:supports_suggestion?) { false }
- end
-
- expect(suggestion).not_to be_appliable
- end
- end
-
context 'when patch is already applied' do
let(:suggestion) { create(:suggestion, :applied) }
diff --git a/spec/models/system_note_metadata_spec.rb b/spec/models/system_note_metadata_spec.rb
index bcd3c03f947..801f139355b 100644
--- a/spec/models/system_note_metadata_spec.rb
+++ b/spec/models/system_note_metadata_spec.rb
@@ -5,6 +5,7 @@ require 'spec_helper'
describe SystemNoteMetadata do
describe 'associations' do
it { is_expected.to belong_to(:note) }
+ it { is_expected.to belong_to(:description_version) }
end
describe 'validation' do
diff --git a/spec/models/timelog_spec.rb b/spec/models/timelog_spec.rb
index 28fc82f2a32..7321a458817 100644
--- a/spec/models/timelog_spec.rb
+++ b/spec/models/timelog_spec.rb
@@ -4,6 +4,7 @@ require 'spec_helper'
RSpec.describe Timelog do
subject { build(:timelog) }
+
let(:issue) { create(:issue) }
let(:merge_request) { create(:merge_request) }
diff --git a/spec/models/todo_spec.rb b/spec/models/todo_spec.rb
index c2566ccd047..487a1c619c6 100644
--- a/spec/models/todo_spec.rb
+++ b/spec/models/todo_spec.rb
@@ -253,14 +253,14 @@ describe Todo do
end
end
- describe '.for_group_and_descendants' do
+ describe '.for_group_ids_and_descendants' do
it 'returns the todos for a group and its descendants' do
parent_group = create(:group)
child_group = create(:group, parent: parent_group)
todo1 = create(:todo, group: parent_group)
todo2 = create(:todo, group: child_group)
- todos = described_class.for_group_and_descendants(parent_group)
+ todos = described_class.for_group_ids_and_descendants([parent_group.id])
expect(todos).to contain_exactly(todo1, todo2)
end
diff --git a/spec/models/upload_spec.rb b/spec/models/upload_spec.rb
index d97bb8cfb90..03434c95218 100644
--- a/spec/models/upload_spec.rb
+++ b/spec/models/upload_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
describe Upload do
- describe 'assocations' do
+ describe 'associations' do
it { is_expected.to belong_to(:model) }
end
@@ -107,6 +107,52 @@ describe Upload do
end
end
+ describe '#build_uploader' do
+ it 'returns a uploader object with current upload associated with it' do
+ subject = build(:upload)
+ uploader = subject.build_uploader
+
+ expect(uploader.upload).to eq(subject)
+ expect(uploader.mounted_as).to eq(subject.send(:mount_point))
+ expect(uploader.file).to be_nil
+ end
+ end
+
+ describe '#retrieve_uploader' do
+ it 'returns a uploader object with current uploader associated with and cache retrieved' do
+ subject = build(:upload)
+ uploader = subject.retrieve_uploader
+
+ expect(uploader.upload).to eq(subject)
+ expect(uploader.mounted_as).to eq(subject.send(:mount_point))
+ expect(uploader.file).not_to be_nil
+ end
+ end
+
+ describe '#needs_checksum?' do
+ context 'with local storage' do
+ it 'returns true when no checksum exists' do
+ subject = create(:upload, :with_file, checksum: nil)
+
+ expect(subject.needs_checksum?).to be_truthy
+ end
+
+ it 'returns false when checksum is already present' do
+ subject = create(:upload, :with_file, checksum: 'something')
+
+ expect(subject.needs_checksum?).to be_falsey
+ end
+ end
+
+ context 'with remote storage' do
+ subject { build(:upload, :object_storage) }
+
+ it 'returns false' do
+ expect(subject.needs_checksum?).to be_falsey
+ end
+ end
+ end
+
describe '#exist?' do
it 'returns true when the file exists' do
upload = described_class.new(path: __FILE__, store: ObjectStorage::Store::LOCAL)
diff --git a/spec/models/uploads/fog_spec.rb b/spec/models/uploads/fog_spec.rb
index 4a44cf5ab0f..b93d9449da9 100644
--- a/spec/models/uploads/fog_spec.rb
+++ b/spec/models/uploads/fog_spec.rb
@@ -44,7 +44,7 @@ describe Uploads::Fog do
subject { data_store.delete_keys(keys) }
before do
- uploads.each { |upload| upload.build_uploader.migrate!(2) }
+ uploads.each { |upload| upload.retrieve_uploader.migrate!(2) }
end
it 'deletes multiple data' do
diff --git a/spec/models/user_interacted_project_spec.rb b/spec/models/user_interacted_project_spec.rb
index 47d919c1d12..b96ff08e22d 100644
--- a/spec/models/user_interacted_project_spec.rb
+++ b/spec/models/user_interacted_project_spec.rb
@@ -5,6 +5,7 @@ require 'spec_helper'
describe UserInteractedProject do
describe '.track' do
subject { described_class.track(event) }
+
let(:event) { build(:event) }
Event::ACTIONS.each do |action|
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index a701b858783..8eb2f9b5bc0 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -79,7 +79,7 @@ describe User do
describe '#group_members' do
it 'does not include group memberships for which user is a requester' do
user = create(:user)
- group = create(:group, :public, :access_requestable)
+ group = create(:group, :public)
group.request_access(user)
expect(user.group_members).to be_empty
@@ -89,7 +89,7 @@ describe User do
describe '#project_members' do
it 'does not include project memberships for which user is a requester' do
user = create(:user)
- project = create(:project, :public, :access_requestable)
+ project = create(:project, :public)
project.request_access(user)
expect(user.project_members).to be_empty
@@ -1120,6 +1120,30 @@ describe User do
end
end
+ describe 'deactivating a user' do
+ let(:user) { create(:user, name: 'John Smith') }
+
+ context "an active user" do
+ it "can be deactivated" do
+ user.deactivate
+
+ expect(user.deactivated?).to be_truthy
+ end
+ end
+
+ context "a user who is blocked" do
+ before do
+ user.block
+ end
+
+ it "cannot be deactivated" do
+ user.deactivate
+
+ expect(user.reload.deactivated?).to be_falsy
+ end
+ end
+ end
+
describe '.filter_items' do
let(:user) { double }
@@ -1141,6 +1165,12 @@ describe User do
expect(described_class.filter_items('blocked')).to include user
end
+ it 'filters by deactivated' do
+ expect(described_class).to receive(:deactivated).and_return([user])
+
+ expect(described_class.filter_items('deactivated')).to include user
+ end
+
it 'filters by two_factor_disabled' do
expect(described_class).to receive(:without_two_factor).and_return([user])
@@ -1161,7 +1191,7 @@ describe User do
end
describe '.without_projects' do
- let!(:project) { create(:project, :public, :access_requestable) }
+ let!(:project) { create(:project, :public) }
let!(:user) { create(:user) }
let!(:user_without_project) { create(:user) }
let!(:user_without_project2) { create(:user) }
@@ -1427,10 +1457,18 @@ describe User do
expect(described_class.search(user.username)).to eq([user, user2])
end
+ it 'returns users with a matching username starting with a @' do
+ expect(described_class.search("@#{user.username}")).to eq([user, user2])
+ end
+
it 'returns users with a partially matching username' do
expect(described_class.search(user.username[0..2])).to eq([user, user2])
end
+ it 'returns users with a partially matching username starting with @' do
+ expect(described_class.search("@#{user.username[0..2]}")).to eq([user, user2])
+ end
+
it 'returns users with a matching username regardless of the casing' do
expect(described_class.search(user2.username.upcase)).to eq([user2])
end
@@ -1516,15 +1554,22 @@ describe User do
end
describe '.find_by_ssh_key_id' do
- context 'using an existing SSH key ID' do
- let(:user) { create(:user) }
- let(:key) { create(:key, user: user) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:key) { create(:key, user: user) }
+ context 'using an existing SSH key ID' do
it 'returns the corresponding User' do
expect(described_class.find_by_ssh_key_id(key.id)).to eq(user)
end
end
+ it 'only performs a single query' do
+ key # Don't count the queries for creating the key and user
+
+ expect { described_class.find_by_ssh_key_id(key.id) }
+ .not_to exceed_query_limit(1)
+ end
+
context 'using an invalid SSH key ID' do
it 'returns nil' do
expect(described_class.find_by_ssh_key_id(-1)).to be_nil
@@ -2034,8 +2079,98 @@ describe User do
end
end
+ describe "#last_active_at" do
+ let(:last_activity_on) { 5.days.ago.to_date }
+ let(:current_sign_in_at) { 8.days.ago }
+
+ context 'for a user that has `last_activity_on` set' do
+ let(:user) { create(:user, last_activity_on: last_activity_on) }
+
+ it 'returns `last_activity_on` with current time zone' do
+ expect(user.last_active_at).to eq(last_activity_on.to_time.in_time_zone)
+ end
+ end
+
+ context 'for a user that has `current_sign_in_at` set' do
+ let(:user) { create(:user, current_sign_in_at: current_sign_in_at) }
+
+ it 'returns `current_sign_in_at`' do
+ expect(user.last_active_at).to eq(current_sign_in_at)
+ end
+ end
+
+ context 'for a user that has both `current_sign_in_at` & ``last_activity_on`` set' do
+ let(:user) { create(:user, current_sign_in_at: current_sign_in_at, last_activity_on: last_activity_on) }
+
+ it 'returns the latest among `current_sign_in_at` & `last_activity_on`' do
+ latest_event = [current_sign_in_at, last_activity_on.to_time.in_time_zone].max
+ expect(user.last_active_at).to eq(latest_event)
+ end
+ end
+
+ context 'for a user that does not have both `current_sign_in_at` & `last_activity_on` set' do
+ let(:user) { create(:user, current_sign_in_at: nil, last_activity_on: nil) }
+
+ it 'returns nil' do
+ expect(user.last_active_at).to eq(nil)
+ end
+ end
+ end
+
+ describe "#can_be_deactivated?" do
+ let(:activity) { {} }
+ let(:user) { create(:user, name: 'John Smith', **activity) }
+ let(:day_within_minium_inactive_days_threshold) { User::MINIMUM_INACTIVE_DAYS.pred.days.ago }
+ let(:day_outside_minium_inactive_days_threshold) { User::MINIMUM_INACTIVE_DAYS.next.days.ago }
+
+ shared_examples 'not eligible for deactivation' do
+ it 'returns false' do
+ expect(user.can_be_deactivated?).to be_falsey
+ end
+ end
+
+ shared_examples 'eligible for deactivation' do
+ it 'returns true' do
+ expect(user.can_be_deactivated?).to be_truthy
+ end
+ end
+
+ context "a user who is not active" do
+ before do
+ user.block
+ end
+
+ it_behaves_like 'not eligible for deactivation'
+ end
+
+ context 'a user who has activity within the specified minimum inactive days' do
+ let(:activity) { { last_activity_on: day_within_minium_inactive_days_threshold } }
+
+ it_behaves_like 'not eligible for deactivation'
+ end
+
+ context 'a user who has signed in within the specified minimum inactive days' do
+ let(:activity) { { current_sign_in_at: day_within_minium_inactive_days_threshold } }
+
+ it_behaves_like 'not eligible for deactivation'
+ end
+
+ context 'a user who has no activity within the specified minimum inactive days' do
+ let(:activity) { { last_activity_on: day_outside_minium_inactive_days_threshold } }
+
+ it_behaves_like 'eligible for deactivation'
+ end
+
+ context 'a user who has not signed in within the specified minimum inactive days' do
+ let(:activity) { { current_sign_in_at: day_outside_minium_inactive_days_threshold } }
+
+ it_behaves_like 'eligible for deactivation'
+ end
+ end
+
describe "#contributed_projects" do
subject { create(:user) }
+
let!(:project1) { create(:project) }
let!(:project2) { fork_project(project3) }
let!(:project3) { create(:project) }
@@ -3600,6 +3735,80 @@ describe User do
end
end
+ describe '#notification_settings_for' do
+ let(:user) { create(:user) }
+ let(:source) { nil }
+
+ subject { user.notification_settings_for(source) }
+
+ context 'when source is nil' do
+ it 'returns a blank global notification settings object' do
+ expect(subject.source).to eq(nil)
+ expect(subject.notification_email).to eq(nil)
+ expect(subject.level).to eq('global')
+ end
+ end
+
+ context 'when source is a Group' do
+ let(:group) { create(:group) }
+
+ subject { user.notification_settings_for(group, inherit: true) }
+
+ context 'when group has no existing notification settings' do
+ context 'when group has no ancestors' do
+ it 'will be a default Global notification setting' do
+ expect(subject.notification_email).to eq(nil)
+ expect(subject.level).to eq('global')
+ end
+ end
+
+ context 'when group has ancestors' do
+ context 'when an ancestor has a level other than Global' do
+ let(:ancestor) { create(:group) }
+ let(:group) { create(:group, parent: ancestor) }
+
+ before do
+ create(:notification_setting, user: user, source: ancestor, level: 'participating', notification_email: 'ancestor@example.com')
+ end
+
+ it 'has the same level set' do
+ expect(subject.level).to eq('participating')
+ end
+
+ it 'has the same email set' do
+ expect(subject.notification_email).to eq('ancestor@example.com')
+ end
+
+ context 'when inherit is false' do
+ subject { user.notification_settings_for(group) }
+
+ it 'does not inherit settings' do
+ expect(subject.notification_email).to eq(nil)
+ expect(subject.level).to eq('global')
+ end
+ end
+ end
+
+ context 'when an ancestor has a Global level but has an email set' do
+ let(:grand_ancestor) { create(:group) }
+ let(:ancestor) { create(:group, parent: grand_ancestor) }
+ let(:group) { create(:group, parent: ancestor) }
+
+ before do
+ create(:notification_setting, user: user, source: grand_ancestor, level: 'participating', notification_email: 'grand@example.com')
+ create(:notification_setting, user: user, source: ancestor, level: 'global', notification_email: 'ancestor@example.com')
+ end
+
+ it 'has the same email set' do
+ expect(subject.level).to eq('global')
+ expect(subject.notification_email).to eq('ancestor@example.com')
+ end
+ end
+ end
+ end
+ end
+ end
+
describe '#notification_email_for' do
let(:user) { create(:user) }
let(:group) { create(:group) }
@@ -3632,4 +3841,34 @@ describe User do
end
end
end
+
+ describe '#password_expired?' do
+ let(:user) { build(:user, password_expires_at: password_expires_at) }
+
+ subject { user.password_expired? }
+
+ context 'when password_expires_at is not set' do
+ let(:password_expires_at) {}
+
+ it 'returns false' do
+ is_expected.to be_falsey
+ end
+ end
+
+ context 'when password_expires_at is in the past' do
+ let(:password_expires_at) { 1.minute.ago }
+
+ it 'returns true' do
+ is_expected.to be_truthy
+ end
+ end
+
+ context 'when password_expires_at is in the future' do
+ let(:password_expires_at) { 1.minute.from_now }
+
+ it 'returns false' do
+ is_expected.to be_falsey
+ end
+ end
+ end
end
diff --git a/spec/policies/deploy_keys_project_policy_spec.rb b/spec/policies/deploy_keys_project_policy_spec.rb
new file mode 100644
index 00000000000..952da86b7a7
--- /dev/null
+++ b/spec/policies/deploy_keys_project_policy_spec.rb
@@ -0,0 +1,73 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe DeployKeysProjectPolicy do
+ subject { described_class.new(current_user, deploy_key.deploy_keys_project_for(project)) }
+
+ describe 'updating a deploy_keys_project' do
+ context 'when a project maintainer' do
+ let(:current_user) { create(:user) }
+
+ context 'tries to update private deploy key attached to project' do
+ let(:deploy_key) { create(:deploy_key, public: false) }
+ let(:project) { create(:project_empty_repo) }
+
+ before do
+ project.add_maintainer(current_user)
+ project.deploy_keys << deploy_key
+ end
+
+ it { is_expected.to be_disallowed(:update_deploy_keys_project) }
+ end
+
+ context 'tries to update public deploy key attached to project' do
+ let(:deploy_key) { create(:deploy_key, public: true) }
+ let(:project) { create(:project_empty_repo) }
+
+ before do
+ project.add_maintainer(current_user)
+ project.deploy_keys << deploy_key
+ end
+
+ it { is_expected.to be_allowed(:update_deploy_keys_project) }
+ end
+ end
+
+ context 'when a non-maintainer project member' do
+ let(:current_user) { create(:user) }
+ let(:project) { create(:project_empty_repo) }
+
+ before do
+ project.add_developer(current_user)
+ project.deploy_keys << deploy_key
+ end
+
+ context 'tries to update private deploy key attached to project' do
+ let(:deploy_key) { create(:deploy_key, public: false) }
+
+ it { is_expected.to be_disallowed(:update_deploy_keys_project) }
+ end
+
+ context 'tries to update public deploy key attached to project' do
+ let(:deploy_key) { create(:deploy_key, public: true) }
+
+ it { is_expected.to be_disallowed(:update_deploy_keys_project) }
+ end
+ end
+
+ context 'when a user is not a project member' do
+ let(:current_user) { create(:user) }
+ let(:project) { create(:project_empty_repo) }
+ let(:deploy_key) { create(:deploy_key, public: true) }
+
+ before do
+ project.deploy_keys << deploy_key
+ end
+
+ context 'tries to update public deploy key attached to project' do
+ it { is_expected.to be_disallowed(:update_deploy_keys_project) }
+ end
+ end
+ end
+end
diff --git a/spec/policies/global_policy_spec.rb b/spec/policies/global_policy_spec.rb
index df6cc526eb0..880f1bcbc05 100644
--- a/spec/policies/global_policy_spec.rb
+++ b/spec/policies/global_policy_spec.rb
@@ -141,6 +141,40 @@ describe GlobalPolicy do
end
end
+ describe 'receive notifications' do
+ describe 'regular user' do
+ it { is_expected.to be_allowed(:receive_notifications) }
+ end
+
+ describe 'admin' do
+ let(:current_user) { create(:admin) }
+
+ it { is_expected.to be_allowed(:receive_notifications) }
+ end
+
+ describe 'anonymous' do
+ let(:current_user) { nil }
+
+ it { is_expected.not_to be_allowed(:receive_notifications) }
+ end
+
+ describe 'blocked user' do
+ before do
+ current_user.block
+ end
+
+ it { is_expected.not_to be_allowed(:receive_notifications) }
+ end
+
+ describe 'deactivated user' do
+ before do
+ current_user.deactivate
+ end
+
+ it { is_expected.not_to be_allowed(:receive_notifications) }
+ end
+ end
+
describe 'git access' do
describe 'regular user' do
it { is_expected.to be_allowed(:access_git) }
@@ -158,6 +192,14 @@ describe GlobalPolicy do
it { is_expected.to be_allowed(:access_git) }
end
+ describe 'deactivated user' do
+ before do
+ current_user.deactivate
+ end
+
+ it { is_expected.not_to be_allowed(:access_git) }
+ end
+
context 'when terms are enforced' do
before do
enforce_terms
@@ -246,6 +288,14 @@ describe GlobalPolicy do
it { is_expected.not_to be_allowed(:use_slash_commands) }
end
+ context 'when deactivated' do
+ before do
+ current_user.deactivate
+ end
+
+ it { is_expected.not_to be_allowed(:use_slash_commands) }
+ end
+
context 'when access locked' do
before do
current_user.lock_access!
diff --git a/spec/policies/group_policy_spec.rb b/spec/policies/group_policy_spec.rb
index be55d94daec..603e7e874c9 100644
--- a/spec/policies/group_policy_spec.rb
+++ b/spec/policies/group_policy_spec.rb
@@ -3,12 +3,13 @@ require 'spec_helper'
describe GroupPolicy do
include_context 'GroupPolicy context'
- context 'with no user' do
+ context 'public group with no user' do
let(:group) { create(:group, :public) }
let(:current_user) { nil }
it do
expect_allowed(:read_group)
+ expect_allowed(*read_group_permissions)
expect_disallowed(:upload_file)
expect_disallowed(*reporter_permissions)
expect_disallowed(*developer_permissions)
@@ -27,11 +28,11 @@ describe GroupPolicy do
end
it { expect_disallowed(:read_group) }
+ it { expect_disallowed(*read_group_permissions) }
end
context 'with foreign user and public project' do
let(:project) { create(:project, :public) }
- let(:user) { create(:user) }
let(:current_user) { create(:user) }
before do
@@ -39,6 +40,7 @@ describe GroupPolicy do
end
it { expect_disallowed(:read_group) }
+ it { expect_disallowed(*read_group_permissions) }
end
context 'has projects' do
@@ -49,13 +51,13 @@ describe GroupPolicy do
project.add_developer(current_user)
end
- it { expect_allowed(:read_label, :read_list) }
+ it { expect_allowed(*read_group_permissions) }
context 'in subgroups' do
let(:subgroup) { create(:group, :private, parent: group) }
let(:project) { create(:project, namespace: subgroup) }
- it { expect_allowed(:read_label, :read_list) }
+ it { expect_allowed(*read_group_permissions) }
end
end
@@ -63,6 +65,7 @@ describe GroupPolicy do
let(:current_user) { guest }
it do
+ expect_allowed(*read_group_permissions)
expect_allowed(*guest_permissions)
expect_disallowed(*reporter_permissions)
expect_disallowed(*developer_permissions)
@@ -75,6 +78,7 @@ describe GroupPolicy do
let(:current_user) { reporter }
it do
+ expect_allowed(*read_group_permissions)
expect_allowed(*guest_permissions)
expect_allowed(*reporter_permissions)
expect_disallowed(*developer_permissions)
@@ -87,6 +91,7 @@ describe GroupPolicy do
let(:current_user) { developer }
it do
+ expect_allowed(*read_group_permissions)
expect_allowed(*guest_permissions)
expect_allowed(*reporter_permissions)
expect_allowed(*developer_permissions)
@@ -99,8 +104,8 @@ describe GroupPolicy do
let(:current_user) { maintainer }
context 'with subgroup_creation level set to maintainer' do
- let(:group) do
- create(:group, :private, subgroup_creation_level: ::Gitlab::Access::MAINTAINER_SUBGROUP_ACCESS)
+ before_all do
+ group.update(subgroup_creation_level: ::Gitlab::Access::MAINTAINER_SUBGROUP_ACCESS)
end
it 'allows every maintainer permission plus creating subgroups' do
@@ -110,6 +115,7 @@ describe GroupPolicy do
updated_owner_permissions =
owner_permissions - create_subgroup_permission
+ expect_allowed(*read_group_permissions)
expect_allowed(*guest_permissions)
expect_allowed(*reporter_permissions)
expect_allowed(*developer_permissions)
@@ -120,6 +126,7 @@ describe GroupPolicy do
context 'with subgroup_creation_level set to owner' do
it 'allows every maintainer permission' do
+ expect_allowed(*read_group_permissions)
expect_allowed(*guest_permissions)
expect_allowed(*reporter_permissions)
expect_allowed(*developer_permissions)
@@ -133,6 +140,7 @@ describe GroupPolicy do
let(:current_user) { owner }
it do
+ expect_allowed(*read_group_permissions)
expect_allowed(*guest_permissions)
expect_allowed(*reporter_permissions)
expect_allowed(*developer_permissions)
@@ -145,6 +153,7 @@ describe GroupPolicy do
let(:current_user) { admin }
it do
+ expect_allowed(*read_group_permissions)
expect_allowed(*guest_permissions)
expect_allowed(*reporter_permissions)
expect_allowed(*developer_permissions)
@@ -154,11 +163,11 @@ describe GroupPolicy do
end
describe 'private nested group use the highest access level from the group and inherited permissions' do
- let(:nested_group) do
+ let_it_be(:nested_group) do
create(:group, :private, :owner_subgroup_creation_only, parent: group)
end
- before do
+ before_all do
nested_group.add_guest(guest)
nested_group.add_guest(reporter)
nested_group.add_guest(developer)
@@ -176,6 +185,7 @@ describe GroupPolicy do
let(:current_user) { nil }
it do
+ expect_disallowed(*read_group_permissions)
expect_disallowed(*guest_permissions)
expect_disallowed(*reporter_permissions)
expect_disallowed(*developer_permissions)
@@ -188,6 +198,7 @@ describe GroupPolicy do
let(:current_user) { guest }
it do
+ expect_allowed(*read_group_permissions)
expect_allowed(*guest_permissions)
expect_disallowed(*reporter_permissions)
expect_disallowed(*developer_permissions)
@@ -200,6 +211,7 @@ describe GroupPolicy do
let(:current_user) { reporter }
it do
+ expect_allowed(*read_group_permissions)
expect_allowed(*guest_permissions)
expect_allowed(*reporter_permissions)
expect_disallowed(*developer_permissions)
@@ -212,6 +224,7 @@ describe GroupPolicy do
let(:current_user) { developer }
it do
+ expect_allowed(*read_group_permissions)
expect_allowed(*guest_permissions)
expect_allowed(*reporter_permissions)
expect_allowed(*developer_permissions)
@@ -224,6 +237,7 @@ describe GroupPolicy do
let(:current_user) { maintainer }
it do
+ expect_allowed(*read_group_permissions)
expect_allowed(*guest_permissions)
expect_allowed(*reporter_permissions)
expect_allowed(*developer_permissions)
@@ -236,6 +250,7 @@ describe GroupPolicy do
let(:current_user) { owner }
it do
+ expect_allowed(*read_group_permissions)
expect_allowed(*guest_permissions)
expect_allowed(*reporter_permissions)
expect_allowed(*developer_permissions)
@@ -252,6 +267,10 @@ describe GroupPolicy do
context 'when the group share_with_group_lock is enabled' do
let(:group) { create(:group, share_with_group_lock: true, parent: parent) }
+ before do
+ group.add_owner(owner)
+ end
+
context 'when the parent group share_with_group_lock is enabled' do
context 'when the group has a grandparent' do
let(:parent) { create(:group, share_with_group_lock: true, parent: grandparent) }
@@ -337,7 +356,9 @@ describe GroupPolicy do
context "create_projects" do
context 'when group has no project creation level set' do
- let(:group) { create(:group, project_creation_level: nil) }
+ before_all do
+ group.update(project_creation_level: nil)
+ end
context 'reporter' do
let(:current_user) { reporter }
@@ -365,7 +386,9 @@ describe GroupPolicy do
end
context 'when group has project creation level set to no one' do
- let(:group) { create(:group, project_creation_level: ::Gitlab::Access::NO_ONE_PROJECT_ACCESS) }
+ before_all do
+ group.update(project_creation_level: ::Gitlab::Access::NO_ONE_PROJECT_ACCESS)
+ end
context 'reporter' do
let(:current_user) { reporter }
@@ -393,7 +416,9 @@ describe GroupPolicy do
end
context 'when group has project creation level set to maintainer only' do
- let(:group) { create(:group, project_creation_level: ::Gitlab::Access::MAINTAINER_PROJECT_ACCESS) }
+ before_all do
+ group.update(project_creation_level: ::Gitlab::Access::MAINTAINER_PROJECT_ACCESS)
+ end
context 'reporter' do
let(:current_user) { reporter }
@@ -421,7 +446,9 @@ describe GroupPolicy do
end
context 'when group has project creation level set to developers + maintainer' do
- let(:group) { create(:group, project_creation_level: ::Gitlab::Access::DEVELOPER_MAINTAINER_PROJECT_ACCESS) }
+ before_all do
+ group.update(project_creation_level: ::Gitlab::Access::DEVELOPER_MAINTAINER_PROJECT_ACCESS)
+ end
context 'reporter' do
let(:current_user) { reporter }
@@ -451,10 +478,8 @@ describe GroupPolicy do
context "create_subgroup" do
context 'when group has subgroup creation level set to owner' do
- let(:group) do
- create(
- :group,
- subgroup_creation_level: ::Gitlab::Access::OWNER_SUBGROUP_ACCESS)
+ before_all do
+ group.update(subgroup_creation_level: ::Gitlab::Access::OWNER_SUBGROUP_ACCESS)
end
context 'reporter' do
@@ -483,10 +508,8 @@ describe GroupPolicy do
end
context 'when group has subgroup creation level set to maintainer' do
- let(:group) do
- create(
- :group,
- subgroup_creation_level: ::Gitlab::Access::MAINTAINER_SUBGROUP_ACCESS)
+ before_all do
+ group.update(subgroup_creation_level: ::Gitlab::Access::MAINTAINER_SUBGROUP_ACCESS)
end
context 'reporter' do
@@ -524,4 +547,28 @@ describe GroupPolicy do
groups: [clusterable])
end
end
+
+ describe 'update_max_artifacts_size' do
+ let(:group) { create(:group, :public) }
+
+ context 'when no user' do
+ let(:current_user) { nil }
+
+ it { expect_disallowed(:update_max_artifacts_size) }
+ end
+
+ context 'admin' do
+ let(:current_user) { admin }
+
+ it { expect_allowed(:update_max_artifacts_size) }
+ end
+
+ %w(guest reporter developer maintainer owner).each do |role|
+ context role do
+ let(:current_user) { send(role) }
+
+ it { expect_disallowed(:update_max_artifacts_size) }
+ end
+ end
+ end
end
diff --git a/spec/policies/identity_provider_policy_spec.rb b/spec/policies/identity_provider_policy_spec.rb
index 2520469d4e7..52b6d2c89ba 100644
--- a/spec/policies/identity_provider_policy_spec.rb
+++ b/spec/policies/identity_provider_policy_spec.rb
@@ -4,6 +4,7 @@ require 'spec_helper'
describe IdentityProviderPolicy do
subject(:policy) { described_class.new(user, provider) }
+
let(:user) { User.new }
let(:provider) { :a_provider }
diff --git a/spec/policies/merge_request_policy_spec.rb b/spec/policies/merge_request_policy_spec.rb
index 87205f56589..af4c9703eb4 100644
--- a/spec/policies/merge_request_policy_spec.rb
+++ b/spec/policies/merge_request_policy_spec.rb
@@ -53,21 +53,25 @@ describe MergeRequestPolicy do
describe 'the author' do
subject { author }
+
it_behaves_like 'a denied user'
end
describe 'a guest' do
subject { guest }
+
it_behaves_like 'a denied user'
end
describe 'a developer' do
subject { developer }
+
it_behaves_like 'a denied user'
end
describe 'any other user' do
subject { non_team_member }
+
it_behaves_like 'a denied user'
end
end
@@ -82,11 +86,13 @@ describe MergeRequestPolicy do
describe 'a non-team-member' do
subject { non_team_member }
+
it_behaves_like 'a denied user'
end
describe 'a developer' do
subject { developer }
+
it_behaves_like 'a user with access'
end
end
diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb
index 71ba73d5661..e61a064e82c 100644
--- a/spec/policies/project_policy_spec.rb
+++ b/spec/policies/project_policy_spec.rb
@@ -40,14 +40,14 @@ describe ProjectPolicy do
update_commit_status create_build update_build create_pipeline
update_pipeline create_merge_request_from create_wiki push_code
resolve_note create_container_image update_container_image destroy_container_image
- create_environment create_deployment create_release update_release
+ create_environment create_deployment update_deployment create_release update_release
]
end
let(:base_maintainer_permissions) do
%i[
push_to_delete_protected_branch update_project_snippet update_environment
- update_deployment admin_project_snippet admin_project_member admin_note admin_wiki admin_project
+ admin_project_snippet admin_project_member admin_note admin_wiki admin_project
admin_commit_status admin_build admin_container_image
admin_pipeline admin_environment admin_deployment destroy_release add_cluster
daily_statistics
@@ -478,4 +478,28 @@ describe ProjectPolicy do
end
end
end
+
+ describe 'update_max_artifacts_size' do
+ subject { described_class.new(current_user, project) }
+
+ context 'when no user' do
+ let(:current_user) { nil }
+
+ it { expect_disallowed(:update_max_artifacts_size) }
+ end
+
+ context 'admin' do
+ let(:current_user) { admin }
+
+ it { expect_allowed(:update_max_artifacts_size) }
+ end
+
+ %w(guest reporter developer maintainer owner).each do |role|
+ context role do
+ let(:current_user) { send(role) }
+
+ it { expect_disallowed(:update_max_artifacts_size) }
+ end
+ end
+ end
end
diff --git a/spec/policies/todo_policy_spec.rb b/spec/policies/todo_policy_spec.rb
new file mode 100644
index 00000000000..be6fecd1045
--- /dev/null
+++ b/spec/policies/todo_policy_spec.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe TodoPolicy do
+ let_it_be(:author) { create(:user) }
+
+ let_it_be(:user1) { create(:user) }
+ let_it_be(:user2) { create(:user) }
+ let_it_be(:user3) { create(:user) }
+
+ let_it_be(:todo1) { create(:todo, author: author, user: user1) }
+ let_it_be(:todo2) { create(:todo, author: author, user: user2) }
+ let_it_be(:todo3) { create(:todo, author: author, user: user2) }
+ let_it_be(:todo4) { create(:todo, author: author, user: user3) }
+
+ def permissions(user, todo)
+ described_class.new(user, todo)
+ end
+
+ describe 'own_todo' do
+ it 'allows owners to access their own todos' do
+ [
+ [user1, todo1],
+ [user2, todo2],
+ [user2, todo3],
+ [user3, todo4]
+ ].each do |user, todo|
+ expect(permissions(user, todo)).to be_allowed(:read_todo)
+ end
+ end
+
+ it 'does not allow users to access todos of other users' do
+ [
+ [user1, todo2],
+ [user1, todo3],
+ [user2, todo1],
+ [user2, todo4],
+ [user3, todo1],
+ [user3, todo2],
+ [user3, todo3]
+ ].each do |user, todo|
+ expect(permissions(user, todo)).to be_disallowed(:read_todo)
+ end
+ end
+ end
+end
diff --git a/spec/presenters/ci/build_runner_presenter_spec.rb b/spec/presenters/ci/build_runner_presenter_spec.rb
index a4234d14255..fa8791f2257 100644
--- a/spec/presenters/ci/build_runner_presenter_spec.rb
+++ b/spec/presenters/ci/build_runner_presenter_spec.rb
@@ -207,5 +207,22 @@ describe Ci::BuildRunnerPresenter do
end
end
end
+
+ context 'when persistent pipeline ref exists' do
+ let(:project) { create(:project, :repository) }
+ let(:sha) { project.repository.commit.sha }
+ let(:pipeline) { create(:ci_pipeline, sha: sha, project: project) }
+ let(:build) { create(:ci_build, pipeline: pipeline) }
+
+ before do
+ pipeline.persistent_ref.create
+ end
+
+ it 'exposes the persistent pipeline ref' do
+ is_expected
+ .to contain_exactly("+refs/pipelines/#{pipeline.id}:refs/pipelines/#{pipeline.id}",
+ "+refs/heads/#{build.ref}:refs/remotes/origin/#{build.ref}")
+ end
+ end
end
end
diff --git a/spec/presenters/ci/pipeline_presenter_spec.rb b/spec/presenters/ci/pipeline_presenter_spec.rb
index 7e8bbedcf6d..8cfcd9befb3 100644
--- a/spec/presenters/ci/pipeline_presenter_spec.rb
+++ b/spec/presenters/ci/pipeline_presenter_spec.rb
@@ -176,6 +176,44 @@ describe Ci::PipelinePresenter do
end
end
+ describe '#all_related_merge_request_text' do
+ subject { presenter.all_related_merge_request_text }
+
+ context 'with zero related merge requests (branch pipeline)' do
+ it { is_expected.to eq('No related merge requests found.') }
+ end
+
+ context 'with one related merge request' do
+ let!(:mr_1) { create(:merge_request, project: project, source_project: project) }
+
+ it {
+ is_expected.to eq("1 related merge request: " \
+ "<a class=\"mr-iid\" href=\"#{merge_request_path(mr_1)}\">#{mr_1.to_reference} #{mr_1.title}</a>")
+ }
+ end
+
+ context 'with two related merge requests' do
+ let!(:mr_1) { create(:merge_request, project: project, source_project: project, target_branch: 'staging') }
+ let!(:mr_2) { create(:merge_request, project: project, source_project: project, target_branch: 'feature') }
+
+ it {
+ is_expected.to eq("2 related merge requests: " \
+ "<a class=\"mr-iid\" href=\"#{merge_request_path(mr_2)}\">#{mr_2.to_reference} #{mr_2.title}</a>, " \
+ "<a class=\"mr-iid\" href=\"#{merge_request_path(mr_1)}\">#{mr_1.to_reference} #{mr_1.title}</a>")
+ }
+ end
+ end
+
+ describe '#all_related_merge_requests' do
+ it 'memoizes the returned relation' do
+ query_count = ActiveRecord::QueryRecorder.new do
+ 2.times { presenter.send(:all_related_merge_requests).count }
+ end.count
+
+ expect(query_count).to eq(1)
+ end
+ end
+
describe '#link_to_merge_request' do
subject { presenter.link_to_merge_request }
diff --git a/spec/presenters/commit_presenter_spec.rb b/spec/presenters/commit_presenter_spec.rb
index 4a0d3a28c32..bc749acfa3a 100644
--- a/spec/presenters/commit_presenter_spec.rb
+++ b/spec/presenters/commit_presenter_spec.rb
@@ -17,15 +17,19 @@ describe CommitPresenter do
end
it 'returns commit status for ref' do
- expect(commit).to receive(:status).with('ref').and_return('test')
+ pipeline = double
+ status = double
- expect(subject).to eq('test')
+ expect(commit).to receive(:latest_pipeline).with('ref').and_return(pipeline)
+ expect(pipeline).to receive(:detailed_status).with(user).and_return(status)
+
+ expect(subject).to eq(status)
end
end
context 'when user can not read_commit_status' do
- it 'is false' do
- is_expected.to eq(false)
+ it 'is nil' do
+ is_expected.to eq(nil)
end
end
end
@@ -51,4 +55,17 @@ describe CommitPresenter do
end
end
end
+
+ describe '#signature_html' do
+ let(:signature) { 'signature' }
+
+ before do
+ expect(commit).to receive(:has_signature?).and_return(true)
+ allow(ApplicationController.renderer).to receive(:render).and_return(signature)
+ end
+
+ it 'renders html for displaying signature' do
+ expect(presenter.signature_html).to eq(signature)
+ end
+ end
end
diff --git a/spec/presenters/conversational_development_index/metric_presenter_spec.rb b/spec/presenters/conversational_development_index/metric_presenter_spec.rb
index 81eb05a9a6b..b8b68a676e6 100644
--- a/spec/presenters/conversational_development_index/metric_presenter_spec.rb
+++ b/spec/presenters/conversational_development_index/metric_presenter_spec.rb
@@ -2,6 +2,7 @@ require 'spec_helper'
describe ConversationalDevelopmentIndex::MetricPresenter do
subject { described_class.new(metric) }
+
let(:metric) { build(:conversational_development_index_metric) }
describe '#cards' do
diff --git a/spec/presenters/issue_presenter_spec.rb b/spec/presenters/issue_presenter_spec.rb
index 8e24559341b..1eb674d1f8f 100644
--- a/spec/presenters/issue_presenter_spec.rb
+++ b/spec/presenters/issue_presenter_spec.rb
@@ -17,13 +17,27 @@ describe IssuePresenter do
describe '#web_url' do
it 'returns correct path' do
- expect(presenter.web_url).to eq "http://localhost/#{group.name}/#{project.name}/issues/#{issue.iid}"
+ expect(presenter.web_url).to eq("http://localhost/#{group.name}/#{project.name}/issues/#{issue.iid}")
+ end
+ end
+
+ describe '#subscribed?' do
+ subject { presenter.subscribed? }
+
+ it 'returns not subscribed' do
+ is_expected.to be(false)
+ end
+
+ it 'returns subscribed' do
+ create(:subscription, user: user, project: project, subscribable: issue, subscribed: true)
+
+ is_expected.to be(true)
end
end
describe '#issue_path' do
it 'returns correct path' do
- expect(presenter.issue_path).to eq "/#{group.name}/#{project.name}/issues/#{issue.iid}"
+ expect(presenter.issue_path).to eq("/#{group.name}/#{project.name}/issues/#{issue.iid}")
end
end
end
diff --git a/spec/presenters/project_presenter_spec.rb b/spec/presenters/project_presenter_spec.rb
index 5bf80f6e318..2a00548c2c3 100644
--- a/spec/presenters/project_presenter_spec.rb
+++ b/spec/presenters/project_presenter_spec.rb
@@ -430,4 +430,26 @@ describe ProjectPresenter do
)
end
end
+
+ describe '#empty_repo_statistics_buttons' do
+ let(:project) { create(:project, :repository) }
+ let(:presenter) { described_class.new(project, current_user: user) }
+
+ subject(:empty_repo_statistics_buttons) { presenter.empty_repo_statistics_buttons }
+
+ before do
+ project.add_developer(user)
+ allow(project).to receive(:auto_devops_enabled?).and_return(false)
+ end
+
+ it 'orders the items correctly in an empty project' do
+ expect(empty_repo_statistics_buttons.map(&:label)).to start_with(
+ a_string_including('New'),
+ a_string_including('README'),
+ a_string_including('CHANGELOG'),
+ a_string_including('CONTRIBUTING'),
+ a_string_including('CI/CD')
+ )
+ end
+ end
end
diff --git a/spec/rake_helper.rb b/spec/rake_helper.rb
index 298a520f5ca..77fece5842d 100644
--- a/spec/rake_helper.rb
+++ b/spec/rake_helper.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
require 'rake'
diff --git a/spec/requests/api/access_requests_spec.rb b/spec/requests/api/access_requests_spec.rb
index 1af6602ea9e..100f3d33c7b 100644
--- a/spec/requests/api/access_requests_spec.rb
+++ b/spec/requests/api/access_requests_spec.rb
@@ -7,7 +7,7 @@ describe API::AccessRequests do
set(:stranger) { create(:user) }
set(:project) do
- create(:project, :public, :access_requestable, creator_id: maintainer.id, namespace: maintainer.namespace) do |project|
+ create(:project, :public, creator_id: maintainer.id, namespace: maintainer.namespace) do |project|
project.add_developer(developer)
project.add_maintainer(maintainer)
project.request_access(access_requester)
@@ -15,7 +15,7 @@ describe API::AccessRequests do
end
set(:group) do
- create(:group, :public, :access_requestable) do |group|
+ create(:group, :public) do |group|
group.add_developer(developer)
group.add_owner(maintainer)
group.request_access(access_requester)
diff --git a/spec/requests/api/badges_spec.rb b/spec/requests/api/badges_spec.rb
index 1dd0cb4817c..771a78a2d91 100644
--- a/spec/requests/api/badges_spec.rb
+++ b/spec/requests/api/badges_spec.rb
@@ -345,7 +345,7 @@ describe API::Badges do
end
def setup_project
- create(:project, :public, :access_requestable, creator_id: maintainer.id, namespace: project_group) do |project|
+ create(:project, :public, creator_id: maintainer.id, namespace: project_group) do |project|
project.add_developer(developer)
project.add_maintainer(maintainer)
project.request_access(access_requester)
@@ -356,7 +356,7 @@ describe API::Badges do
end
def setup_group
- create(:group, :public, :access_requestable) do |group|
+ create(:group, :public) do |group|
group.add_developer(developer)
group.add_owner(maintainer)
group.request_access(access_requester)
diff --git a/spec/requests/api/commit_statuses_spec.rb b/spec/requests/api/commit_statuses_spec.rb
index 1be8883bd3c..6cb02ba2f6b 100644
--- a/spec/requests/api/commit_statuses_spec.rb
+++ b/spec/requests/api/commit_statuses_spec.rb
@@ -125,25 +125,55 @@ describe API::CommitStatuses do
let(:post_url) { "/projects/#{project.id}/statuses/#{sha}" }
context 'developer user' do
- %w[pending running success failed canceled].each do |status|
- context "for #{status}" do
- context 'uses only required parameters' do
- it 'creates commit status' do
- post api(post_url, developer), params: { state: status }
+ context 'uses only required parameters' do
+ %w[pending running success failed canceled].each do |status|
+ context "for #{status}" do
+ context 'when pipeline for sha does not exists' do
+ it 'creates commit status' do
+ post api(post_url, developer), params: { state: status }
+
+ expect(response).to have_gitlab_http_status(201)
+ expect(json_response['sha']).to eq(commit.id)
+ expect(json_response['status']).to eq(status)
+ expect(json_response['name']).to eq('default')
+ expect(json_response['ref']).not_to be_empty
+ expect(json_response['target_url']).to be_nil
+ expect(json_response['description']).to be_nil
+
+ if status == 'failed'
+ expect(CommitStatus.find(json_response['id'])).to be_api_failure
+ end
+ end
+ end
+ end
+ end
+
+ context 'when pipeline already exists for the specified sha' do
+ let!(:pipeline) { create(:ci_pipeline, project: project, sha: sha, ref: 'ref') }
+ let(:params) { { state: 'pending' } }
+
+ shared_examples_for 'creates a commit status for the existing pipeline' do
+ it do
+ expect do
+ post api(post_url, developer), params: params
+ end.not_to change { Ci::Pipeline.count }
+
+ job = pipeline.statuses.find_by_name(json_response['name'])
expect(response).to have_gitlab_http_status(201)
- expect(json_response['sha']).to eq(commit.id)
- expect(json_response['status']).to eq(status)
- expect(json_response['name']).to eq('default')
- expect(json_response['ref']).not_to be_empty
- expect(json_response['target_url']).to be_nil
- expect(json_response['description']).to be_nil
-
- if status == 'failed'
- expect(CommitStatus.find(json_response['id'])).to be_api_failure
- end
+ expect(job.status).to eq('pending')
end
end
+
+ it_behaves_like 'creates a commit status for the existing pipeline'
+
+ context 'with pipeline for merge request' do
+ let!(:merge_request) { create(:merge_request, :with_detached_merge_request_pipeline, source_project: project) }
+ let!(:pipeline) { merge_request.all_pipelines.last }
+ let(:sha) { pipeline.sha }
+
+ it_behaves_like 'creates a commit status for the existing pipeline'
+ end
end
end
diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb
index 5e6ff40e8cf..90ff1d12bf1 100644
--- a/spec/requests/api/commits_spec.rb
+++ b/spec/requests/api/commits_spec.rb
@@ -169,6 +169,18 @@ describe API::Commits do
end
end
+ context 'first_parent optional parameter' do
+ it 'returns all first_parent commits' do
+ commit_count = project.repository.count_commits(ref: SeedRepo::Commit::ID, first_parent: true)
+
+ get api("/projects/#{project_id}/repository/commits", user), params: { ref_name: SeedRepo::Commit::ID, first_parent: 'true' }
+
+ expect(response).to include_pagination_headers
+ expect(commit_count).to eq(12)
+ expect(response.headers['X-Total']).to eq(commit_count.to_s)
+ end
+ end
+
context 'with_stats optional parameter' do
let(:project) { create(:project, :public, :repository) }
diff --git a/spec/requests/api/deploy_keys_spec.rb b/spec/requests/api/deploy_keys_spec.rb
index b93ee148736..e0cc18abcca 100644
--- a/spec/requests/api/deploy_keys_spec.rb
+++ b/spec/requests/api/deploy_keys_spec.rb
@@ -2,6 +2,7 @@ require 'spec_helper'
describe API::DeployKeys do
let(:user) { create(:user) }
+ let(:maintainer) { create(:user) }
let(:admin) { create(:admin) }
let(:project) { create(:project, creator_id: user.id) }
let(:project2) { create(:project, creator_id: user.id) }
@@ -124,45 +125,109 @@ describe API::DeployKeys do
end
describe 'PUT /projects/:id/deploy_keys/:key_id' do
- let(:private_deploy_key) { create(:another_deploy_key, public: false) }
- let(:project_private_deploy_key) do
- create(:deploy_keys_project, project: project, deploy_key: private_deploy_key)
+ let(:extra_params) { {} }
+
+ subject do
+ put api("/projects/#{project.id}/deploy_keys/#{deploy_key.id}", api_user), params: extra_params
end
- it 'updates a public deploy key as admin' do
- expect do
- put api("/projects/#{project.id}/deploy_keys/#{deploy_key.id}", admin), params: { title: 'new title' }
- end.not_to change(deploy_key, :title)
+ context 'with non-admin' do
+ let(:api_user) { user }
- expect(response).to have_gitlab_http_status(200)
+ it 'does not update a public deploy key' do
+ expect { subject }.not_to change(deploy_key, :title)
+
+ expect(response).to have_gitlab_http_status(404)
+ end
end
- it 'does not update a public deploy key as non admin' do
- expect do
- put api("/projects/#{project.id}/deploy_keys/#{deploy_key.id}", user), params: { title: 'new title' }
- end.not_to change(deploy_key, :title)
+ context 'with admin' do
+ let(:api_user) { admin }
- expect(response).to have_gitlab_http_status(404)
+ context 'public deploy key attached to project' do
+ let(:extra_params) { { title: 'new title', can_push: true } }
+
+ it 'updates the title of the deploy key' do
+ expect { subject }.to change { deploy_key.reload.title }.to 'new title'
+ expect(response).to have_gitlab_http_status(200)
+ end
+
+ it 'updates can_push of deploy_keys_project' do
+ expect { subject }.to change { deploy_keys_project.reload.can_push }.from(false).to(true)
+ expect(response).to have_gitlab_http_status(200)
+ end
+ end
+
+ context 'private deploy key' do
+ let(:deploy_key) { create(:another_deploy_key, public: false) }
+ let(:deploy_keys_project) do
+ create(:deploy_keys_project, project: project, deploy_key: deploy_key)
+ end
+ let(:extra_params) { { title: 'new title', can_push: true } }
+
+ it 'updates the title of the deploy key' do
+ expect { subject }.to change { deploy_key.reload.title }.to 'new title'
+ expect(response).to have_gitlab_http_status(200)
+ end
+
+ it 'updates can_push of deploy_keys_project' do
+ expect { subject }.to change { deploy_keys_project.reload.can_push }.from(false).to(true)
+ expect(response).to have_gitlab_http_status(200)
+ end
+
+ context 'invalid title' do
+ let(:extra_params) { { title: '' } }
+
+ it 'does not update the title of the deploy key' do
+ expect { subject }.not_to change { deploy_key.reload.title }
+ expect(response).to have_gitlab_http_status(400)
+ end
+ end
+ end
end
- it 'does not update a private key with invalid title' do
- project_private_deploy_key
+ context 'with admin as project maintainer' do
+ let(:api_user) { admin }
- expect do
- put api("/projects/#{project.id}/deploy_keys/#{private_deploy_key.id}", admin), params: { title: '' }
- end.not_to change(deploy_key, :title)
+ before do
+ project.add_maintainer(admin)
+ end
- expect(response).to have_gitlab_http_status(400)
+ context 'public deploy key attached to project' do
+ let(:extra_params) { { title: 'new title', can_push: true } }
+
+ it 'updates the title of the deploy key' do
+ expect { subject }.to change { deploy_key.reload.title }.to 'new title'
+ expect(response).to have_gitlab_http_status(200)
+ end
+
+ it 'updates can_push of deploy_keys_project' do
+ expect { subject }.to change { deploy_keys_project.reload.can_push }.from(false).to(true)
+ expect(response).to have_gitlab_http_status(200)
+ end
+ end
end
- it 'updates a private ssh key with correct attributes' do
- project_private_deploy_key
+ context 'with maintainer' do
+ let(:api_user) { maintainer }
- put api("/projects/#{project.id}/deploy_keys/#{private_deploy_key.id}", admin), params: { title: 'new title', can_push: true }
+ before do
+ project.add_maintainer(maintainer)
+ end
- expect(json_response['id']).to eq(private_deploy_key.id)
- expect(json_response['title']).to eq('new title')
- expect(json_response['can_push']).to eq(true)
+ context 'public deploy key attached to project' do
+ let(:extra_params) { { title: 'new title', can_push: true } }
+
+ it 'does not update the title of the deploy key' do
+ expect { subject }.not_to change { deploy_key.reload.title }
+ expect(response).to have_gitlab_http_status(200)
+ end
+
+ it 'updates can_push of deploy_keys_project' do
+ expect { subject }.to change { deploy_keys_project.reload.can_push }.from(false).to(true)
+ expect(response).to have_gitlab_http_status(200)
+ end
+ end
end
end
diff --git a/spec/requests/api/deployments_spec.rb b/spec/requests/api/deployments_spec.rb
index 3dac7225b7a..ad7be531979 100644
--- a/spec/requests/api/deployments_spec.rb
+++ b/spec/requests/api/deployments_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe API::Deployments do
@@ -96,4 +98,164 @@ describe API::Deployments do
end
end
end
+
+ describe 'POST /projects/:id/deployments' do
+ let!(:project) { create(:project, :repository) }
+ let(:sha) { 'b83d6e391c22777fca1ed3012fce84f633d7fed0' }
+
+ context 'as a maintainer' do
+ it 'creates a new deployment' do
+ post(
+ api("/projects/#{project.id}/deployments", user),
+ params: {
+ environment: 'production',
+ sha: sha,
+ ref: 'master',
+ tag: false,
+ status: 'success'
+ }
+ )
+
+ expect(response).to have_gitlab_http_status(201)
+
+ expect(json_response['sha']).to eq(sha)
+ expect(json_response['ref']).to eq('master')
+ expect(json_response['environment']['name']).to eq('production')
+ end
+
+ it 'errors when creating a deployment with an invalid name' do
+ post(
+ api("/projects/#{project.id}/deployments", user),
+ params: {
+ environment: 'a' * 300,
+ sha: sha,
+ ref: 'master',
+ tag: false,
+ status: 'success'
+ }
+ )
+
+ expect(response).to have_gitlab_http_status(500)
+ end
+ end
+
+ context 'as a developer' do
+ it 'creates a new deployment' do
+ developer = create(:user)
+
+ project.add_developer(developer)
+
+ post(
+ api("/projects/#{project.id}/deployments", developer),
+ params: {
+ environment: 'production',
+ sha: sha,
+ ref: 'master',
+ tag: false,
+ status: 'success'
+ }
+ )
+
+ expect(response).to have_gitlab_http_status(201)
+
+ expect(json_response['sha']).to eq(sha)
+ expect(json_response['ref']).to eq('master')
+ end
+ end
+
+ context 'as non member' do
+ it 'returns a 404 status code' do
+ post(
+ api( "/projects/#{project.id}/deployments", non_member),
+ params: {
+ environment: 'production',
+ sha: '123',
+ ref: 'master',
+ tag: false,
+ status: 'success'
+ }
+ )
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+ end
+
+ describe 'PUT /projects/:id/deployments/:deployment_id' do
+ let(:project) { create(:project) }
+ let(:build) { create(:ci_build, :failed, project: project) }
+ let(:environment) { create(:environment, project: project) }
+ let(:deploy) do
+ create(
+ :deployment,
+ :failed,
+ project: project,
+ environment: environment,
+ deployable: nil
+ )
+ end
+
+ context 'as a maintainer' do
+ it 'returns a 403 when updating a deployment with a build' do
+ deploy.update(deployable: build)
+
+ put(
+ api("/projects/#{project.id}/deployments/#{deploy.id}", user),
+ params: { status: 'success' }
+ )
+
+ expect(response).to have_gitlab_http_status(403)
+ end
+
+ it 'updates a deployment without an associated build' do
+ put(
+ api("/projects/#{project.id}/deployments/#{deploy.id}", user),
+ params: { status: 'success' }
+ )
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response['status']).to eq('success')
+ end
+ end
+
+ context 'as a developer' do
+ let(:developer) { create(:user) }
+
+ before do
+ project.add_developer(developer)
+ end
+
+ it 'returns a 403 when updating a deployment with a build' do
+ deploy.update(deployable: build)
+
+ put(
+ api("/projects/#{project.id}/deployments/#{deploy.id}", developer),
+ params: { status: 'success' }
+ )
+
+ expect(response).to have_gitlab_http_status(403)
+ end
+
+ it 'updates a deployment without an associated build' do
+ put(
+ api("/projects/#{project.id}/deployments/#{deploy.id}", developer),
+ params: { status: 'success' }
+ )
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response['status']).to eq('success')
+ end
+ end
+
+ context 'as non member' do
+ it 'returns a 404 status code' do
+ put(
+ api("/projects/#{project.id}/deployments/#{deploy.id}", non_member),
+ params: { status: 'success' }
+ )
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+ end
end
diff --git a/spec/requests/api/doorkeeper_access_spec.rb b/spec/requests/api/doorkeeper_access_spec.rb
index d74484c8d29..cfee3f6c0f8 100644
--- a/spec/requests/api/doorkeeper_access_spec.rb
+++ b/spec/requests/api/doorkeeper_access_spec.rb
@@ -38,21 +38,35 @@ describe 'doorkeeper access' do
end
end
- describe "when user is blocked" do
- it "returns authorization error" do
- user.block
+ shared_examples 'forbidden request' do
+ it 'returns 403 response' do
get api("/user"), params: { access_token: token.token }
expect(response).to have_gitlab_http_status(403)
end
end
- describe "when user is ldap_blocked" do
- it "returns authorization error" do
+ context "when user is blocked" do
+ before do
+ user.block
+ end
+
+ it_behaves_like 'forbidden request'
+ end
+
+ context "when user is ldap_blocked" do
+ before do
user.ldap_block
- get api("/user"), params: { access_token: token.token }
+ end
- expect(response).to have_gitlab_http_status(403)
+ it_behaves_like 'forbidden request'
+ end
+
+ context "when user is deactivated" do
+ before do
+ user.deactivate
end
+
+ it_behaves_like 'forbidden request'
end
end
diff --git a/spec/requests/api/events_spec.rb b/spec/requests/api/events_spec.rb
index 2fc772b12af..992fd5e9c66 100644
--- a/spec/requests/api/events_spec.rb
+++ b/spec/requests/api/events_spec.rb
@@ -122,6 +122,7 @@ describe API::Events do
expect(payload_hash['action']).to eq(payload.action)
expect(payload_hash['ref_type']).to eq(payload.ref_type)
expect(payload_hash['commit_to']).to eq(payload.commit_to)
+ expect(payload_hash['ref_count']).to eq(payload.ref_count)
end
end
diff --git a/spec/requests/api/graphql/namespace/projects_spec.rb b/spec/requests/api/graphql/namespace/projects_spec.rb
index 815e9531ecf..2a95b99572f 100644
--- a/spec/requests/api/graphql/namespace/projects_spec.rb
+++ b/spec/requests/api/graphql/namespace/projects_spec.rb
@@ -67,6 +67,7 @@ describe 'getting projects' do
context 'when the namespace is a user' do
subject { user.namespace }
+
let(:include_subgroups) { false }
it_behaves_like 'a graphql namespace'
diff --git a/spec/requests/api/graphql/read_only_spec.rb b/spec/requests/api/graphql/read_only_spec.rb
new file mode 100644
index 00000000000..1d28a71258d
--- /dev/null
+++ b/spec/requests/api/graphql/read_only_spec.rb
@@ -0,0 +1,57 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'Requests on a read-only node' do
+ include GraphqlHelpers
+
+ before do
+ allow(Gitlab::Database).to receive(:read_only?) { true }
+ end
+
+ context 'mutations' do
+ let(:current_user) { note.author }
+ let!(:note) { create(:note) }
+
+ let(:mutation) do
+ variables = {
+ id: GitlabSchema.id_from_object(note).to_s
+ }
+
+ graphql_mutation(:destroy_note, variables)
+ end
+
+ def mutation_response
+ graphql_mutation_response(:destroy_note)
+ end
+
+ it 'disallows the query' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect(json_response['errors'].first['message']).to eq(Mutations::BaseMutation::ERROR_MESSAGE)
+ end
+
+ it 'does not destroy the Note' do
+ expect do
+ post_graphql_mutation(mutation, current_user: current_user)
+ end.not_to change { Note.count }
+ end
+ end
+
+ context 'read-only queries' do
+ let(:current_user) { create(:user) }
+ let(:project) { create(:project, :repository) }
+
+ before do
+ project.add_developer(current_user)
+ end
+
+ it 'allows the query' do
+ query = graphql_query_for('project', 'fullPath' => project.full_path)
+
+ post_graphql(query, current_user: current_user)
+
+ expect(graphql_data['project']).not_to be_nil
+ end
+ end
+end
diff --git a/spec/requests/api/group_labels_spec.rb b/spec/requests/api/group_labels_spec.rb
index 0be4e2e9121..f7994b55efa 100644
--- a/spec/requests/api/group_labels_spec.rb
+++ b/spec/requests/api/group_labels_spec.rb
@@ -5,9 +5,11 @@ require 'spec_helper'
describe API::GroupLabels do
let(:user) { create(:user) }
let(:group) { create(:group) }
+ let(:subgroup) { create(:group, parent: group) }
let!(:group_member) { create(:group_member, group: group, user: user) }
- let!(:label1) { create(:group_label, title: 'feature', group: group) }
- let!(:label2) { create(:group_label, title: 'bug', group: group) }
+ let!(:group_label1) { create(:group_label, title: 'feature', group: group) }
+ let!(:group_label2) { create(:group_label, title: 'bug', group: group) }
+ let!(:subgroup_label) { create(:group_label, title: 'support', group: subgroup) }
describe 'GET :id/labels' do
it 'returns all available labels for the group' do
@@ -35,6 +37,45 @@ describe API::GroupLabels do
end
end
+ describe 'GET :subgroup_id/labels' do
+ context 'when the include_ancestor_groups parameter is not set' do
+ it 'returns all available labels for the group and ancestor groups' do
+ get api("/groups/#{subgroup.id}/labels", user)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ expect(json_response).to all(match_schema('public_api/v4/labels/label'))
+ expect(json_response.size).to eq(3)
+ expect(json_response.map {|r| r['name'] }).to contain_exactly('feature', 'bug', 'support')
+ end
+ end
+
+ context 'when the include_ancestor_groups parameter is set to false' do
+ it 'returns all available labels for the group but not for ancestor groups' do
+ get api("/groups/#{subgroup.id}/labels", user), params: { include_ancestor_groups: false }
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ expect(json_response).to all(match_schema('public_api/v4/labels/label'))
+ expect(json_response.size).to eq(1)
+ expect(json_response.map {|r| r['name'] }).to contain_exactly('support')
+ end
+ end
+ end
+
+ describe 'GET :id/labels/:label_id' do
+ it 'returns a single label for the group' do
+ get api("/groups/#{group.id}/labels/#{group_label1.name}", user)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response['name']).to eq(group_label1.name)
+ expect(json_response['color']).to eq(group_label1.color)
+ expect(json_response['description']).to eq(group_label1.description)
+ end
+ end
+
describe 'POST /groups/:id/labels' do
it 'returns created label when all params are given' do
post api("/groups/#{group.id}/labels", user),
@@ -78,7 +119,7 @@ describe API::GroupLabels do
it 'returns 409 if label already exists' do
post api("/groups/#{group.id}/labels", user),
params: {
- name: label1.name,
+ name: group_label1.name,
color: '#FFAABB'
}
@@ -87,15 +128,15 @@ describe API::GroupLabels do
end
end
- describe 'DELETE /groups/:id/labels' do
+ describe 'DELETE /groups/:id/labels (deprecated)' do
it 'returns 204 for existing label' do
- delete api("/groups/#{group.id}/labels", user), params: { name: label1.name }
+ delete api("/groups/#{group.id}/labels", user), params: { name: group_label1.name }
expect(response).to have_gitlab_http_status(204)
end
it 'returns 404 for non existing label' do
- delete api("/groups/#{group.id}/labels", user), params: { name: 'label2' }
+ delete api("/groups/#{group.id}/labels", user), params: { name: 'not_exists' }
expect(response).to have_gitlab_http_status(404)
expect(json_response['message']).to eq('404 Label Not Found')
@@ -115,20 +156,50 @@ describe API::GroupLabels do
expect(response).to have_gitlab_http_status(204)
expect(subgroup.labels.size).to eq(0)
- expect(group.labels).to include(label1)
+ expect(group.labels).to include(group_label1)
end
it_behaves_like '412 response' do
let(:request) { api("/groups/#{group.id}/labels", user) }
- let(:params) { { name: label1.name } }
+ let(:params) { { name: group_label1.name } }
end
end
- describe 'PUT /groups/:id/labels' do
+ describe 'DELETE /groups/:id/labels/:label_id' do
+ it 'returns 204 for existing label' do
+ delete api("/groups/#{group.id}/labels/#{group_label1.name}", user)
+
+ expect(response).to have_gitlab_http_status(204)
+ end
+
+ it 'returns 404 for non existing label' do
+ delete api("/groups/#{group.id}/labels/not_exists", user)
+
+ expect(response).to have_gitlab_http_status(404)
+ expect(json_response['message']).to eq('404 Label Not Found')
+ end
+
+ it "does not delete parent's group labels" do
+ subgroup = create(:group, parent: group)
+ subgroup_label = create(:group_label, title: 'feature', group: subgroup)
+
+ delete api("/groups/#{subgroup.id}/labels/#{subgroup_label.name}", user)
+
+ expect(response).to have_gitlab_http_status(204)
+ expect(subgroup.labels.size).to eq(0)
+ expect(group.labels).to include(group_label1)
+ end
+
+ it_behaves_like '412 response' do
+ let(:request) { api("/groups/#{group.id}/labels/#{group_label1.name}", user) }
+ end
+ end
+
+ describe 'PUT /groups/:id/labels (deprecated)' do
it 'returns 200 if name and colors and description are changed' do
put api("/groups/#{group.id}/labels", user),
params: {
- name: label1.name,
+ name: group_label1.name,
new_name: 'New Label',
color: '#FFFFFF',
description: 'test'
@@ -152,13 +223,13 @@ describe API::GroupLabels do
expect(response).to have_gitlab_http_status(200)
expect(subgroup.labels[0].name).to eq('New Label')
- expect(label1.name).to eq('feature')
+ expect(group_label1.name).to eq('feature')
end
it 'returns 404 if label does not exist' do
put api("/groups/#{group.id}/labels", user),
params: {
- name: 'label2',
+ name: 'not_exists',
new_name: 'label3'
}
@@ -166,14 +237,61 @@ describe API::GroupLabels do
end
it 'returns 400 if no label name given' do
- put api("/groups/#{group.id}/labels", user), params: { new_name: label1.name }
+ put api("/groups/#{group.id}/labels", user), params: { new_name: group_label1.name }
+
+ expect(response).to have_gitlab_http_status(400)
+ expect(json_response['error']).to eq('label_id, name are missing, exactly one parameter must be provided')
+ end
+
+ it 'returns 400 if no new parameters given' do
+ put api("/groups/#{group.id}/labels", user), params: { name: group_label1.name }
expect(response).to have_gitlab_http_status(400)
- expect(json_response['error']).to eq('name is missing')
+ expect(json_response['error']).to eq('new_name, color, description are missing, '\
+ 'at least one parameter must be provided')
+ end
+ end
+
+ describe 'PUT /groups/:id/labels/:label_id' do
+ it 'returns 200 if name and colors and description are changed' do
+ put api("/groups/#{group.id}/labels/#{group_label1.name}", user),
+ params: {
+ new_name: 'New Label',
+ color: '#FFFFFF',
+ description: 'test'
+ }
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response['name']).to eq('New Label')
+ expect(json_response['color']).to eq('#FFFFFF')
+ expect(json_response['description']).to eq('test')
+ end
+
+ it "does not update parent's group label" do
+ subgroup = create(:group, parent: group)
+ subgroup_label = create(:group_label, title: 'feature', group: subgroup)
+
+ put api("/groups/#{subgroup.id}/labels/#{subgroup_label.name}", user),
+ params: {
+ new_name: 'New Label'
+ }
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(subgroup.labels[0].name).to eq('New Label')
+ expect(group_label1.name).to eq('feature')
+ end
+
+ it 'returns 404 if label does not exist' do
+ put api("/groups/#{group.id}/labels/not_exists", user),
+ params: {
+ new_name: 'label3'
+ }
+
+ expect(response).to have_gitlab_http_status(404)
end
it 'returns 400 if no new parameters given' do
- put api("/groups/#{group.id}/labels", user), params: { name: label1.name }
+ put api("/groups/#{group.id}/labels/#{group_label1.name}", user)
expect(response).to have_gitlab_http_status(400)
expect(json_response['error']).to eq('new_name, color, description are missing, '\
@@ -184,31 +302,31 @@ describe API::GroupLabels do
describe 'POST /groups/:id/labels/:label_id/subscribe' do
context 'when label_id is a label title' do
it 'subscribes to the label' do
- post api("/groups/#{group.id}/labels/#{label1.title}/subscribe", user)
+ post api("/groups/#{group.id}/labels/#{group_label1.title}/subscribe", user)
expect(response).to have_gitlab_http_status(201)
- expect(json_response['name']).to eq(label1.title)
+ expect(json_response['name']).to eq(group_label1.title)
expect(json_response['subscribed']).to be_truthy
end
end
context 'when label_id is a label ID' do
it 'subscribes to the label' do
- post api("/groups/#{group.id}/labels/#{label1.id}/subscribe", user)
+ post api("/groups/#{group.id}/labels/#{group_label1.id}/subscribe", user)
expect(response).to have_gitlab_http_status(201)
- expect(json_response['name']).to eq(label1.title)
+ expect(json_response['name']).to eq(group_label1.title)
expect(json_response['subscribed']).to be_truthy
end
end
context 'when user is already subscribed to label' do
before do
- label1.subscribe(user)
+ group_label1.subscribe(user)
end
it 'returns 304' do
- post api("/groups/#{group.id}/labels/#{label1.id}/subscribe", user)
+ post api("/groups/#{group.id}/labels/#{group_label1.id}/subscribe", user)
expect(response).to have_gitlab_http_status(304)
end
@@ -225,36 +343,36 @@ describe API::GroupLabels do
describe 'POST /groups/:id/labels/:label_id/unsubscribe' do
before do
- label1.subscribe(user)
+ group_label1.subscribe(user)
end
context 'when label_id is a label title' do
it 'unsubscribes from the label' do
- post api("/groups/#{group.id}/labels/#{label1.title}/unsubscribe", user)
+ post api("/groups/#{group.id}/labels/#{group_label1.title}/unsubscribe", user)
expect(response).to have_gitlab_http_status(201)
- expect(json_response['name']).to eq(label1.title)
+ expect(json_response['name']).to eq(group_label1.title)
expect(json_response['subscribed']).to be_falsey
end
end
context 'when label_id is a label ID' do
it 'unsubscribes from the label' do
- post api("/groups/#{group.id}/labels/#{label1.id}/unsubscribe", user)
+ post api("/groups/#{group.id}/labels/#{group_label1.id}/unsubscribe", user)
expect(response).to have_gitlab_http_status(201)
- expect(json_response['name']).to eq(label1.title)
+ expect(json_response['name']).to eq(group_label1.title)
expect(json_response['subscribed']).to be_falsey
end
end
context 'when user is already unsubscribed from label' do
before do
- label1.unsubscribe(user)
+ group_label1.unsubscribe(user)
end
it 'returns 304' do
- post api("/groups/#{group.id}/labels/#{label1.id}/unsubscribe", user)
+ post api("/groups/#{group.id}/labels/#{group_label1.id}/unsubscribe", user)
expect(response).to have_gitlab_http_status(304)
end
diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb
index b3acf531ccb..902a5ec2a86 100644
--- a/spec/requests/api/groups_spec.rb
+++ b/spec/requests/api/groups_spec.rb
@@ -1,6 +1,7 @@
require 'spec_helper'
describe API::Groups do
+ include GroupAPIHelpers
include UploadHelpers
let(:user1) { create(:user, can_create_group: false) }
@@ -350,6 +351,13 @@ describe API::Groups do
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(only_path: false))
+ expect(json_response['share_with_group_lock']).to eq(group1.share_with_group_lock)
+ expect(json_response['require_two_factor_authentication']).to eq(group1.require_two_factor_authentication)
+ expect(json_response['two_factor_grace_period']).to eq(group1.two_factor_grace_period)
+ expect(json_response['auto_devops_enabled']).to eq(group1.auto_devops_enabled)
+ expect(json_response['emails_disabled']).to eq(group1.emails_disabled)
+ expect(json_response['project_creation_level']).to eq('maintainer')
+ expect(json_response['subgroup_creation_level']).to eq('maintainer')
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)
@@ -485,11 +493,30 @@ describe API::Groups do
context 'when authenticated as the group owner' do
it 'updates the group' do
- put api("/groups/#{group1.id}", user1), params: { name: new_group_name, request_access_enabled: true }
+ put api("/groups/#{group1.id}", user1), params: {
+ name: new_group_name,
+ request_access_enabled: true,
+ project_creation_level: "noone",
+ subgroup_creation_level: "maintainer"
+ }
expect(response).to have_gitlab_http_status(200)
expect(json_response['name']).to eq(new_group_name)
+ expect(json_response['description']).to eq('')
+ expect(json_response['visibility']).to eq('public')
+ expect(json_response['share_with_group_lock']).to eq(false)
+ expect(json_response['require_two_factor_authentication']).to eq(false)
+ expect(json_response['two_factor_grace_period']).to eq(48)
+ expect(json_response['auto_devops_enabled']).to eq(nil)
+ expect(json_response['emails_disabled']).to eq(nil)
+ expect(json_response['project_creation_level']).to eq("noone")
+ expect(json_response['subgroup_creation_level']).to eq("maintainer")
expect(json_response['request_access_enabled']).to eq(true)
+ expect(json_response['parent_id']).to eq(nil)
+ expect(json_response['projects']).to be_an Array
+ expect(json_response['projects'].length).to eq(2)
+ expect(json_response['shared_projects']).to be_an Array
+ expect(json_response['shared_projects'].length).to eq(0)
end
it 'returns 404 for a non existing group' do
@@ -497,6 +524,29 @@ describe API::Groups do
expect(response).to have_gitlab_http_status(404)
end
+
+ context 'within a subgroup' do
+ let(:group3) { create(:group, visibility_level: Gitlab::VisibilityLevel::PUBLIC) }
+ let!(:subgroup) { create(:group, parent: group3, visibility_level: Gitlab::VisibilityLevel::PUBLIC) }
+
+ before do
+ group3.add_owner(user3)
+ end
+
+ it 'does not change visibility when not requested' do
+ put api("/groups/#{group3.id}", user3), params: { description: 'Bug #23083' }
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response['visibility']).to eq('public')
+ end
+
+ it 'prevents making private a group containing public subgroups' do
+ put api("/groups/#{group3.id}", user3), params: { visibility: 'private' }
+
+ expect(response).to have_gitlab_http_status(400)
+ expect(json_response['message']['visibility_level']).to contain_exactly('private is not allowed since there are sub-groups with higher visibility.')
+ end
+ end
end
context 'when authenticated as the admin' do
@@ -841,7 +891,9 @@ describe API::Groups do
describe "POST /groups" do
context "when authenticated as user without group permissions" do
it "does not create group" do
- post api("/groups", user1), params: attributes_for(:group)
+ group = attributes_for_group_api
+
+ post api("/groups", user1), params: group
expect(response).to have_gitlab_http_status(403)
end
@@ -873,7 +925,7 @@ describe API::Groups do
context "when authenticated as user with group permissions" do
it "creates group" do
- group = attributes_for(:group, { request_access_enabled: false })
+ group = attributes_for_group_api request_access_enabled: false
post api("/groups", user3), params: group
@@ -888,7 +940,7 @@ describe API::Groups do
it "creates a nested group" do
parent = create(:group)
parent.add_owner(user3)
- group = attributes_for(:group, { parent_id: parent.id })
+ group = attributes_for_group_api parent_id: parent.id
post api("/groups", user3), params: group
diff --git a/spec/requests/api/helpers_spec.rb b/spec/requests/api/helpers_spec.rb
index 29dc84c1924..a1a007811fe 100644
--- a/spec/requests/api/helpers_spec.rb
+++ b/spec/requests/api/helpers_spec.rb
@@ -343,6 +343,8 @@ describe API::Helpers do
end
context 'sudo' do
+ include_context 'custom session'
+
shared_examples 'successful sudo' do
it 'sets current_user' do
expect(current_user).to eq(user)
diff --git a/spec/requests/api/internal/base_spec.rb b/spec/requests/api/internal/base_spec.rb
index a56527073c7..01a2e33c0d9 100644
--- a/spec/requests/api/internal/base_spec.rb
+++ b/spec/requests/api/internal/base_spec.rb
@@ -43,61 +43,6 @@ describe API::Internal::Base do
end
end
- describe 'GET /internal/broadcast_message' do
- context 'broadcast message exists' do
- let!(:broadcast_message) { create(:broadcast_message, starts_at: 1.day.ago, ends_at: 1.day.from_now ) }
-
- it 'returns one broadcast message' do
- get api('/internal/broadcast_message'), params: { secret_token: secret_token }
-
- expect(response).to have_gitlab_http_status(200)
- expect(json_response['message']).to eq(broadcast_message.message)
- end
- end
-
- context 'broadcast message does not exist' do
- it 'returns nothing' do
- get api('/internal/broadcast_message'), params: { secret_token: secret_token }
-
- expect(response).to have_gitlab_http_status(200)
- expect(json_response).to be_empty
- end
- end
-
- context 'nil broadcast message' do
- it 'returns nothing' do
- allow(BroadcastMessage).to receive(:current).and_return(nil)
-
- get api('/internal/broadcast_message'), params: { secret_token: secret_token }
-
- expect(response).to have_gitlab_http_status(200)
- expect(json_response).to be_empty
- end
- end
- end
-
- describe 'GET /internal/broadcast_messages' do
- context 'broadcast message(s) exist' do
- let!(:broadcast_message) { create(:broadcast_message, starts_at: 1.day.ago, ends_at: 1.day.from_now ) }
-
- it 'returns active broadcast message(s)' do
- get api('/internal/broadcast_messages'), params: { secret_token: secret_token }
-
- expect(response).to have_gitlab_http_status(200)
- expect(json_response[0]['message']).to eq(broadcast_message.message)
- end
- end
-
- context 'broadcast message does not exist' do
- it 'returns nothing' do
- get api('/internal/broadcast_messages'), params: { secret_token: secret_token }
-
- expect(response).to have_gitlab_http_status(200)
- expect(json_response).to be_empty
- end
- end
- end
-
describe 'GET /internal/two_factor_recovery_codes' do
it 'returns an error message when the key does not exist' do
post api('/internal/two_factor_recovery_codes'),
@@ -237,14 +182,6 @@ describe API::Internal::Base do
expect(json_response['name']).to eq(user.name)
end
- it "finds a user by user id" do
- get(api("/internal/discover"), params: { user_id: user.id, secret_token: secret_token })
-
- expect(response).to have_gitlab_http_status(200)
-
- expect(json_response['name']).to eq(user.name)
- end
-
it "finds a user by username" do
get(api("/internal/discover"), params: { username: user.username, secret_token: secret_token })
@@ -403,12 +340,30 @@ describe API::Internal::Base do
end
context 'when receive_max_input_size has been updated' do
- it 'returns custom git config' do
+ before do
allow(Gitlab::CurrentSettings).to receive(:receive_max_input_size) { 1 }
+ end
+ it 'returns custom git config' do
push(key, project)
expect(json_response["git_config_options"]).to be_present
+ expect(json_response["git_config_options"]).to include("uploadpack.allowFilter=true")
+ expect(json_response["git_config_options"]).to include("uploadpack.allowAnySHA1InWant=true")
+ end
+
+ context 'when gitaly_upload_pack_filter feature flag is disabled' do
+ before do
+ stub_feature_flags(gitaly_upload_pack_filter: { enabled: false, thing: project })
+ end
+
+ it 'does not include allowFilter and allowAnySha1InWant in the git config options' do
+ push(key, project)
+
+ expect(json_response["git_config_options"]).to be_present
+ expect(json_response["git_config_options"]).not_to include("uploadpack.allowFilter=true")
+ expect(json_response["git_config_options"]).not_to include("uploadpack.allowAnySHA1InWant=true")
+ end
end
end
@@ -752,47 +707,6 @@ describe API::Internal::Base do
end
end
- describe 'GET /internal/merge_request_urls' do
- let(:repo_name) { "#{project.full_path}" }
- let(:changes) { URI.escape("#{Gitlab::Git::BLANK_SHA} 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/new_branch") }
-
- before do
- project.add_developer(user)
- end
-
- it 'returns link to create new merge request' do
- get api("/internal/merge_request_urls?project=#{repo_name}&changes=#{changes}"), params: { secret_token: secret_token }
-
- expect(json_response).to match [{
- "branch_name" => "new_branch",
- "url" => "http://#{Gitlab.config.gitlab.host}/#{project.full_path}/merge_requests/new?merge_request%5Bsource_branch%5D=new_branch",
- "new_merge_request" => true
- }]
- end
-
- it 'returns empty array if printing_merge_request_link_enabled is false' do
- project.update!(printing_merge_request_link_enabled: false)
-
- get api("/internal/merge_request_urls?project=#{repo_name}&changes=#{changes}"), params: { secret_token: secret_token }
-
- 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}"), params: { secret_token: secret_token }
-
- expect(json_response).to match [{
- "branch_name" => "new_branch",
- "url" => "http://#{Gitlab.config.gitlab.host}/#{project.full_path}/merge_requests/new?merge_request%5Bsource_branch%5D=new_branch",
- "new_merge_request" => true
- }]
- end
- end
- end
-
# TODO: Uncomment when the end-point is reenabled
# describe 'POST /notify_post_receive' do
# let(:valid_params) do
@@ -933,6 +847,19 @@ describe API::Internal::Base do
expect(json_response['messages']).to include(build_basic_message(message))
end
+ it 'returns the link to an existing merge request when it exists' do
+ merge_request = create(:merge_request, source_project: project, source_branch: branch_name, target_branch: 'master')
+
+ post api('/internal/post_receive'), params: valid_params
+
+ message = <<~MESSAGE.strip
+ View merge request for feature:
+ #{project_merge_request_url(project, merge_request)}
+ MESSAGE
+
+ expect(json_response['messages']).to include(build_basic_message(message))
+ end
+
it 'returns no merge request messages if printing_merge_request_link_enabled is false' do
project.update!(printing_merge_request_link_enabled: false)
diff --git a/spec/requests/api/internal/pages_spec.rb b/spec/requests/api/internal/pages_spec.rb
index e1b563b92f4..03bf748b471 100644
--- a/spec/requests/api/internal/pages_spec.rb
+++ b/spec/requests/api/internal/pages_spec.rb
@@ -43,6 +43,10 @@ describe API::Internal::Pages do
super(host, headers)
end
+ def deploy_pages(project)
+ project.mark_pages_as_deployed
+ end
+
context 'not existing host' do
it 'responds with 404 Not Found' do
query_host('pages.gitlab.io')
@@ -56,18 +60,104 @@ describe API::Internal::Pages do
let(:project) { create(:project, namespace: namespace, name: 'gitlab-ce') }
let!(:pages_domain) { create(:pages_domain, domain: 'pages.gitlab.io', project: project) }
- it 'responds with the correct domain configuration' do
- query_host('pages.gitlab.io')
+ context 'when there are no pages deployed for the related project' do
+ it 'responds with 204 No Content' do
+ query_host('pages.gitlab.io')
- expect(response).to have_gitlab_http_status(200)
- expect(response).to match_response_schema('internal/pages/virtual_domain')
+ expect(response).to have_gitlab_http_status(204)
+ end
+ end
- expect(json_response['certificate']).to eq(pages_domain.certificate)
- expect(json_response['key']).to eq(pages_domain.key)
+ context 'when there are pages deployed for the related project' do
+ it 'responds with the correct domain configuration' do
+ deploy_pages(project)
+
+ query_host('pages.gitlab.io')
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(response).to match_response_schema('internal/pages/virtual_domain')
+
+ expect(json_response['certificate']).to eq(pages_domain.certificate)
+ expect(json_response['key']).to eq(pages_domain.key)
+
+ expect(json_response['lookup_paths']).to eq(
+ [
+ {
+ 'project_id' => project.id,
+ 'access_control' => false,
+ 'https_only' => false,
+ 'prefix' => '/',
+ 'source' => {
+ 'type' => 'file',
+ 'path' => 'gitlab-org/gitlab-ce/public/'
+ }
+ }
+ ]
+ )
+ end
+ end
+ end
+
+ context 'namespaced domain' do
+ let(:group) { create(:group, name: 'mygroup') }
+
+ before do
+ allow(Settings.pages).to receive(:host).and_return('gitlab-pages.io')
+ allow(Gitlab.config.pages).to receive(:url).and_return("http://gitlab-pages.io")
+ end
+
+ context 'regular project' do
+ it 'responds with the correct domain configuration' do
+ project = create(:project, group: group, name: 'myproject')
+ deploy_pages(project)
+
+ query_host('mygroup.gitlab-pages.io')
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(response).to match_response_schema('internal/pages/virtual_domain')
+
+ expect(json_response['lookup_paths']).to eq(
+ [
+ {
+ 'project_id' => project.id,
+ 'access_control' => false,
+ 'https_only' => false,
+ 'prefix' => '/myproject/',
+ 'source' => {
+ 'type' => 'file',
+ 'path' => 'mygroup/myproject/public/'
+ }
+ }
+ ]
+ )
+ end
+ end
- lookup_path = json_response['lookup_paths'][0]
- expect(lookup_path['prefix']).to eq('/')
- expect(lookup_path['source']['path']).to eq('gitlab-org/gitlab-ce/public/')
+ context 'group root project' do
+ it 'responds with the correct domain configuration' do
+ project = create(:project, group: group, name: 'mygroup.gitlab-pages.io')
+ deploy_pages(project)
+
+ query_host('mygroup.gitlab-pages.io')
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(response).to match_response_schema('internal/pages/virtual_domain')
+
+ expect(json_response['lookup_paths']).to eq(
+ [
+ {
+ 'project_id' => project.id,
+ 'access_control' => false,
+ 'https_only' => false,
+ 'prefix' => '/',
+ 'source' => {
+ 'type' => 'file',
+ 'path' => 'mygroup/mygroup.gitlab-pages.io/public/'
+ }
+ }
+ ]
+ )
+ end
end
end
end
diff --git a/spec/requests/api/issues/get_project_issues_spec.rb b/spec/requests/api/issues/get_project_issues_spec.rb
index c10f5b2bd58..06a43ea6b02 100644
--- a/spec/requests/api/issues/get_project_issues_spec.rb
+++ b/spec/requests/api/issues/get_project_issues_spec.rb
@@ -736,6 +736,8 @@ describe API::Issues do
get_related_merge_requests(project.id, issue.iid)
expect_paginated_array_response(related_mr.id)
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response.last).not_to have_key('subscribed')
end
it 'renders 404 if project is not visible' do
diff --git a/spec/requests/api/issues/post_projects_issues_spec.rb b/spec/requests/api/issues/post_projects_issues_spec.rb
index b74e8867310..3a55b437ead 100644
--- a/spec/requests/api/issues/post_projects_issues_spec.rb
+++ b/spec/requests/api/issues/post_projects_issues_spec.rb
@@ -374,9 +374,17 @@ describe API::Issues do
end
describe 'POST /projects/:id/issues with spam filtering' do
+ def post_issue
+ post api("/projects/#{project.id}/issues", user), params: params
+ end
+
before do
- allow_any_instance_of(SpamService).to receive(:check_for_spam?).and_return(true)
- allow_any_instance_of(AkismetService).to receive_messages(spam?: true)
+ expect_next_instance_of(SpamService) do |spam_service|
+ expect(spam_service).to receive_messages(check_for_spam?: true)
+ end
+ expect_next_instance_of(AkismetService) do |akismet_service|
+ expect(akismet_service).to receive_messages(spam?: true)
+ end
end
let(:params) do
@@ -387,17 +395,43 @@ describe API::Issues do
}
end
- it 'does not create a new project issue' do
- expect { post api("/projects/#{project.id}/issues", user), params: params }.not_to change(Issue, :count)
- expect(response).to have_gitlab_http_status(400)
- expect(json_response['message']).to eq({ 'error' => 'Spam detected' })
-
- spam_logs = SpamLog.all
- expect(spam_logs.count).to eq(1)
- expect(spam_logs[0].title).to eq('new issue')
- expect(spam_logs[0].description).to eq('content here')
- expect(spam_logs[0].user).to eq(user)
- expect(spam_logs[0].noteable_type).to eq('Issue')
+ context 'when allow_possible_spam feature flag is false' do
+ before do
+ stub_feature_flags(allow_possible_spam: false)
+ end
+
+ it 'does not create a new project issue' do
+ expect { post_issue }.not_to change(Issue, :count)
+ end
+
+ it 'returns correct status and message' do
+ post_issue
+
+ expect(response).to have_gitlab_http_status(400)
+ expect(json_response['message']).to eq({ 'error' => 'Spam detected' })
+ end
+
+ it 'creates a new spam log entry' do
+ expect { post_issue }
+ .to log_spam(title: 'new issue', description: 'content here', user_id: user.id, noteable_type: 'Issue')
+ end
+ end
+
+ context 'when allow_possible_spam feature flag is true' do
+ it 'does creates a new project issue' do
+ expect { post_issue }.to change(Issue, :count).by(1)
+ end
+
+ it 'returns correct status' do
+ post_issue
+
+ expect(response).to have_gitlab_http_status(201)
+ end
+
+ it 'creates a new spam log entry' do
+ expect { post_issue }
+ .to log_spam(title: 'new issue', description: 'content here', user_id: user.id, noteable_type: 'Issue')
+ end
end
end
diff --git a/spec/requests/api/issues/put_projects_issues_spec.rb b/spec/requests/api/issues/put_projects_issues_spec.rb
index 267cba93713..43f302ed194 100644
--- a/spec/requests/api/issues/put_projects_issues_spec.rb
+++ b/spec/requests/api/issues/put_projects_issues_spec.rb
@@ -181,6 +181,10 @@ describe API::Issues do
end
describe 'PUT /projects/:id/issues/:issue_iid with spam filtering' do
+ def update_issue
+ put api("/projects/#{project.id}/issues/#{issue.iid}", user), params: params
+ end
+
let(:params) do
{
title: 'updated title',
@@ -189,21 +193,52 @@ describe API::Issues do
}
end
- it 'does not create a new project issue' do
- allow_any_instance_of(SpamService).to receive_messages(check_for_spam?: true)
- allow_any_instance_of(AkismetService).to receive_messages(spam?: true)
+ before do
+ expect_next_instance_of(SpamService) do |spam_service|
+ expect(spam_service).to receive_messages(check_for_spam?: true)
+ end
+ expect_next_instance_of(AkismetService) do |akismet_service|
+ expect(akismet_service).to receive_messages(spam?: true)
+ end
+ end
- put api("/projects/#{project.id}/issues/#{issue.iid}", user), params: params
+ context 'when allow_possible_spam feature flag is false' do
+ before do
+ stub_feature_flags(allow_possible_spam: false)
+ end
- expect(response).to have_gitlab_http_status(400)
- expect(json_response['message']).to eq({ 'error' => 'Spam detected' })
-
- spam_logs = SpamLog.all
- expect(spam_logs.count).to eq(1)
- expect(spam_logs[0].title).to eq('updated title')
- expect(spam_logs[0].description).to eq('content here')
- expect(spam_logs[0].user).to eq(user)
- expect(spam_logs[0].noteable_type).to eq('Issue')
+ it 'does not update a project issue' do
+ expect { update_issue }.not_to change { issue.reload.title }
+ end
+
+ it 'returns correct status and message' do
+ update_issue
+
+ expect(response).to have_gitlab_http_status(400)
+ expect(json_response).to include('message' => { 'error' => 'Spam detected' })
+ end
+
+ it 'creates a new spam log entry' do
+ expect { update_issue }
+ .to log_spam(title: 'updated title', description: 'content here', user_id: user.id, noteable_type: 'Issue')
+ end
+ end
+
+ context 'when allow_possible_spam feature flag is true' do
+ it 'updates a project issue' do
+ expect { update_issue }.to change { issue.reload.title }
+ end
+
+ it 'returns correct status and message' do
+ update_issue
+
+ expect(response).to have_gitlab_http_status(200)
+ end
+
+ it 'creates a new spam log entry' do
+ expect { update_issue }
+ .to log_spam(title: 'updated title', description: 'content here', user_id: user.id, noteable_type: 'Issue')
+ end
end
end
diff --git a/spec/requests/api/labels_spec.rb b/spec/requests/api/labels_spec.rb
index 9aef67e28a7..7089da3d351 100644
--- a/spec/requests/api/labels_spec.rb
+++ b/spec/requests/api/labels_spec.rb
@@ -1,139 +1,122 @@
require 'spec_helper'
describe API::Labels do
+ def put_labels_api(route_type, user, spec_params, request_params = {})
+ if route_type == :deprecated
+ put api("/projects/#{project.id}/labels", user),
+ params: request_params.merge(spec_params)
+ else
+ label_id = spec_params[:name] || spec_params[:label_id]
+
+ put api("/projects/#{project.id}/labels/#{label_id}", user),
+ params: request_params.merge(spec_params.except(:name, :id))
+ end
+ end
+
let(:user) { create(:user) }
let(:project) { create(:project, creator_id: user.id, namespace: user.namespace) }
let!(:label1) { create(:label, title: 'label1', project: project) }
let!(:priority_label) { create(:label, title: 'bug', project: project, priority: 3) }
- shared_examples 'label update API' do
- it 'returns 200 if name is changed' do
- request_params = {
- new_name: 'New Label'
- }.merge(spec_params)
-
- put api("/projects/#{project.id}/labels", user),
- params: request_params
-
- expect(response).to have_gitlab_http_status(200)
- expect(json_response['name']).to eq('New Label')
- expect(json_response['color']).to eq(label1.color)
- end
+ route_types = [:deprecated, :rest]
- it 'returns 200 if colors is changed' do
- request_params = {
- color: '#FFFFFF'
- }.merge(spec_params)
+ shared_examples 'label update API' do
+ route_types.each do |route_type|
+ it "returns 200 if name is changed (#{route_type} route)" do
+ put_labels_api(route_type, user, spec_params, new_name: 'New Label')
- put api("/projects/#{project.id}/labels", user),
- params: request_params
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response['name']).to eq('New Label')
+ expect(json_response['color']).to eq(label1.color)
+ end
- expect(response).to have_gitlab_http_status(200)
- expect(json_response['name']).to eq(label1.name)
- expect(json_response['color']).to eq('#FFFFFF')
- end
+ it "returns 200 if colors is changed (#{route_type} route)" do
+ put_labels_api(route_type, user, spec_params, color: '#FFFFFF')
- it 'returns 200 if a priority is added' do
- request_params = {
- priority: 3
- }.merge(spec_params)
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response['name']).to eq(label1.name)
+ expect(json_response['color']).to eq('#FFFFFF')
+ end
- put api("/projects/#{project.id}/labels", user),
- params: request_params
+ it "returns 200 if a priority is added (#{route_type} route)" do
+ put_labels_api(route_type, user, spec_params, priority: 3)
- expect(response.status).to eq(200)
- expect(json_response['name']).to eq(label1.name)
- expect(json_response['priority']).to eq(3)
- end
+ expect(response.status).to eq(200)
+ expect(json_response['name']).to eq(label1.name)
+ expect(json_response['priority']).to eq(3)
+ end
- it 'returns 400 if no new parameters given' do
- put api("/projects/#{project.id}/labels", user), params: spec_params
+ it "returns 400 if no new parameters given (#{route_type} route)" do
+ put_labels_api(route_type, user, spec_params)
- expect(response).to have_gitlab_http_status(400)
- expect(json_response['error']).to eq('new_name, color, description, priority are missing, '\
- 'at least one parameter must be provided')
- end
+ expect(response).to have_gitlab_http_status(400)
+ expect(json_response['error']).to eq('new_name, color, description, priority are missing, '\
+ 'at least one parameter must be provided')
+ end
- it 'returns 400 when color code is too short' do
- request_params = {
- color: '#FF'
- }.merge(spec_params)
+ it "returns 400 when color code is too short (#{route_type} route)" do
+ put_labels_api(route_type, user, spec_params, color: '#FF')
- put api("/projects/#{project.id}/labels", user),
- params: request_params
+ expect(response).to have_gitlab_http_status(400)
+ expect(json_response['message']['color']).to eq(['must be a valid color code'])
+ end
- expect(response).to have_gitlab_http_status(400)
- expect(json_response['message']['color']).to eq(['must be a valid color code'])
- end
+ it "returns 400 for too long color code (#{route_type} route)" do
+ put_labels_api(route_type, user, spec_params, color: '#FFAAFFFF')
- it 'returns 400 for too long color code' do
- request_params = {
- color: '#FFAAFFFF'
- }.merge(spec_params)
+ expect(response).to have_gitlab_http_status(400)
+ expect(json_response['message']['color']).to eq(['must be a valid color code'])
+ end
- put api("/projects/#{project.id}/labels", user),
- params: request_params
+ it "returns 400 for invalid priority (#{route_type} route)" do
+ put_labels_api(route_type, user, spec_params, priority: 'foo')
- expect(response).to have_gitlab_http_status(400)
- expect(json_response['message']['color']).to eq(['must be a valid color code'])
- end
-
- it 'returns 400 for invalid priority' do
- request_params = {
- priority: 'foo'
- }.merge(spec_params)
+ expect(response).to have_gitlab_http_status(400)
+ end
- put api("/projects/#{project.id}/labels", user),
- params: request_params
+ it "returns 200 if name and colors and description are changed (#{route_type} route)" do
+ put_labels_api(route_type, user, spec_params, new_name: 'New Label', color: '#FFFFFF', description: 'test')
- expect(response).to have_gitlab_http_status(400)
- end
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response['name']).to eq('New Label')
+ expect(json_response['color']).to eq('#FFFFFF')
+ expect(json_response['description']).to eq('test')
+ end
- it 'returns 200 if name and colors and description are changed' do
- request_params = {
- new_name: 'New Label',
- color: '#FFFFFF',
- description: 'test'
- }.merge(spec_params)
+ it "returns 400 for invalid name (#{route_type} route)" do
+ put_labels_api(route_type, user, spec_params, new_name: ',', color: '#FFFFFF')
- put api("/projects/#{project.id}/labels", user),
- params: request_params
+ expect(response).to have_gitlab_http_status(400)
+ expect(json_response['message']['title']).to eq(['is invalid'])
+ end
- expect(response).to have_gitlab_http_status(200)
- expect(json_response['name']).to eq('New Label')
- expect(json_response['color']).to eq('#FFFFFF')
- expect(json_response['description']).to eq('test')
- end
+ it "returns 200 if description is changed (#{route_type} route)" do
+ put_labels_api(route_type, user, spec_params, description: 'test')
- it 'returns 400 for invalid name' do
- request_params = {
- new_name: ',',
- color: '#FFFFFF'
- }.merge(spec_params)
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response['id']).to eq(expected_response_label_id)
+ expect(json_response['description']).to eq('test')
+ end
- put api("/projects/#{project.id}/labels", user),
- params: request_params
+ it "returns 200 if priority is changed (#{route_type} route)" do
+ put_labels_api(route_type, user, spec_params, priority: 10)
- expect(response).to have_gitlab_http_status(400)
- expect(json_response['message']['title']).to eq(['is invalid'])
+ expect(response.status).to eq(200)
+ expect(json_response['id']).to eq(expected_response_label_id)
+ expect(json_response['priority']).to eq(10)
+ end
end
- it 'returns 200 if description is changed' do
- request_params = {
- description: 'test'
- }.merge(spec_params)
+ it 'returns 200 if a priority is removed (deprecated route)' do
+ label = find_by_spec_params(spec_params)
- put api("/projects/#{project.id}/labels", user),
- params: request_params
+ expect(label).not_to be_nil
- expect(response).to have_gitlab_http_status(200)
- expect(json_response['id']).to eq(expected_response_label_id)
- expect(json_response['description']).to eq('test')
- end
+ label.priorities.create(project: label.project, priority: 1)
+ label.save!
- it 'returns 200 if priority is changed' do
request_params = {
- priority: 10
+ priority: nil
}.merge(spec_params)
put api("/projects/#{project.id}/labels", user),
@@ -141,21 +124,22 @@ describe API::Labels do
expect(response.status).to eq(200)
expect(json_response['id']).to eq(expected_response_label_id)
- expect(json_response['priority']).to eq(10)
+ expect(json_response['priority']).to be_nil
end
- it 'returns 200 if a priority is removed' do
+ it 'returns 200 if a priority is removed (rest route)' do
label = find_by_spec_params(spec_params)
expect(label).not_to be_nil
+ label_id = spec_params[:name] || spec_params[:label_id]
label.priorities.create(project: label.project, priority: 1)
label.save!
request_params = {
priority: nil
- }.merge(spec_params)
+ }.merge(spec_params.except(:name, :id))
- put api("/projects/#{project.id}/labels", user),
+ put api("/projects/#{project.id}/labels/#{label_id}", user),
params: request_params
expect(response.status).to eq(200)
@@ -173,11 +157,18 @@ describe API::Labels do
end
shared_examples 'label delete API' do
- it 'returns 204 for existing label' do
+ it 'returns 204 for existing label (deprecated route)' do
delete api("/projects/#{project.id}/labels", user), params: spec_params
expect(response).to have_gitlab_http_status(204)
end
+
+ it 'returns 204 for existing label (rest route)' do
+ label_id = spec_params[:name] || spec_params[:label_id]
+ delete api("/projects/#{project.id}/labels/#{label_id}", user), params: spec_params.except(:name, :label_id)
+
+ expect(response).to have_gitlab_http_status(204)
+ end
end
before do
@@ -256,6 +247,52 @@ describe API::Labels do
'is_project_label' => true)
end
end
+
+ context 'when the include_ancestor_groups parameter is not set' do
+ let(:group) { create(:group) }
+ let!(:group_label) { create(:group_label, title: 'feature', group: group) }
+ let(:subgroup) { create(:group, parent: group) }
+ let!(:subgroup_label) { create(:group_label, title: 'support', group: subgroup) }
+
+ before do
+ subgroup.add_owner(user)
+ project.update!(group: subgroup)
+ end
+
+ it 'returns all available labels for the project, parent group and ancestor groups' do
+ get api("/projects/#{project.id}/labels", user)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ expect(json_response).to all(match_schema('public_api/v4/labels/label'))
+ expect(json_response.size).to eq(4)
+ expect(json_response.map {|r| r['name'] }).to contain_exactly(group_label.name, subgroup_label.name, priority_label.name, label1.name)
+ end
+ end
+
+ context 'when the include_ancestor_groups parameter is set to false' do
+ let(:group) { create(:group) }
+ let!(:group_label) { create(:group_label, title: 'feature', group: group) }
+ let(:subgroup) { create(:group, parent: group) }
+ let!(:subgroup_label) { create(:group_label, title: 'support', group: subgroup) }
+
+ before do
+ subgroup.add_owner(user)
+ project.update!(group: subgroup)
+ end
+
+ it 'returns all available labels for the project and the parent group only' do
+ get api("/projects/#{project.id}/labels", user), params: { include_ancestor_groups: false }
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ expect(json_response).to all(match_schema('public_api/v4/labels/label'))
+ expect(json_response.size).to eq(3)
+ expect(json_response.map {|r| r['name'] }).to contain_exactly(subgroup_label.name, priority_label.name, label1.name)
+ end
+ end
end
describe 'POST /projects/:id/labels' do
diff --git a/spec/requests/api/members_spec.rb b/spec/requests/api/members_spec.rb
index 26f6e705528..eb55d747179 100644
--- a/spec/requests/api/members_spec.rb
+++ b/spec/requests/api/members_spec.rb
@@ -7,7 +7,7 @@ describe API::Members do
let(:stranger) { create(:user) }
let(:project) do
- create(:project, :public, :access_requestable, creator_id: maintainer.id, namespace: maintainer.namespace) do |project|
+ create(:project, :public, creator_id: maintainer.id, namespace: maintainer.namespace) do |project|
project.add_developer(developer)
project.add_maintainer(maintainer)
project.request_access(access_requester)
@@ -15,7 +15,7 @@ describe API::Members do
end
let!(:group) do
- create(:group, :public, :access_requestable) do |group|
+ create(:group, :public) do |group|
group.add_developer(developer)
group.add_owner(maintainer)
group.request_access(access_requester)
@@ -87,6 +87,15 @@ describe API::Members do
expect(json_response.first['username']).to eq(maintainer.username)
end
+ it 'finds members with the given user_ids' do
+ get api(members_url, developer), params: { user_ids: [maintainer.id, developer.id, stranger.id] }
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ expect(json_response.map { |u| u['id'] }).to contain_exactly(maintainer.id, developer.id)
+ end
+
it 'finds all members with no query specified' do
get api(members_url, developer), params: { query: '' }
@@ -155,10 +164,10 @@ describe API::Members do
end
end
- shared_examples 'GET /:source_type/:id/members/:user_id' do |source_type|
- context "with :source_type == #{source_type.pluralize}" do
+ shared_examples 'GET /:source_type/:id/members/(all/):user_id' do |source_type, all|
+ context "with :source_type == #{source_type.pluralize} and all == #{all}" do
it_behaves_like 'a 404 response when source is private' do
- let(:route) { get api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", stranger) }
+ let(:route) { get api("/#{source_type.pluralize}/#{source.id}/members/#{all ? 'all/' : ''}#{developer.id}", stranger) }
end
context 'when authenticated as a non-member' do
@@ -166,7 +175,7 @@ describe API::Members do
context "as a #{type}" do
it 'returns 200' do
user = public_send(type)
- get api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", user)
+ get api("/#{source_type.pluralize}/#{source.id}/members/#{all ? 'all/' : ''}#{developer.id}", user)
expect(response).to have_gitlab_http_status(200)
# User attributes
@@ -434,12 +443,14 @@ describe API::Members do
end
end
- it_behaves_like 'GET /:source_type/:id/members/:user_id', 'project' do
- let(:source) { project }
- end
+ [false, true].each do |all|
+ it_behaves_like 'GET /:source_type/:id/members/(all/):user_id', 'project', all do
+ let(:source) { all ? create(:project, :public, group: group) : project }
+ end
- it_behaves_like 'GET /:source_type/:id/members/:user_id', 'group' do
- let(:source) { group }
+ it_behaves_like 'GET /:source_type/:id/members/(all/):user_id', 'group', all do
+ let(:source) { all ? create(:group, parent: group) : group }
+ end
end
it_behaves_like 'POST /:source_type/:id/members', 'project' do
diff --git a/spec/requests/api/pages/internal_access_spec.rb b/spec/requests/api/pages/internal_access_spec.rb
index c41eabe0a48..28abe1a8456 100644
--- a/spec/requests/api/pages/internal_access_spec.rb
+++ b/spec/requests/api/pages/internal_access_spec.rb
@@ -27,6 +27,7 @@ describe "Internal Project Pages Access" do
describe "Project should be internal" do
describe '#internal?' do
subject { project.internal? }
+
it { is_expected.to be_truthy }
end
end
diff --git a/spec/requests/api/pages/private_access_spec.rb b/spec/requests/api/pages/private_access_spec.rb
index c647537038e..6af441caf74 100644
--- a/spec/requests/api/pages/private_access_spec.rb
+++ b/spec/requests/api/pages/private_access_spec.rb
@@ -27,6 +27,7 @@ describe "Private Project Pages Access" do
describe "Project should be private" do
describe '#private?' do
subject { project.private? }
+
it { is_expected.to be_truthy }
end
end
diff --git a/spec/requests/api/pages/public_access_spec.rb b/spec/requests/api/pages/public_access_spec.rb
index 16cc5697f30..d99224eca5b 100644
--- a/spec/requests/api/pages/public_access_spec.rb
+++ b/spec/requests/api/pages/public_access_spec.rb
@@ -27,6 +27,7 @@ describe "Public Project Pages Access" do
describe "Project should be public" do
describe '#public?' do
subject { project.public? }
+
it { is_expected.to be_truthy }
end
end
diff --git a/spec/requests/api/pipelines_spec.rb b/spec/requests/api/pipelines_spec.rb
index 3a3f0e970a4..3ac63dc381b 100644
--- a/spec/requests/api/pipelines_spec.rb
+++ b/spec/requests/api/pipelines_spec.rb
@@ -29,7 +29,7 @@ describe API::Pipelines do
expect(json_response.first['sha']).to match /\A\h{40}\z/
expect(json_response.first['id']).to eq pipeline.id
expect(json_response.first['web_url']).to be_present
- expect(json_response.first.keys).to contain_exactly(*%w[id sha ref status web_url])
+ expect(json_response.first.keys).to contain_exactly(*%w[id sha ref status web_url created_at updated_at])
end
context 'when parameter is passed' do
diff --git a/spec/requests/api/project_container_repositories_spec.rb b/spec/requests/api/project_container_repositories_spec.rb
index f1dc4e6f0b2..3ac7ff7656b 100644
--- a/spec/requests/api/project_container_repositories_spec.rb
+++ b/spec/requests/api/project_container_repositories_spec.rb
@@ -150,7 +150,7 @@ describe API::ProjectContainerRepositories do
expect(response).to have_gitlab_http_status(:accepted)
end
- context 'called multiple times in one hour' do
+ context 'called multiple times in one hour', :clean_gitlab_redis_shared_state do
it 'returns 400 with an error message' do
stub_exclusive_lease_taken(lease_key, timeout: 1.hour)
subject
@@ -202,6 +202,8 @@ describe API::ProjectContainerRepositories do
end
describe 'DELETE /projects/:id/registry/repositories/:repository_id/tags/:tag_name' do
+ let(:service) { double('service') }
+
subject { delete api("/projects/#{project.id}/registry/repositories/#{root_repository.id}/tags/rootA", api_user) }
it_behaves_like 'rejected container repository access', :reporter, :forbidden
@@ -210,18 +212,34 @@ describe API::ProjectContainerRepositories do
context 'for developer' do
let(:api_user) { developer }
- before do
- stub_container_registry_tags(repository: root_repository.path, tags: %w(rootA), with_manifest: true)
+ context 'when there are multiple tags' do
+ before do
+ stub_container_registry_tags(repository: root_repository.path, tags: %w(rootA rootB), with_manifest: true)
+ end
+
+ it 'properly removes tag' do
+ expect(service).to receive(:execute).with(root_repository) { { status: :success } }
+ expect(Projects::ContainerRepository::DeleteTagsService).to receive(:new).with(root_repository.project, api_user, tags: %w[rootA]) { service }
+
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
end
- it 'properly removes tag' do
- expect_any_instance_of(ContainerRegistry::Client)
- .to receive(:delete_repository_tag).with(root_repository.path,
- 'sha256:4c8e63ca4cb663ce6c688cb06f1c372b088dac5b6d7ad7d49cd620d85cf72a15')
+ context 'when there\'s only one tag' do
+ before do
+ stub_container_registry_tags(repository: root_repository.path, tags: %w(rootA), with_manifest: true)
+ end
- subject
+ it 'properly removes tag' do
+ expect(service).to receive(:execute).with(root_repository) { { status: :success } }
+ expect(Projects::ContainerRepository::DeleteTagsService).to receive(:new).with(root_repository.project, api_user, tags: %w[rootA]) { service }
- expect(response).to have_gitlab_http_status(:ok)
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
end
end
end
diff --git a/spec/requests/api/project_export_spec.rb b/spec/requests/api/project_export_spec.rb
index 1d2f81a397d..7de8935097a 100644
--- a/spec/requests/api/project_export_spec.rb
+++ b/spec/requests/api/project_export_spec.rb
@@ -30,7 +30,7 @@ describe API::ProjectExport do
FileUtils.mkdir_p File.join(project_started.export_path, 'securerandom-hex')
# simulate in after export action
- FileUtils.touch Gitlab::ImportExport::AfterExportStrategies::BaseAfterExportStrategy.lock_file_path(project_after_export)
+ FileUtils.touch File.join(project_after_export.import_export_shared.lock_files_path, SecureRandom.hex)
end
after do
diff --git a/spec/requests/api/project_import_spec.rb b/spec/requests/api/project_import_spec.rb
index 594b42bb6c0..d2b1fb063b8 100644
--- a/spec/requests/api/project_import_spec.rb
+++ b/spec/requests/api/project_import_spec.rb
@@ -33,6 +33,53 @@ describe API::ProjectImport do
expect(response).to have_gitlab_http_status(201)
end
+ context 'when a name is explicitly set' do
+ let(:expected_name) { 'test project import' }
+
+ it 'schedules an import using a namespace and a different name' do
+ stub_import(namespace)
+
+ post api('/projects/import', user), params: { path: 'test-import', file: fixture_file_upload(file), namespace: namespace.id, name: expected_name }
+
+ expect(response).to have_gitlab_http_status(201)
+ end
+
+ it 'schedules an import using the namespace path and a different name' do
+ stub_import(namespace)
+
+ post api('/projects/import', user), params: { path: 'test-import', file: fixture_file_upload(file), namespace: namespace.full_path, name: expected_name }
+
+ expect(response).to have_gitlab_http_status(201)
+ end
+
+ it 'sets name correctly' do
+ stub_import(namespace)
+
+ post api('/projects/import', user), params: { path: 'test-import', file: fixture_file_upload(file), namespace: namespace.full_path, name: expected_name }
+
+ project = Project.find(json_response['id'])
+ expect(project.name).to eq(expected_name)
+ end
+
+ it 'sets name correctly with an overwrite' do
+ stub_import(namespace)
+
+ post api('/projects/import', user), params: { path: 'test-import', file: fixture_file_upload(file), namespace: namespace.full_path, name: 'new project name', overwrite: true }
+
+ project = Project.find(json_response['id'])
+ expect(project.name).to eq('new project name')
+ end
+
+ it 'schedules an import using the path and name explicitly set to nil' do
+ stub_import(namespace)
+
+ post api('/projects/import', user), params: { path: 'test-import', file: fixture_file_upload(file), namespace: namespace.full_path, name: nil }
+
+ project = Project.find(json_response['id'])
+ expect(project.name).to eq('test-import')
+ end
+ end
+
it 'schedules an import at the user namespace level' do
stub_import(user.namespace)
diff --git a/spec/requests/api/project_snippets_spec.rb b/spec/requests/api/project_snippets_spec.rb
index 2e6e13aa927..ef0cabad4b0 100644
--- a/spec/requests/api/project_snippets_spec.rb
+++ b/spec/requests/api/project_snippets_spec.rb
@@ -198,7 +198,7 @@ describe API::ProjectSnippets do
it 'creates a spam log' do
expect { create_snippet(project, visibility: 'public') }
- .to change { SpamLog.count }.by(1)
+ .to log_spam(title: 'Test Title', user_id: user.id, noteable_type: 'ProjectSnippet')
end
end
end
@@ -289,7 +289,7 @@ describe API::ProjectSnippets do
it 'creates a spam log' do
expect { update_snippet(title: 'Foo') }
- .to change { SpamLog.count }.by(1)
+ .to log_spam(title: 'Foo', user_id: admin.id, noteable_type: 'ProjectSnippet')
end
end
@@ -306,7 +306,7 @@ describe API::ProjectSnippets do
it 'creates a spam log' do
expect { update_snippet(title: 'Foo', visibility: 'public') }
- .to change { SpamLog.count }.by(1)
+ .to log_spam(title: 'Foo', user_id: admin.id, noteable_type: 'ProjectSnippet')
end
end
end
diff --git a/spec/requests/api/releases_spec.rb b/spec/requests/api/releases_spec.rb
index 206e898381d..99d0ceee76b 100644
--- a/spec/requests/api/releases_spec.rb
+++ b/spec/requests/api/releases_spec.rb
@@ -54,6 +54,31 @@ describe API::Releases do
expect(response).to match_response_schema('public_api/v4/releases')
end
+
+ it 'returns rendered helper paths' do
+ get api("/projects/#{project.id}/releases", maintainer)
+
+ expect(json_response.first['commit_path']).to eq("/#{release_2.project.full_path}/commit/#{release_2.commit.id}")
+ expect(json_response.first['tag_path']).to eq("/#{release_2.project.full_path}/-/tags/#{release_2.tag}")
+ expect(json_response.second['commit_path']).to eq("/#{release_1.project.full_path}/commit/#{release_1.commit.id}")
+ expect(json_response.second['tag_path']).to eq("/#{release_1.project.full_path}/-/tags/#{release_1.tag}")
+ end
+
+ it 'returns the merge requests and issues links, with correct query' do
+ get api("/projects/#{project.id}/releases", maintainer)
+
+ links = json_response.first['_links']
+ release = json_response.first['tag_name']
+ expected_query = "release_tag=#{release}&scope=all&state=opened"
+ path_base = "/#{project.namespace.path}/#{project.path}"
+ mr_uri = URI.parse(links['merge_requests_url'])
+ issue_uri = URI.parse(links['issues_url'])
+
+ expect(mr_uri.path).to eq("#{path_base}/merge_requests")
+ expect(issue_uri.path).to eq("#{path_base}/issues")
+ expect(mr_uri.query).to eq(expected_query)
+ expect(issue_uri.query).to eq(expected_query)
+ end
end
it 'returns an upcoming_release status for a future release' do
@@ -103,11 +128,13 @@ describe API::Releases do
expect(response).to have_gitlab_http_status(:ok)
end
- it "does not expose tag, commit and source code" do
+ it "does not expose tag, commit, source code or helper paths" do
get api("/projects/#{project.id}/releases", guest)
expect(response).to match_response_schema('public_api/v4/release/releases_for_guest')
expect(json_response[0]['assets']['count']).to eq(release.links.count)
+ expect(json_response[0]['commit_path']).to be_nil
+ expect(json_response[0]['tag_path']).to be_nil
end
context 'when project is public' do
@@ -119,11 +146,13 @@ describe API::Releases do
expect(response).to have_gitlab_http_status(:ok)
end
- it "exposes tag, commit and source code" do
+ it "exposes tag, commit, source code and helper paths" do
get api("/projects/#{project.id}/releases", guest)
expect(response).to match_response_schema('public_api/v4/releases')
- expect(json_response[0]['assets']['count']).to eq(release.links.count + release.sources.count)
+ expect(json_response.first['assets']['count']).to eq(release.links.count + release.sources.count)
+ expect(json_response.first['commit_path']).to eq("/#{release.project.full_path}/commit/#{release.commit.id}")
+ expect(json_response.first['tag_path']).to eq("/#{release.project.full_path}/-/tags/#{release.tag}")
end
end
end
@@ -172,6 +201,8 @@ describe API::Releases do
expect(json_response['author']['name']).to eq(maintainer.name)
expect(json_response['commit']['id']).to eq(commit.id)
expect(json_response['assets']['count']).to eq(4)
+ expect(json_response['commit_path']).to eq("/#{release.project.full_path}/commit/#{release.commit.id}")
+ expect(json_response['tag_path']).to eq("/#{release.project.full_path}/-/tags/#{release.tag}")
end
it 'matches response schema' do
diff --git a/spec/requests/api/runner_spec.rb b/spec/requests/api/runner_spec.rb
index d9ef5edb848..70a95663aea 100644
--- a/spec/requests/api/runner_spec.rb
+++ b/spec/requests/api/runner_spec.rb
@@ -3,6 +3,7 @@ require 'spec_helper'
describe API::Runner, :clean_gitlab_redis_shared_state do
include StubGitlabCalls
include RedisHelpers
+ include WorkhorseHelpers
let(:registration_token) { 'abcdefg123456' }
@@ -308,7 +309,9 @@ describe API::Runner, :clean_gitlab_redis_shared_state do
end
describe '/api/v4/jobs' do
- let(:project) { create(:project, shared_runners_enabled: false) }
+ let(:root_namespace) { create(:namespace) }
+ let(:namespace) { create(:namespace, parent: root_namespace) }
+ let(:project) { create(:project, namespace: namespace, shared_runners_enabled: false) }
let(:pipeline) { create(:ci_pipeline_without_jobs, project: project, ref: 'master') }
let(:runner) { create(:ci_runner, :project, projects: [project]) }
let(:job) do
@@ -1393,7 +1396,7 @@ describe API::Runner, :clean_gitlab_redis_shared_state do
expect(response).to have_gitlab_http_status(200)
expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
- expect(json_response['TempPath']).to eq(JobArtifactUploader.workhorse_local_upload_path)
+ expect(json_response).not_to have_key('TempPath')
expect(json_response['RemoteObject']).to have_key('ID')
expect(json_response['RemoteObject']).to have_key('GetURL')
expect(json_response['RemoteObject']).to have_key('StoreURL')
@@ -1412,12 +1415,54 @@ describe API::Runner, :clean_gitlab_redis_shared_state do
end
end
- it 'fails to post too large artifact' do
- stub_application_setting(max_artifacts_size: 0)
+ context 'when artifact is too large' do
+ let(:sample_max_size) { 100 }
- authorize_artifacts_with_token_in_params(filesize: 100)
+ shared_examples_for 'rejecting too large artifacts' do
+ it 'fails to post' do
+ authorize_artifacts_with_token_in_params(filesize: sample_max_size.megabytes.to_i)
- expect(response).to have_gitlab_http_status(413)
+ expect(response).to have_gitlab_http_status(413)
+ end
+ end
+
+ context 'based on application setting' do
+ before do
+ stub_application_setting(max_artifacts_size: sample_max_size)
+ end
+
+ it_behaves_like 'rejecting too large artifacts'
+ end
+
+ context 'based on root namespace setting' do
+ before do
+ stub_application_setting(max_artifacts_size: 200)
+ root_namespace.update!(max_artifacts_size: sample_max_size)
+ end
+
+ it_behaves_like 'rejecting too large artifacts'
+ end
+
+ context 'based on child namespace setting' do
+ before do
+ stub_application_setting(max_artifacts_size: 200)
+ root_namespace.update!(max_artifacts_size: 200)
+ namespace.update!(max_artifacts_size: sample_max_size)
+ end
+
+ it_behaves_like 'rejecting too large artifacts'
+ end
+
+ context 'based on project setting' do
+ before do
+ stub_application_setting(max_artifacts_size: 200)
+ root_namespace.update!(max_artifacts_size: 200)
+ namespace.update!(max_artifacts_size: 200)
+ project.update!(max_artifacts_size: sample_max_size)
+ end
+
+ it_behaves_like 'rejecting too large artifacts'
+ end
end
end
@@ -1518,15 +1563,16 @@ describe API::Runner, :clean_gitlab_redis_shared_state do
let!(:fog_connection) do
stub_artifacts_object_storage(direct_upload: true)
end
-
- before do
+ let(:object) do
fog_connection.directories.new(key: 'artifacts').files.create(
key: 'tmp/uploads/12312300',
body: 'content'
)
+ end
+ let(:file_upload) { fog_to_uploaded_file(object) }
- upload_artifacts(file_upload, headers_with_token,
- { 'file.remote_id' => remote_id })
+ before do
+ upload_artifacts(file_upload, headers_with_token, 'file.remote_id' => remote_id)
end
context 'when valid remote_id is used' do
@@ -1760,12 +1806,13 @@ describe API::Runner, :clean_gitlab_redis_shared_state do
end
def upload_artifacts(file, headers = {}, params = {})
- params = params.merge({
- 'file.path' => file.path,
- 'file.name' => file.original_filename
- })
-
- post api("/jobs/#{job.id}/artifacts"), params: params, headers: headers
+ workhorse_finalize(
+ api("/jobs/#{job.id}/artifacts"),
+ method: :post,
+ file_key: :file,
+ params: params.merge(file: file),
+ headers: headers
+ )
end
end
diff --git a/spec/requests/api/settings_spec.rb b/spec/requests/api/settings_spec.rb
index d98b9be726a..f3bfb258029 100644
--- a/spec/requests/api/settings_spec.rb
+++ b/spec/requests/api/settings_spec.rb
@@ -72,7 +72,9 @@ describe API::Settings, 'Settings' do
default_branch_protection: ::Gitlab::Access::PROTECTION_DEV_CAN_MERGE,
local_markdown_version: 3,
allow_local_requests_from_web_hooks_and_services: true,
- allow_local_requests_from_system_hooks: false
+ allow_local_requests_from_system_hooks: false,
+ push_event_hooks_limit: 2,
+ push_event_activities_limit: 2
}
expect(response).to have_gitlab_http_status(200)
@@ -102,6 +104,8 @@ describe API::Settings, 'Settings' do
expect(json_response['local_markdown_version']).to eq(3)
expect(json_response['allow_local_requests_from_web_hooks_and_services']).to eq(true)
expect(json_response['allow_local_requests_from_system_hooks']).to eq(false)
+ expect(json_response['push_event_hooks_limit']).to eq(2)
+ expect(json_response['push_event_activities_limit']).to eq(2)
end
end
diff --git a/spec/requests/api/snippets_spec.rb b/spec/requests/api/snippets_spec.rb
index 515912cb305..e7eaaea2418 100644
--- a/spec/requests/api/snippets_spec.rb
+++ b/spec/requests/api/snippets_spec.rb
@@ -254,7 +254,7 @@ describe API::Snippets do
it 'creates a spam log' do
expect { create_snippet(visibility: 'public') }
- .to change { SpamLog.count }.by(1)
+ .to log_spam(title: 'Test Title', user_id: user.id, noteable_type: 'PersonalSnippet')
end
end
end
@@ -344,8 +344,7 @@ describe API::Snippets do
end
it 'creates a spam log' do
- expect { update_snippet(title: 'Foo') }
- .to change { SpamLog.count }.by(1)
+ expect { update_snippet(title: 'Foo') }.to log_spam(title: 'Foo', user_id: user.id, noteable_type: 'PersonalSnippet')
end
end
@@ -359,7 +358,7 @@ describe API::Snippets do
it 'creates a spam log' do
expect { update_snippet(title: 'Foo', visibility: 'public') }
- .to change { SpamLog.count }.by(1)
+ .to log_spam(title: 'Foo', user_id: user.id, noteable_type: 'PersonalSnippet')
end
end
end
diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb
index af2bee4563a..ee4e783e9ac 100644
--- a/spec/requests/api/users_spec.rb
+++ b/spec/requests/api/users_spec.rb
@@ -634,10 +634,28 @@ describe API::Users do
end
describe "GET /users/sign_up" do
- it "redirects to sign in page" do
- get "/users/sign_up"
- expect(response).to have_gitlab_http_status(302)
- expect(response).to redirect_to(new_user_session_path)
+ context 'when experimental signup_flow is active' do
+ before do
+ stub_experiment(signup_flow: true)
+ end
+
+ it "shows sign up page" do
+ get "/users/sign_up"
+ expect(response).to have_gitlab_http_status(200)
+ expect(response).to render_template(:new)
+ end
+ end
+
+ context 'when experimental signup_flow is not active' do
+ before do
+ stub_experiment(signup_flow: false)
+ end
+
+ it "redirects to sign in page" do
+ get "/users/sign_up"
+ expect(response).to have_gitlab_http_status(302)
+ expect(response).to redirect_to(new_user_session_path(anchor: 'register-pane'))
+ end
end
end
@@ -1809,6 +1827,182 @@ describe API::Users do
end
end
+ context 'activate and deactivate' do
+ shared_examples '404' do
+ it 'returns 404' do
+ expect(response).to have_gitlab_http_status(404)
+ expect(json_response['message']).to eq('404 User Not Found')
+ end
+ end
+
+ describe 'POST /users/:id/activate' do
+ context 'performed by a non-admin user' do
+ it 'is not authorized to perform the action' do
+ post api("/users/#{user.id}/activate", user)
+
+ expect(response).to have_gitlab_http_status(403)
+ end
+ end
+
+ context 'performed by an admin user' do
+ context 'for a deactivated user' do
+ before do
+ user.deactivate
+
+ post api("/users/#{user.id}/activate", admin)
+ end
+
+ it 'activates a deactivated user' do
+ expect(response).to have_gitlab_http_status(201)
+ expect(user.reload.state).to eq('active')
+ end
+ end
+
+ context 'for an active user' do
+ before do
+ user.activate
+
+ post api("/users/#{user.id}/activate", admin)
+ end
+
+ it 'returns 201' do
+ expect(response).to have_gitlab_http_status(201)
+ expect(user.reload.state).to eq('active')
+ end
+ end
+
+ context 'for a blocked user' do
+ before do
+ user.block
+
+ post api("/users/#{user.id}/activate", admin)
+ end
+
+ it 'returns 403' do
+ expect(response).to have_gitlab_http_status(403)
+ expect(json_response['message']).to eq('403 Forbidden - A blocked user must be unblocked to be activated')
+ expect(user.reload.state).to eq('blocked')
+ end
+ end
+
+ context 'for a ldap blocked user' do
+ before do
+ user.ldap_block
+
+ post api("/users/#{user.id}/activate", admin)
+ end
+
+ it 'returns 403' do
+ expect(response).to have_gitlab_http_status(403)
+ expect(json_response['message']).to eq('403 Forbidden - A blocked user must be unblocked to be activated')
+ expect(user.reload.state).to eq('ldap_blocked')
+ end
+ end
+
+ context 'for a user that does not exist' do
+ before do
+ post api("/users/0/activate", admin)
+ end
+
+ it_behaves_like '404'
+ end
+ end
+ end
+
+ describe 'POST /users/:id/deactivate' do
+ context 'performed by a non-admin user' do
+ it 'is not authorized to perform the action' do
+ post api("/users/#{user.id}/deactivate", user)
+
+ expect(response).to have_gitlab_http_status(403)
+ end
+ end
+
+ context 'performed by an admin user' do
+ context 'for an active user' do
+ let(:activity) { {} }
+ let(:user) { create(:user, username: 'user.with.dot', **activity) }
+
+ context 'with no recent activity' do
+ let(:activity) { { last_activity_on: ::User::MINIMUM_INACTIVE_DAYS.next.days.ago } }
+
+ before do
+ post api("/users/#{user.id}/deactivate", admin)
+ end
+
+ it 'deactivates an active user' do
+ expect(response).to have_gitlab_http_status(201)
+ expect(user.reload.state).to eq('deactivated')
+ end
+ end
+
+ context 'with recent activity' do
+ let(:activity) { { last_activity_on: ::User::MINIMUM_INACTIVE_DAYS.pred.days.ago } }
+
+ before do
+ post api("/users/#{user.id}/deactivate", admin)
+ end
+
+ it 'does not deactivate an active user' do
+ expect(response).to have_gitlab_http_status(403)
+ expect(json_response['message']).to eq("403 Forbidden - The user you are trying to deactivate has been active in the past #{::User::MINIMUM_INACTIVE_DAYS} days and cannot be deactivated")
+ expect(user.reload.state).to eq('active')
+ end
+ end
+ end
+
+ context 'for a deactivated user' do
+ before do
+ user.deactivate
+
+ post api("/users/#{user.id}/deactivate", admin)
+ end
+
+ it 'returns 201' do
+ expect(response).to have_gitlab_http_status(201)
+ expect(user.reload.state).to eq('deactivated')
+ end
+ end
+
+ context 'for a blocked user' do
+ before do
+ user.block
+
+ post api("/users/#{user.id}/deactivate", admin)
+ end
+
+ it 'returns 403' do
+ expect(response).to have_gitlab_http_status(403)
+ expect(json_response['message']).to eq('403 Forbidden - A blocked user cannot be deactivated by the API')
+ expect(user.reload.state).to eq('blocked')
+ end
+ end
+
+ context 'for a ldap blocked user' do
+ before do
+ user.ldap_block
+
+ post api("/users/#{user.id}/deactivate", admin)
+ end
+
+ it 'returns 403' do
+ expect(response).to have_gitlab_http_status(403)
+ expect(json_response['message']).to eq('403 Forbidden - A blocked user cannot be deactivated by the API')
+ expect(user.reload.state).to eq('ldap_blocked')
+ end
+ end
+
+ context 'for a user that does not exist' do
+ before do
+ post api("/users/0/deactivate", admin)
+ end
+
+ it_behaves_like '404'
+ end
+ end
+ end
+ end
+
describe 'POST /users/:id/block' do
before do
admin
@@ -1841,6 +2035,7 @@ describe API::Users do
describe 'POST /users/:id/unblock' do
let(:blocked_user) { create(:user, state: 'blocked') }
+ let(:deactivated_user) { create(:user, state: 'deactivated') }
before do
admin
@@ -1864,7 +2059,13 @@ describe API::Users do
expect(ldap_blocked_user.reload.state).to eq('ldap_blocked')
end
- it 'does not be available for non admin users' do
+ it 'does not unblock deactivated users' do
+ post api("/users/#{deactivated_user.id}/unblock", admin)
+ expect(response).to have_gitlab_http_status(403)
+ expect(deactivated_user.reload.state).to eq('deactivated')
+ end
+
+ it 'is not available for non admin users' do
post api("/users/#{user.id}/unblock", user)
expect(response).to have_gitlab_http_status(403)
expect(user.reload.state).to eq('active')
diff --git a/spec/requests/boards/lists_controller_spec.rb b/spec/requests/boards/lists_controller_spec.rb
new file mode 100644
index 00000000000..7451ad93efd
--- /dev/null
+++ b/spec/requests/boards/lists_controller_spec.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Boards::ListsController do
+ describe '#index' do
+ let(:board) { create(:board) }
+ let(:user) { board.project.owner }
+
+ it 'does not have N+1 queries' do
+ login_as(user)
+
+ # First request has more queries because we create the default `backlog` list
+ get board_lists_path(board)
+
+ create(:list, board: board)
+
+ control_count = ActiveRecord::QueryRecorder.new { get board_lists_path(board) }.count
+
+ create_list(:list, 5, board: board)
+
+ expect { get board_lists_path(board) }.not_to exceed_query_limit(control_count)
+ end
+ end
+end
diff --git a/spec/requests/lfs_http_spec.rb b/spec/requests/lfs_http_spec.rb
index dc25e4d808e..62b9ee1d361 100644
--- a/spec/requests/lfs_http_spec.rb
+++ b/spec/requests/lfs_http_spec.rb
@@ -1,10 +1,14 @@
+# frozen_string_literal: true
require 'spec_helper'
describe 'Git LFS API and storage' do
- include WorkhorseHelpers
+ include LfsHttpHelpers
include ProjectForksHelper
+ include WorkhorseHelpers
- let(:user) { create(:user) }
+ set(:project) { create(:project, :repository) }
+ set(:other_project) { create(:project, :repository) }
+ set(:user) { create(:user) }
let!(:lfs_object) { create(:lfs_object, :with_file) }
let(:headers) do
@@ -19,201 +23,163 @@ describe 'Git LFS API and storage' do
let(:sample_oid) { lfs_object.oid }
let(:sample_size) { lfs_object.size }
+ let(:sample_object) { { 'oid' => sample_oid, 'size' => sample_size } }
+ let(:non_existing_object_oid) { '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897' }
+ let(:non_existing_object_size) { 1575078 }
+ let(:non_existing_object) { { 'oid' => non_existing_object_oid, 'size' => non_existing_object_size } }
+ let(:multiple_objects) { [sample_object, non_existing_object] }
- describe 'when lfs is disabled' do
- let(:project) { create(:project) }
- let(:body) do
- {
- 'objects' => [
- { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897',
- 'size' => 1575078 },
- { 'oid' => sample_oid,
- 'size' => sample_size }
- ],
- 'operation' => 'upload'
- }
- end
+ let(:lfs_enabled) { true }
+
+ before do
+ stub_lfs_setting(enabled: lfs_enabled)
+ end
+
+ describe 'when LFS is disabled' do
+ let(:lfs_enabled) { false }
+ let(:body) { upload_body(multiple_objects) }
let(:authorization) { authorize_user }
before do
- allow(Gitlab.config.lfs).to receive(:enabled).and_return(false)
- post_lfs_json "#{project.http_url_to_repo}/info/lfs/objects/batch", body, headers
+ post_lfs_json batch_url(project), body, headers
end
- it 'responds with 501' do
- expect(response).to have_gitlab_http_status(501)
- expect(json_response).to include('message' => 'Git LFS is not enabled on this GitLab server, contact your admin.')
- end
+ it_behaves_like 'LFS http 501 response'
end
context 'project specific LFS settings' do
- let(:project) { create(:project) }
- let(:body) do
- {
- 'objects' => [
- { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897',
- 'size' => 1575078 },
- { 'oid' => sample_oid,
- 'size' => sample_size }
- ],
- 'operation' => 'upload'
- }
- end
+ let(:body) { upload_body(sample_object) }
let(:authorization) { authorize_user }
+ before do
+ project.add_maintainer(user)
+ project.update_attribute(:lfs_enabled, project_lfs_enabled)
+
+ subject
+ end
+
context 'with LFS disabled globally' do
- before do
- project.add_maintainer(user)
- allow(Gitlab.config.lfs).to receive(:enabled).and_return(false)
- end
+ let(:lfs_enabled) { false }
describe 'LFS disabled in project' do
- before do
- project.update_attribute(:lfs_enabled, false)
- end
+ let(:project_lfs_enabled) { false }
- it 'responds with a 501 message on upload' do
- post_lfs_json "#{project.http_url_to_repo}/info/lfs/objects/batch", body, headers
+ context 'when uploading' do
+ subject { post_lfs_json(batch_url(project), body, headers) }
- expect(response).to have_gitlab_http_status(501)
+ it_behaves_like 'LFS http 501 response'
end
- it 'responds with a 501 message on download' do
- get "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}", params: {}, headers: headers
+ context 'when downloading' do
+ subject { get(objects_url(project, sample_oid), params: {}, headers: headers) }
- expect(response).to have_gitlab_http_status(501)
+ it_behaves_like 'LFS http 501 response'
end
end
describe 'LFS enabled in project' do
- before do
- project.update_attribute(:lfs_enabled, true)
- end
+ let(:project_lfs_enabled) { true }
- it 'responds with a 501 message on upload' do
- post_lfs_json "#{project.http_url_to_repo}/info/lfs/objects/batch", body, headers
+ context 'when uploading' do
+ subject { post_lfs_json(batch_url(project), body, headers) }
- expect(response).to have_gitlab_http_status(501)
+ it_behaves_like 'LFS http 501 response'
end
- it 'responds with a 501 message on download' do
- get "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}", params: {}, headers: headers
+ context 'when downloading' do
+ subject { get(objects_url(project, sample_oid), params: {}, headers: headers) }
- expect(response).to have_gitlab_http_status(501)
+ it_behaves_like 'LFS http 501 response'
end
end
end
context 'with LFS enabled globally' do
- before do
- project.add_maintainer(user)
- enable_lfs
- end
-
describe 'LFS disabled in project' do
- before do
- project.update_attribute(:lfs_enabled, false)
- end
+ let(:project_lfs_enabled) { false }
- it 'responds with a 403 message on upload' do
- post_lfs_json "#{project.http_url_to_repo}/info/lfs/objects/batch", body, headers
+ context 'when uploading' do
+ subject { post_lfs_json(batch_url(project), body, headers) }
- expect(response).to have_gitlab_http_status(403)
- expect(json_response).to include('message' => 'Access forbidden. Check your access level.')
+ it_behaves_like 'LFS http 403 response'
end
- it 'responds with a 403 message on download' do
- get "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}", params: {}, headers: headers
+ context 'when downloading' do
+ subject { get(objects_url(project, sample_oid), params: {}, headers: headers) }
- expect(response).to have_gitlab_http_status(403)
- expect(json_response).to include('message' => 'Access forbidden. Check your access level.')
+ it_behaves_like 'LFS http 403 response'
end
end
describe 'LFS enabled in project' do
- before do
- project.update_attribute(:lfs_enabled, true)
- end
+ let(:project_lfs_enabled) { true }
- it 'responds with a 200 message on upload' do
- post_lfs_json "#{project.http_url_to_repo}/info/lfs/objects/batch", body, headers
+ context 'when uploading' do
+ subject { post_lfs_json(batch_url(project), body, headers) }
- expect(response).to have_gitlab_http_status(200)
- expect(json_response['objects'].first['size']).to eq(1575078)
+ it_behaves_like 'LFS http 200 response'
end
- it 'responds with a 200 message on download' do
- get "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}", params: {}, headers: headers
+ context 'when downloading' do
+ subject { get(objects_url(project, sample_oid), params: {}, headers: headers) }
- expect(response).to have_gitlab_http_status(200)
+ it_behaves_like 'LFS http 200 response'
end
end
end
end
describe 'deprecated API' do
- let(:project) { create(:project) }
-
- before do
- enable_lfs
- end
+ let(:authorization) { authorize_user }
- shared_examples 'a deprecated' do
- it 'responds with 501' do
- expect(response).to have_gitlab_http_status(501)
+ shared_examples 'deprecated request' do
+ before do
+ subject
end
- it 'returns deprecated message' do
- expect(json_response).to include('message' => 'Server supports batch API only, please update your Git LFS client to version 1.0.1 and up.')
+ it_behaves_like 'LFS http expected response code and message' do
+ let(:response_code) { 501 }
+ let(:message) { 'Server supports batch API only, please update your Git LFS client to version 1.0.1 and up.' }
end
end
- context 'when fetching lfs object using deprecated API' do
- let(:authorization) { authorize_user }
+ context 'when fetching LFS object using deprecated API' do
+ subject { get(deprecated_objects_url(project, sample_oid), params: {}, headers: headers) }
- before do
- get "#{project.http_url_to_repo}/info/lfs/objects/#{sample_oid}", params: {}, headers: headers
- end
-
- it_behaves_like 'a deprecated'
+ it_behaves_like 'deprecated request'
end
- context 'when handling lfs request using deprecated API' do
- let(:authorization) { authorize_user }
- before do
- post_lfs_json "#{project.http_url_to_repo}/info/lfs/objects", nil, headers
- end
+ context 'when handling LFS request using deprecated API' do
+ subject { post_lfs_json(deprecated_objects_url(project), nil, headers) }
+
+ it_behaves_like 'deprecated request'
+ end
- it_behaves_like 'a deprecated'
+ def deprecated_objects_url(project, oid = nil)
+ File.join(["#{project.http_url_to_repo}/info/lfs/objects/", oid].compact)
end
end
- describe 'when fetching lfs object' do
- let(:project) { create(:project) }
+ describe 'when fetching LFS object' do
let(:update_permissions) { }
let(:before_get) { }
before do
- enable_lfs
update_permissions
before_get
- get "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}", params: {}, headers: headers
+ get objects_url(project, sample_oid), params: {}, headers: headers
end
context 'and request comes from gitlab-workhorse' do
context 'without user being authorized' do
- it 'responds with status 401' do
- expect(response).to have_gitlab_http_status(401)
- end
+ it_behaves_like 'LFS http 401 response'
end
context 'with required headers' do
shared_examples 'responds with a file' do
let(:sendfile) { 'X-Sendfile' }
- it 'responds with status 200' do
- expect(response).to have_gitlab_http_status(200)
- end
+ it_behaves_like 'LFS http 200 response'
it 'responds with the file location' do
expect(response.headers['Content-Type']).to eq('application/octet-stream')
@@ -229,9 +195,7 @@ describe 'Git LFS API and storage' do
project.lfs_objects << lfs_object
end
- it 'responds with status 404' do
- expect(response).to have_gitlab_http_status(404)
- end
+ it_behaves_like 'LFS http 404 response'
end
context 'and does have project access' do
@@ -249,9 +213,7 @@ describe 'Git LFS API and storage' do
lfs_object.file.migrate!(LfsObjectUploader::Store::REMOTE)
end
- it 'responds with redirect' do
- expect(response).to have_gitlab_http_status(200)
- end
+ it_behaves_like 'LFS http 200 response'
it 'responds with the workhorse send-url' do
expect(response.headers[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with("send-url:")
@@ -288,7 +250,7 @@ describe 'Git LFS API and storage' do
it_behaves_like 'responds with a file'
end
- describe 'when using a user key' do
+ describe 'when using a user key (LFSToken)' do
let(:authorization) { authorize_user_key }
context 'when user allowed' do
@@ -298,6 +260,18 @@ describe 'Git LFS API and storage' do
end
it_behaves_like 'responds with a file'
+
+ context 'when user password is expired' do
+ let(:user) { create(:user, password_expires_at: 1.minute.ago)}
+
+ it_behaves_like 'LFS http 401 response'
+ end
+
+ context 'when user is blocked' do
+ let(:user) { create(:user, :blocked)}
+
+ it_behaves_like 'LFS http 401 response'
+ end
end
context 'when user not allowed' do
@@ -305,9 +279,7 @@ describe 'Git LFS API and storage' do
project.lfs_objects << lfs_object
end
- it 'responds with status 404' do
- expect(response).to have_gitlab_http_status(404)
- end
+ it_behaves_like 'LFS http 404 response'
end
end
@@ -337,7 +309,6 @@ describe 'Git LFS API and storage' do
end
context 'for other project' do
- let(:other_project) { create(:project) }
let(:pipeline) { create(:ci_empty_pipeline, project: other_project) }
let(:update_permissions) do
@@ -361,7 +332,6 @@ describe 'Git LFS API and storage' do
end
context 'regular user' do
- let(:user) { create(:user) }
let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) }
it_behaves_like 'can download LFS only from own projects' do
@@ -384,166 +354,147 @@ describe 'Git LFS API and storage' do
context 'without required headers' do
let(:authorization) { authorize_user }
- it 'responds with status 404' do
- expect(response).to have_gitlab_http_status(404)
- end
+ it_behaves_like 'LFS http 404 response'
end
end
end
- describe 'when handling lfs batch request' do
+ describe 'when handling LFS batch request' do
let(:update_lfs_permissions) { }
let(:update_user_permissions) { }
before do
- enable_lfs
update_lfs_permissions
update_user_permissions
- post_lfs_json "#{project.http_url_to_repo}/info/lfs/objects/batch", body, headers
+ post_lfs_json batch_url(project), body, headers
end
- describe 'download' do
- let(:project) { create(:project) }
- let(:body) do
- {
- 'operation' => 'download',
- 'objects' => [
- { 'oid' => sample_oid,
- 'size' => sample_size }
- ]
- }
+ shared_examples 'process authorization header' do |renew_authorization:|
+ let(:response_authorization) do
+ authorization_in_action(lfs_actions.first)
end
- shared_examples 'an authorized requests' do
- context 'when downloading an lfs object that is assigned to our project' do
- let(:update_lfs_permissions) do
- project.lfs_objects << lfs_object
+ if renew_authorization
+ context 'when the authorization comes from a user' do
+ it 'returns a new valid LFS token authorization' do
+ expect(response_authorization).not_to eq(authorization)
end
- it 'responds with status 200' do
- expect(response).to have_gitlab_http_status(200)
+ it 'returns a a valid token' do
+ username, token = ::Base64.decode64(response_authorization.split(' ', 2).last).split(':', 2)
+
+ expect(username).to eq(user.username)
+ expect(Gitlab::LfsToken.new(user).token_valid?(token)).to be_truthy
end
- it 'with href to download' do
- expect(json_response).to eq({
- 'objects' => [
- {
- 'oid' => sample_oid,
- 'size' => sample_size,
- 'actions' => {
- 'download' => {
- 'href' => "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}",
- 'header' => { 'Authorization' => authorization }
- }
- }
- }
- ]
- })
+ it 'generates only one new token per each request' do
+ authorizations = lfs_actions.map do |action|
+ authorization_in_action(action)
+ end.compact
+
+ expect(authorizations.uniq.count).to eq 1
end
end
+ else
+ context 'when the authorization comes from a token' do
+ it 'returns the same authorization header' do
+ expect(response_authorization).to eq(authorization)
+ end
+ end
+ end
+
+ def lfs_actions
+ json_response['objects'].map { |a| a['actions'] }.compact
+ end
+
+ def authorization_in_action(action)
+ (action['upload'] || action['download']).dig('header', 'Authorization')
+ end
+ end
- context 'when downloading an lfs object that is assigned to other project' do
- let(:other_project) { create(:project) }
+ describe 'download' do
+ let(:body) { download_body(sample_object) }
+
+ shared_examples 'an authorized request' do |renew_authorization:|
+ context 'when downloading an LFS object that is assigned to our project' do
let(:update_lfs_permissions) do
- other_project.lfs_objects << lfs_object
+ project.lfs_objects << lfs_object
end
- it 'responds with status 200' do
- expect(response).to have_gitlab_http_status(200)
- end
+ it_behaves_like 'LFS http 200 response'
it 'with href to download' do
- expect(json_response).to eq({
- 'objects' => [
- {
- 'oid' => sample_oid,
- 'size' => sample_size,
- 'error' => {
- 'code' => 404,
- 'message' => "Object does not exist on the server or you don't have permissions to access it"
- }
- }
- ]
- })
+ expect(json_response['objects'].first).to include(sample_object)
+ expect(json_response['objects'].first['actions']['download']['href']).to eq(objects_url(project, sample_oid))
end
+
+ it_behaves_like 'process authorization header', renew_authorization: renew_authorization
end
- context 'when downloading a lfs object that does not exist' do
- let(:body) do
- {
- 'operation' => 'download',
- 'objects' => [
- { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897',
- 'size' => 1575078 }
- ]
- }
+ context 'when downloading an LFS object that is assigned to other project' do
+ let(:update_lfs_permissions) do
+ other_project.lfs_objects << lfs_object
end
- it 'responds with status 200' do
- expect(response).to have_gitlab_http_status(200)
- end
+ it_behaves_like 'LFS http 200 response'
it 'with an 404 for specific object' do
- expect(json_response).to eq({
- 'objects' => [
- {
- 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897',
- 'size' => 1575078,
- 'error' => {
- 'code' => 404,
- 'message' => "Object does not exist on the server or you don't have permissions to access it"
- }
- }
- ]
- })
+ expect(json_response['objects'].first).to include(sample_object)
+ expect(json_response['objects'].first['error']).to include('code' => 404, 'message' => "Object does not exist on the server or you don't have permissions to access it")
end
end
- context 'when downloading one new and one existing lfs object' do
- let(:body) do
- {
- 'operation' => 'download',
- 'objects' => [
- { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897',
- 'size' => 1575078 },
- { 'oid' => sample_oid,
- 'size' => sample_size }
- ]
- }
+ context 'when downloading a LFS object that does not exist' do
+ let(:body) { download_body(non_existing_object) }
+
+ it_behaves_like 'LFS http 200 response'
+
+ it 'with an 404 for specific object' do
+ expect(json_response['objects'].first).to include(non_existing_object)
+ expect(json_response['objects'].first['error']).to include('code' => 404, 'message' => "Object does not exist on the server or you don't have permissions to access it")
end
+ end
+ context 'when downloading one new and one existing LFS object' do
+ let(:body) { download_body(multiple_objects) }
let(:update_lfs_permissions) do
project.lfs_objects << lfs_object
end
- it 'responds with status 200' do
- expect(response).to have_gitlab_http_status(200)
- end
+ it_behaves_like 'LFS http 200 response'
- it 'responds with upload hypermedia link for the new object' do
- expect(json_response).to eq({
- 'objects' => [
- {
- 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897',
- 'size' => 1575078,
- 'error' => {
- 'code' => 404,
- 'message' => "Object does not exist on the server or you don't have permissions to access it"
- }
- },
- {
- 'oid' => sample_oid,
- 'size' => sample_size,
- 'actions' => {
- 'download' => {
- 'href' => "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}",
- 'header' => { 'Authorization' => authorization }
- }
- }
- }
- ]
+ it 'responds with download hypermedia link for the new object' do
+ expect(json_response['objects'].first).to include(sample_object)
+ expect(json_response['objects'].first['actions']['download']).to include('href' => objects_url(project, sample_oid))
+ expect(json_response['objects'].last).to eq({
+ 'oid' => non_existing_object_oid,
+ 'size' => non_existing_object_size,
+ 'error' => {
+ 'code' => 404,
+ 'message' => "Object does not exist on the server or you don't have permissions to access it"
+ }
})
end
+
+ it_behaves_like 'process authorization header', renew_authorization: renew_authorization
+ end
+
+ context 'when downloading two existing LFS objects' do
+ let(:body) { download_body(multiple_objects) }
+ let(:other_object) { create(:lfs_object, :with_file, oid: non_existing_object_oid, size: non_existing_object_size) }
+ let(:update_lfs_permissions) do
+ project.lfs_objects << [lfs_object, other_object]
+ end
+
+ it 'responds with the download hypermedia link for each object' do
+ expect(json_response['objects'].first).to include(sample_object)
+ expect(json_response['objects'].first['actions']['download']).to include('href' => objects_url(project, sample_oid))
+
+ expect(json_response['objects'].last).to include(non_existing_object)
+ expect(json_response['objects'].last['actions']['download']).to include('href' => objects_url(project, non_existing_object_oid))
+ end
+
+ it_behaves_like 'process authorization header', renew_authorization: renew_authorization
end
end
@@ -554,29 +505,41 @@ describe 'Git LFS API and storage' do
project.add_role(user, role)
end
- it_behaves_like 'an authorized requests' do
+ it_behaves_like 'an authorized request', renew_authorization: true do
let(:role) { :reporter }
end
context 'when user does is not member of the project' do
let(:update_user_permissions) { nil }
- it 'responds with 404' do
- expect(response).to have_gitlab_http_status(404)
- end
+ it_behaves_like 'LFS http 404 response'
end
context 'when user does not have download access' do
let(:role) { :guest }
- it 'responds with 403' do
- expect(response).to have_gitlab_http_status(403)
+ it_behaves_like 'LFS http 403 response'
+ end
+
+ context 'when user password is expired' do
+ let(:role) { :reporter}
+ let(:user) { create(:user, password_expires_at: 1.minute.ago)}
+
+ it 'with an 404 for specific object' do
+ expect(json_response['objects'].first).to include(sample_object)
+ expect(json_response['objects'].first['error']).to include('code' => 404, 'message' => "Object does not exist on the server or you don't have permissions to access it")
end
end
+
+ context 'when user is blocked' do
+ let(:role) { :reporter}
+ let(:user) { create(:user, :blocked)}
+
+ it_behaves_like 'LFS http 401 response'
+ end
end
context 'when using Deploy Tokens' do
- let(:project) { create(:project, :repository) }
let(:authorization) { authorize_deploy_token }
let(:update_user_permissions) { nil }
let(:role) { nil }
@@ -587,25 +550,19 @@ describe 'Git LFS API and storage' do
context 'when Deploy Token is valid' do
let(:deploy_token) { create(:deploy_token, projects: [project]) }
- it_behaves_like 'an authorized requests'
+ it_behaves_like 'an authorized request', renew_authorization: false
end
context 'when Deploy Token is not valid' do
let(:deploy_token) { create(:deploy_token, projects: [project], read_repository: false) }
- it 'responds with access denied' do
- expect(response).to have_gitlab_http_status(401)
- end
+ it_behaves_like 'LFS http 401 response'
end
context 'when Deploy Token is not related to the project' do
- let(:another_project) { create(:project, :repository) }
- let(:deploy_token) { create(:deploy_token, projects: [another_project]) }
+ let(:deploy_token) { create(:deploy_token, projects: [other_project]) }
- it 'responds with access forbidden' do
- # We render 404, to prevent data leakage about existence of the project
- expect(response).to have_gitlab_http_status(404)
- end
+ it_behaves_like 'LFS http 404 response'
end
end
@@ -616,7 +573,7 @@ describe 'Git LFS API and storage' do
project.lfs_objects << lfs_object
end
- shared_examples 'can download LFS only from own projects' do
+ shared_examples 'can download LFS only from own projects' do |renew_authorization:|
context 'for own project' do
let(:pipeline) { create(:ci_empty_pipeline, project: project) }
@@ -624,11 +581,10 @@ describe 'Git LFS API and storage' do
project.add_reporter(user)
end
- it_behaves_like 'an authorized requests'
+ it_behaves_like 'an authorized request', renew_authorization: renew_authorization
end
context 'for other project' do
- let(:other_project) { create(:project) }
let(:pipeline) { create(:ci_empty_pipeline, project: other_project) }
it 'rejects downloading code' do
@@ -641,17 +597,16 @@ describe 'Git LFS API and storage' do
let(:user) { create(:admin) }
let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) }
- it_behaves_like 'can download LFS only from own projects' do
+ it_behaves_like 'can download LFS only from own projects', renew_authorization: true do
# We render 403, because administrator does have normally access
let(:other_project_status) { 403 }
end
end
context 'regular user' do
- let(:user) { create(:user) }
let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) }
- it_behaves_like 'can download LFS only from own projects' do
+ it_behaves_like 'can download LFS only from own projects', renew_authorization: true do
# We render 404, to prevent data leakage about existence of the project
let(:other_project_status) { 404 }
end
@@ -660,7 +615,7 @@ describe 'Git LFS API and storage' do
context 'does not have user' do
let(:build) { create(:ci_build, :running, pipeline: pipeline) }
- it_behaves_like 'can download LFS only from own projects' do
+ it_behaves_like 'can download LFS only from own projects', renew_authorization: false do
# We render 404, to prevent data leakage about existence of the project
let(:other_project_status) { 404 }
end
@@ -675,11 +630,9 @@ describe 'Git LFS API and storage' do
project.lfs_objects << lfs_object
end
- it 'responds with status 200 and href to download' do
- expect(response).to have_gitlab_http_status(200)
- end
+ it_behaves_like 'LFS http 200 response'
- it 'responds with status 200 and href to download' do
+ it 'returns href to download' do
expect(json_response).to eq({
'objects' => [
{
@@ -688,7 +641,7 @@ describe 'Git LFS API and storage' do
'authenticated' => true,
'actions' => {
'download' => {
- 'href' => "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}",
+ 'href' => objects_url(project, sample_oid),
'header' => {}
}
}
@@ -703,37 +656,29 @@ describe 'Git LFS API and storage' do
project.lfs_objects << lfs_object
end
- it 'responds with authorization required' do
- expect(response).to have_gitlab_http_status(401)
- end
+ it_behaves_like 'LFS http 401 response'
end
end
end
describe 'upload' do
let(:project) { create(:project, :public) }
- let(:body) do
- {
- 'operation' => 'upload',
- 'objects' => [
- { 'oid' => sample_oid,
- 'size' => sample_size }
- ]
- }
- end
+ let(:body) { upload_body(sample_object) }
- shared_examples 'pushes new LFS objects' do
+ shared_examples 'pushes new LFS objects' do |renew_authorization:|
let(:sample_size) { 150.megabytes }
- let(:sample_oid) { '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897' }
+ let(:sample_oid) { non_existing_object_oid }
+
+ it_behaves_like 'LFS http 200 response'
it 'responds with upload hypermedia link' do
- expect(response).to have_gitlab_http_status(200)
expect(json_response['objects']).to be_kind_of(Array)
- expect(json_response['objects'].first['oid']).to eq(sample_oid)
- expect(json_response['objects'].first['size']).to eq(sample_size)
- expect(json_response['objects'].first['actions']['upload']['href']).to eq("#{Gitlab.config.gitlab.url}/#{project.full_path}.git/gitlab-lfs/objects/#{sample_oid}/#{sample_size}")
- expect(json_response['objects'].first['actions']['upload']['header']).to eq({ 'Authorization' => authorization, 'Content-Type' => 'application/octet-stream' })
+ expect(json_response['objects'].first).to include(sample_object)
+ expect(json_response['objects'].first['actions']['upload']['href']).to eq(objects_url(project, sample_oid, sample_size))
+ expect(json_response['objects'].first['actions']['upload']['header']).to include('Content-Type' => 'application/octet-stream')
end
+
+ it_behaves_like 'process authorization header', renew_authorization: renew_authorization
end
describe 'when request is authenticated' do
@@ -744,107 +689,80 @@ describe 'Git LFS API and storage' do
project.add_developer(user)
end
- context 'when pushing an lfs object that already exists' do
- let(:other_project) { create(:project) }
+ context 'when pushing an LFS object that already exists' do
let(:update_lfs_permissions) do
other_project.lfs_objects << lfs_object
end
- it 'responds with status 200' do
- expect(response).to have_gitlab_http_status(200)
- end
+ it_behaves_like 'LFS http 200 response'
it 'responds with links the object to the project' do
expect(json_response['objects']).to be_kind_of(Array)
- expect(json_response['objects'].first['oid']).to eq(sample_oid)
- expect(json_response['objects'].first['size']).to eq(sample_size)
+ expect(json_response['objects'].first).to include(sample_object)
expect(lfs_object.projects.pluck(:id)).not_to include(project.id)
expect(lfs_object.projects.pluck(:id)).to include(other_project.id)
- expect(json_response['objects'].first['actions']['upload']['href']).to eq("#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}/#{sample_size}")
- expect(json_response['objects'].first['actions']['upload']['header']).to eq({ 'Authorization' => authorization, 'Content-Type' => 'application/octet-stream' })
+ expect(json_response['objects'].first['actions']['upload']['href']).to eq(objects_url(project, sample_oid, sample_size))
+ expect(json_response['objects'].first['actions']['upload']['header']).to include('Content-Type' => 'application/octet-stream')
end
- end
- context 'when pushing a lfs object that does not exist' do
- it_behaves_like 'pushes new LFS objects'
+ it_behaves_like 'process authorization header', renew_authorization: true
end
- context 'when pushing one new and one existing lfs object' do
- let(:body) do
- {
- 'operation' => 'upload',
- 'objects' => [
- { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897',
- 'size' => 1575078 },
- { 'oid' => sample_oid,
- 'size' => sample_size }
- ]
- }
- end
+ context 'when pushing a LFS object that does not exist' do
+ it_behaves_like 'pushes new LFS objects', renew_authorization: true
+ end
+ context 'when pushing one new and one existing LFS object' do
+ let(:body) { upload_body(multiple_objects) }
let(:update_lfs_permissions) do
project.lfs_objects << lfs_object
end
- it 'responds with status 200' do
- expect(response).to have_gitlab_http_status(200)
- end
+ it_behaves_like 'LFS http 200 response'
it 'responds with upload hypermedia link for the new object' do
expect(json_response['objects']).to be_kind_of(Array)
- expect(json_response['objects'].first['oid']).to eq("91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897")
- expect(json_response['objects'].first['size']).to eq(1575078)
- expect(json_response['objects'].first['actions']['upload']['href']).to eq("#{project.http_url_to_repo}/gitlab-lfs/objects/91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897/1575078")
- expect(json_response['objects'].first['actions']['upload']['header']).to eq({ 'Authorization' => authorization, 'Content-Type' => 'application/octet-stream' })
+ expect(json_response['objects'].first).to include(sample_object)
+ expect(json_response['objects'].first).not_to have_key('actions')
- expect(json_response['objects'].last['oid']).to eq(sample_oid)
- expect(json_response['objects'].last['size']).to eq(sample_size)
- expect(json_response['objects'].last).not_to have_key('actions')
+ expect(json_response['objects'].last).to include(non_existing_object)
+ expect(json_response['objects'].last['actions']['upload']['href']).to eq(objects_url(project, non_existing_object_oid, non_existing_object_size))
+ expect(json_response['objects'].last['actions']['upload']['header']).to include('Content-Type' => 'application/octet-stream')
end
+
+ it_behaves_like 'process authorization header', renew_authorization: true
end
end
context 'when user does not have push access' do
let(:authorization) { authorize_user }
- it 'responds with 403' do
- expect(response).to have_gitlab_http_status(403)
- end
+ it_behaves_like 'LFS http 403 response'
end
context 'when build is authorized' do
let(:authorization) { authorize_ci_project }
context 'build has an user' do
- let(:user) { create(:user) }
+ let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) }
context 'tries to push to own project' do
- let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) }
-
- it 'responds with 403 (not 404 because project is public)' do
- expect(response).to have_gitlab_http_status(403)
- end
+ it_behaves_like 'LFS http 403 response'
end
context 'tries to push to other project' do
- let(:other_project) { create(:project) }
let(:pipeline) { create(:ci_empty_pipeline, project: other_project) }
- let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) }
# I'm not sure what this tests that is different from the previous test
- it 'responds with 403 (not 404 because project is public)' do
- expect(response).to have_gitlab_http_status(403)
- end
+ it_behaves_like 'LFS http 403 response'
end
end
context 'does not have user' do
let(:build) { create(:ci_build, :running, pipeline: pipeline) }
- it 'responds with 403 (not 404 because project is public)' do
- expect(response).to have_gitlab_http_status(403)
- end
+ it_behaves_like 'LFS http 403 response'
end
end
@@ -856,7 +774,7 @@ describe 'Git LFS API and storage' do
project.deploy_keys_projects.create(deploy_key: key, can_push: true)
end
- it_behaves_like 'pushes new LFS objects'
+ it_behaves_like 'pushes new LFS objects', renew_authorization: false
end
end
@@ -866,80 +784,60 @@ describe 'Git LFS API and storage' do
project.add_maintainer(user)
end
- it 'responds with status 401' do
- expect(response).to have_gitlab_http_status(401)
- end
+ it_behaves_like 'LFS http 401 response'
end
context 'when user does not have push access' do
- it 'responds with status 401' do
- expect(response).to have_gitlab_http_status(401)
- end
+ it_behaves_like 'LFS http 401 response'
end
end
end
describe 'unsupported' do
- let(:project) { create(:project) }
let(:authorization) { authorize_user }
- let(:body) do
- {
- 'operation' => 'other',
- 'objects' => [
- { 'oid' => sample_oid,
- 'size' => sample_size }
- ]
- }
- end
+ let(:body) { request_body('other', sample_object) }
- it 'responds with status 404' do
- expect(response).to have_gitlab_http_status(404)
- end
+ it_behaves_like 'LFS http 404 response'
end
end
- describe 'when handling lfs batch request on a read-only GitLab instance' do
+ describe 'when handling LFS batch request on a read-only GitLab instance' do
let(:authorization) { authorize_user }
- let(:project) { create(:project) }
- let(:path) { "#{project.http_url_to_repo}/info/lfs/objects/batch" }
- let(:body) do
- { 'objects' => [{ 'oid' => sample_oid, 'size' => sample_size }] }
- end
+
+ subject { post_lfs_json(batch_url(project), body, headers) }
before do
allow(Gitlab::Database).to receive(:read_only?) { true }
+
project.add_maintainer(user)
- enable_lfs
+
+ subject
end
- it 'responds with a 200 message on download' do
- post_lfs_json path, body.merge('operation' => 'download'), headers
+ context 'when downloading' do
+ let(:body) { download_body(sample_object) }
- expect(response).to have_gitlab_http_status(200)
+ it_behaves_like 'LFS http 200 response'
end
- it 'responds with a 403 message on upload' do
- post_lfs_json path, body.merge('operation' => 'upload'), headers
+ context 'when uploading' do
+ let(:body) { upload_body(sample_object) }
- expect(response).to have_gitlab_http_status(403)
- expect(json_response).to include('message' => 'You cannot write to this read-only GitLab instance.')
+ it_behaves_like 'LFS http expected response code and message' do
+ let(:response_code) { 403 }
+ let(:message) { 'You cannot write to this read-only GitLab instance.' }
+ end
end
end
- describe 'when pushing a lfs object' do
- before do
- enable_lfs
- end
-
+ describe 'when pushing a LFS object' do
shared_examples 'unauthorized' do
context 'and request is sent by gitlab-workhorse to authorize the request' do
before do
put_authorize
end
- it 'responds with status 401' do
- expect(response).to have_gitlab_http_status(401)
- end
+ it_behaves_like 'LFS http 401 response'
end
context 'and request is sent by gitlab-workhorse to finalize the upload' do
@@ -947,9 +845,7 @@ describe 'Git LFS API and storage' do
put_finalize
end
- it 'responds with status 401' do
- expect(response).to have_gitlab_http_status(401)
- end
+ it_behaves_like 'LFS http 401 response'
end
context 'and request is sent with a malformed headers' do
@@ -957,9 +853,7 @@ describe 'Git LFS API and storage' do
put_finalize('/etc/passwd')
end
- it 'does not recognize it as a valid lfs command' do
- expect(response).to have_gitlab_http_status(401)
- end
+ it_behaves_like 'LFS http 401 response'
end
end
@@ -969,9 +863,7 @@ describe 'Git LFS API and storage' do
put_authorize
end
- it 'responds with 403' do
- expect(response).to have_gitlab_http_status(403)
- end
+ it_behaves_like 'LFS http 403 response'
end
context 'and request is sent by gitlab-workhorse to finalize the upload' do
@@ -979,9 +871,7 @@ describe 'Git LFS API and storage' do
put_finalize
end
- it 'responds with 403' do
- expect(response).to have_gitlab_http_status(403)
- end
+ it_behaves_like 'LFS http 403 response'
end
context 'and request is sent with a malformed headers' do
@@ -989,15 +879,11 @@ describe 'Git LFS API and storage' do
put_finalize('/etc/passwd')
end
- it 'does not recognize it as a valid lfs command' do
- expect(response).to have_gitlab_http_status(403)
- end
+ it_behaves_like 'LFS http 403 response'
end
end
describe 'to one project' do
- let(:project) { create(:project) }
-
describe 'when user is authenticated' do
let(:authorization) { authorize_user }
@@ -1018,9 +904,7 @@ describe 'Git LFS API and storage' do
put_authorize
end
- it 'responds with status 200' do
- expect(response).to have_gitlab_http_status(200)
- end
+ it_behaves_like 'LFS http 200 response'
it 'uses the gitlab-workhorse content type' do
expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
@@ -1029,7 +913,7 @@ describe 'Git LFS API and storage' do
shared_examples 'a local file' do
it_behaves_like 'a valid response' do
- it 'responds with status 200, location of lfs store and object details' do
+ it 'responds with status 200, location of LFS store and object details' do
expect(json_response['TempPath']).to eq(LfsObjectUploader.workhorse_local_upload_path)
expect(json_response['RemoteObject']).to be_nil
expect(json_response['LfsOid']).to eq(sample_oid)
@@ -1049,8 +933,8 @@ describe 'Git LFS API and storage' do
end
it_behaves_like 'a valid response' do
- it 'responds with status 200, location of lfs remote store and object details' do
- expect(json_response['TempPath']).to eq(LfsObjectUploader.workhorse_local_upload_path)
+ it 'responds with status 200, location of LFS remote store and object details' do
+ expect(json_response).not_to have_key('TempPath')
expect(json_response['RemoteObject']).to have_key('ID')
expect(json_response['RemoteObject']).to have_key('GetURL')
expect(json_response['RemoteObject']).to have_key('StoreURL')
@@ -1077,11 +961,9 @@ describe 'Git LFS API and storage' do
put_finalize
end
- it 'responds with status 200' do
- expect(response).to have_gitlab_http_status(200)
- end
+ it_behaves_like 'LFS http 200 response'
- it 'lfs object is linked to the project' do
+ it 'LFS object is linked to the project' do
expect(lfs_object.projects.pluck(:id)).to include(project.id)
end
end
@@ -1092,7 +974,7 @@ describe 'Git LFS API and storage' do
end
end
- context 'and workhorse requests upload finalize for a new lfs object' do
+ context 'and workhorse requests upload finalize for a new LFS object' do
before do
lfs_object.destroy
end
@@ -1111,10 +993,17 @@ describe 'Git LFS API and storage' do
stub_lfs_object_storage(direct_upload: true)
end
+ let(:tmp_object) do
+ fog_connection.directories.new(key: 'lfs-objects').files.create(
+ key: 'tmp/uploads/12312300',
+ body: 'content'
+ )
+ end
+
['123123', '../../123123'].each do |remote_id|
context "with invalid remote_id: #{remote_id}" do
subject do
- put_finalize(with_tempfile: true, args: {
+ put_finalize(remote_object: tmp_object, args: {
'file.remote_id' => remote_id
})
end
@@ -1128,15 +1017,8 @@ describe 'Git LFS API and storage' do
end
context 'with valid remote_id' do
- before do
- fog_connection.directories.new(key: 'lfs-objects').files.create(
- key: 'tmp/uploads/12312300',
- body: 'content'
- )
- end
-
subject do
- put_finalize(with_tempfile: true, args: {
+ put_finalize(remote_object: tmp_object, args: {
'file.remote_id' => '12312300',
'file.name' => 'name'
})
@@ -1146,6 +1028,10 @@ describe 'Git LFS API and storage' do
subject
expect(response).to have_gitlab_http_status(200)
+
+ object = LfsObject.find_by_oid(sample_oid)
+ expect(object).to be_present
+ expect(object.file.read).to eq(tmp_object.body)
end
it 'schedules migration of file to object storage' do
@@ -1202,33 +1088,25 @@ describe 'Git LFS API and storage' do
let(:authorization) { authorize_ci_project }
context 'build has an user' do
- let(:user) { create(:user) }
+ let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) }
context 'tries to push to own project' do
- let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) }
-
before do
project.add_developer(user)
put_authorize
end
- it 'responds with 403 (not 404 because the build user can read the project)' do
- expect(response).to have_gitlab_http_status(403)
- end
+ it_behaves_like 'LFS http 403 response'
end
context 'tries to push to other project' do
- let(:other_project) { create(:project) }
let(:pipeline) { create(:ci_empty_pipeline, project: other_project) }
- let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) }
before do
put_authorize
end
- it 'responds with 404 (do not leak non-public project existence)' do
- expect(response).to have_gitlab_http_status(404)
- end
+ it_behaves_like 'LFS http 404 response'
end
end
@@ -1239,10 +1117,41 @@ describe 'Git LFS API and storage' do
put_authorize
end
- it 'responds with 404 (do not leak non-public project existence)' do
- expect(response).to have_gitlab_http_status(404)
+ it_behaves_like 'LFS http 404 response'
+ end
+ end
+
+ describe 'when using a user key (LFSToken)' do
+ let(:authorization) { authorize_user_key }
+
+ context 'when user allowed' do
+ before do
+ project.add_developer(user)
+ put_authorize
+ end
+
+ it_behaves_like 'LFS http 200 response'
+
+ context 'when user password is expired' do
+ let(:user) { create(:user, password_expires_at: 1.minute.ago)}
+
+ it_behaves_like 'LFS http 401 response'
+ end
+
+ context 'when user is blocked' do
+ let(:user) { create(:user, :blocked)}
+
+ it_behaves_like 'LFS http 401 response'
end
end
+
+ context 'when user not allowed' do
+ before do
+ put_authorize
+ end
+
+ it_behaves_like 'LFS http 404 response'
+ end
end
context 'for unauthenticated' do
@@ -1268,11 +1177,9 @@ describe 'Git LFS API and storage' do
put_authorize
end
- it 'responds with status 200' do
- expect(response).to have_gitlab_http_status(200)
- end
+ it_behaves_like 'LFS http 200 response'
- it 'with location of lfs store and object details' do
+ it 'with location of LFS store and object details' do
expect(json_response['TempPath']).to eq(LfsObjectUploader.workhorse_local_upload_path)
expect(json_response['LfsOid']).to eq(sample_oid)
expect(json_response['LfsSize']).to eq(sample_size)
@@ -1284,11 +1191,9 @@ describe 'Git LFS API and storage' do
put_finalize
end
- it 'responds with status 200' do
- expect(response).to have_gitlab_http_status(200)
- end
+ it_behaves_like 'LFS http 200 response'
- it 'lfs object is linked to the source project' do
+ it 'LFS object is linked to the source project' do
expect(lfs_object.projects.pluck(:id)).to include(upstream_project.id)
end
end
@@ -1307,34 +1212,24 @@ describe 'Git LFS API and storage' do
end
context 'build has an user' do
- let(:user) { create(:user) }
+ let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) }
context 'tries to push to own project' do
- let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) }
-
- it 'responds with 403 (not 404 because project is public)' do
- expect(response).to have_gitlab_http_status(403)
- end
+ it_behaves_like 'LFS http 403 response'
end
context 'tries to push to other project' do
- let(:other_project) { create(:project) }
let(:pipeline) { create(:ci_empty_pipeline, project: other_project) }
- let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) }
# I'm not sure what this tests that is different from the previous test
- it 'responds with 403 (not 404 because project is public)' do
- expect(response).to have_gitlab_http_status(403)
- end
+ it_behaves_like 'LFS http 403 response'
end
end
context 'does not have user' do
let(:build) { create(:ci_build, :running, pipeline: pipeline) }
- it 'responds with 403 (not 404 because project is public)' do
- expect(response).to have_gitlab_http_status(403)
- end
+ it_behaves_like 'LFS http 403 response'
end
end
@@ -1351,22 +1246,20 @@ describe 'Git LFS API and storage' do
upstream_project.lfs_objects << lfs_object
end
- context 'when pushing the same lfs object to the second project' do
+ context 'when pushing the same LFS object to the second project' do
before do
finalize_headers = headers
.merge('X-Gitlab-Lfs-Tmp' => lfs_tmp_file)
.merge(workhorse_internal_api_request_header)
- put "#{second_project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}/#{sample_size}",
- params: {},
- headers: finalize_headers
+ put objects_url(second_project, sample_oid, sample_size),
+ params: {},
+ headers: finalize_headers
end
- it 'responds with status 200' do
- expect(response).to have_gitlab_http_status(200)
- end
+ it_behaves_like 'LFS http 200 response'
- it 'links the lfs object to the project' do
+ it 'links the LFS object to the project' do
expect(lfs_object.projects.pluck(:id)).to include(second_project.id, upstream_project.id)
end
end
@@ -1377,66 +1270,38 @@ describe 'Git LFS API and storage' do
authorize_headers = headers
authorize_headers.merge!(workhorse_internal_api_request_header) if verified
- put "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}/#{sample_size}/authorize", params: {}, headers: authorize_headers
+ put authorize_url(project, sample_oid, sample_size), params: {}, headers: authorize_headers
end
- def put_finalize(lfs_tmp = lfs_tmp_file, with_tempfile: false, verified: true, args: {})
- upload_path = LfsObjectUploader.workhorse_local_upload_path
- file_path = upload_path + '/' + lfs_tmp if lfs_tmp
+ def put_finalize(lfs_tmp = lfs_tmp_file, with_tempfile: false, verified: true, remote_object: nil, args: {})
+ uploaded_file = nil
if with_tempfile
+ upload_path = LfsObjectUploader.workhorse_local_upload_path
+ file_path = upload_path + '/' + lfs_tmp if lfs_tmp
+
FileUtils.mkdir_p(upload_path)
FileUtils.touch(file_path)
- end
-
- extra_args = {
- 'file.path' => file_path,
- 'file.name' => File.basename(file_path)
- }
- put_finalize_with_args(args.merge(extra_args).compact, verified: verified)
- end
+ uploaded_file = UploadedFile.new(file_path, filename: File.basename(file_path))
+ elsif remote_object
+ uploaded_file = fog_to_uploaded_file(remote_object)
+ end
- def put_finalize_with_args(args, verified:)
finalize_headers = headers
finalize_headers.merge!(workhorse_internal_api_request_header) if verified
- put "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}/#{sample_size}", params: args, headers: finalize_headers
+ workhorse_finalize(
+ objects_url(project, sample_oid, sample_size),
+ method: :put,
+ file_key: :file,
+ params: args.merge(file: uploaded_file),
+ headers: finalize_headers
+ )
end
def lfs_tmp_file
"#{sample_oid}012345678"
end
end
-
- def enable_lfs
- allow(Gitlab.config.lfs).to receive(:enabled).and_return(true)
- end
-
- def authorize_ci_project
- ActionController::HttpAuthentication::Basic.encode_credentials('gitlab-ci-token', build.token)
- end
-
- def authorize_user
- ActionController::HttpAuthentication::Basic.encode_credentials(user.username, user.password)
- end
-
- def authorize_deploy_key
- ActionController::HttpAuthentication::Basic.encode_credentials("lfs+deploy-key-#{key.id}", Gitlab::LfsToken.new(key).token)
- end
-
- def authorize_user_key
- ActionController::HttpAuthentication::Basic.encode_credentials(user.username, Gitlab::LfsToken.new(user).token)
- end
-
- def authorize_deploy_token
- ActionController::HttpAuthentication::Basic.encode_credentials(deploy_token.username, deploy_token.token)
- end
-
- def post_lfs_json(url, body = nil, headers = nil)
- params = body.try(:to_json)
- headers = (headers || {}).merge('Content-Type' => LfsRequest::CONTENT_TYPE)
-
- post(url, params: params, headers: headers)
- end
end
diff --git a/spec/requests/openid_connect_spec.rb b/spec/requests/openid_connect_spec.rb
index da2e7b71dbe..dfa17c5ff27 100644
--- a/spec/requests/openid_connect_spec.rb
+++ b/spec/requests/openid_connect_spec.rb
@@ -148,34 +148,25 @@ describe 'OpenID Connect requests' do
end
end
- # These 2 calls shouldn't actually throw, they should be handled as an
- # unauthorized request, so we should be able to check the response.
- #
- # This was not possible due to an issue with Warden:
- # https://github.com/hassox/warden/pull/162
- #
- # When the patch gets merged and we update Warden, these specs will need to
- # updated to check the response instead of a raised exception.
- # https://gitlab.com/gitlab-org/gitlab-foss/issues/40218
context 'when user is blocked' do
- it 'returns authentication error' do
+ it 'redirects to login page' do
access_grant
user.block!
- expect do
- request_access_token!
- end.to raise_error UncaughtThrowError
+ request_access_token!
+
+ expect(response).to redirect_to('/users/sign_in')
end
end
context 'when user is ldap_blocked' do
- it 'returns authentication error' do
+ it 'redirects to login page' do
access_grant
user.ldap_block!
- expect do
- request_access_token!
- end.to raise_error UncaughtThrowError
+ request_access_token!
+
+ expect(response).to redirect_to('/users/sign_in')
end
end
end
diff --git a/spec/requests/rack_attack_global_spec.rb b/spec/requests/rack_attack_global_spec.rb
index cf459ba99c1..ca8720cd414 100644
--- a/spec/requests/rack_attack_global_spec.rb
+++ b/spec/requests/rack_attack_global_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe 'Rack Attack global throttles' do
+ include RackAttackSpecHelpers
+
let(:settings) { Gitlab::CurrentSettings.current_application_settings }
# Start with really high limits and override them with low limits to ensure
@@ -12,7 +14,9 @@ describe 'Rack Attack global throttles' do
throttle_authenticated_api_requests_per_period: 100,
throttle_authenticated_api_period_in_seconds: 1,
throttle_authenticated_web_requests_per_period: 100,
- throttle_authenticated_web_period_in_seconds: 1
+ throttle_authenticated_web_period_in_seconds: 1,
+ throttle_authenticated_protected_paths_request_per_period: 100,
+ throttle_authenticated_protected_paths_in_seconds: 1
}
end
@@ -20,21 +24,17 @@ describe 'Rack Attack global throttles' do
let(:period_in_seconds) { 10000 }
let(:period) { period_in_seconds.seconds }
- around do |example|
- # Instead of test environment's :null_store so the throttles can increment
- Rack::Attack.cache.store = ActiveSupport::Cache::MemoryStore.new
-
- # Make time-dependent tests deterministic
- Timecop.freeze { example.run }
-
- Rack::Attack.cache.store = Rails.cache
- end
+ include_context 'rack attack cache store'
describe 'unauthenticated requests' do
let(:url_that_does_not_require_authentication) { '/users/sign_in' }
let(:url_api_internal) { '/api/v4/internal/check' }
before do
+ # Disabling protected paths throttle, otherwise requests to
+ # '/users/sign_in' are caught by this throttle.
+ settings_to_set[:throttle_protected_paths_enabled] = false
+
# Set low limits
settings_to_set[:throttle_unauthenticated_requests_per_period] = requests_per_period
settings_to_set[:throttle_unauthenticated_period_in_seconds] = period_in_seconds
@@ -203,29 +203,156 @@ describe 'Rack Attack global throttles' do
it_behaves_like 'rate-limited web authenticated requests'
end
- def api_get_args_with_token_headers(partial_url, token_headers)
- ["/api/#{API::API.version}#{partial_url}", params: nil, headers: token_headers]
- end
+ describe 'protected paths' do
+ context 'unauthenticated requests' do
+ let(:protected_path_that_does_not_require_authentication) do
+ '/users/confirmation'
+ end
- def rss_url(user)
- "/dashboard/projects.atom?feed_token=#{user.feed_token}"
- end
+ before do
+ settings_to_set[:throttle_protected_paths_requests_per_period] = requests_per_period # 1
+ settings_to_set[:throttle_protected_paths_period_in_seconds] = period_in_seconds # 10_000
+ end
- def private_token_headers(user)
- { 'HTTP_PRIVATE_TOKEN' => user.private_token }
- end
+ context 'when protected paths throttle is disabled' do
+ before do
+ settings_to_set[:throttle_protected_paths_enabled] = false
+ stub_application_setting(settings_to_set)
+ end
- def personal_access_token_headers(personal_access_token)
- { 'HTTP_PRIVATE_TOKEN' => personal_access_token.token }
- end
+ it 'allows requests over the rate limit' do
+ (1 + requests_per_period).times do
+ get protected_path_that_does_not_require_authentication
+ expect(response).to have_http_status 200
+ end
+ end
+ end
- def oauth_token_headers(oauth_access_token)
- { 'AUTHORIZATION' => "Bearer #{oauth_access_token.token}" }
- end
+ context 'when protected paths throttle is enabled' do
+ before do
+ settings_to_set[:throttle_protected_paths_enabled] = true
+ stub_application_setting(settings_to_set)
+ end
+
+ it 'rejects requests over the rate limit' do
+ requests_per_period.times do
+ get protected_path_that_does_not_require_authentication
+ expect(response).to have_http_status 200
+ end
+
+ expect_rejection { get protected_path_that_does_not_require_authentication }
+ end
+
+ context 'when Omnibus throttle is present' do
+ before do
+ allow(Gitlab::Throttle)
+ .to receive(:omnibus_protected_paths_present?).and_return(true)
+ end
+
+ it 'allows requests over the rate limit' do
+ (1 + requests_per_period).times do
+ get protected_path_that_does_not_require_authentication
+ expect(response).to have_http_status 200
+ end
+ end
+ end
+ end
+ end
+
+ context 'API requests authenticated with personal access token', :api do
+ let(:user) { create(:user) }
+ let(:token) { create(:personal_access_token, user: user) }
+ let(:other_user) { create(:user) }
+ let(:other_user_token) { create(:personal_access_token, user: other_user) }
+ let(:throttle_setting_prefix) { 'throttle_protected_paths' }
+ let(:api_partial_url) { '/users' }
+
+ let(:protected_paths) do
+ [
+ '/api/v4/users'
+ ]
+ end
+
+ before do
+ settings_to_set[:protected_paths] = protected_paths
+ stub_application_setting(settings_to_set)
+ end
+
+ context 'with the token in the query string' do
+ let(:get_args) { [api(api_partial_url, personal_access_token: token)] }
+ let(:other_user_get_args) { [api(api_partial_url, personal_access_token: other_user_token)] }
+
+ it_behaves_like 'rate-limited token-authenticated requests'
+ end
+
+ context 'with the token in the headers' do
+ let(:get_args) { api_get_args_with_token_headers(api_partial_url, personal_access_token_headers(token)) }
+ let(:other_user_get_args) { api_get_args_with_token_headers(api_partial_url, personal_access_token_headers(other_user_token)) }
+
+ it_behaves_like 'rate-limited token-authenticated requests'
+ end
+
+ context 'when Omnibus throttle is present' do
+ let(:get_args) { [api(api_partial_url, personal_access_token: token)] }
+ let(:other_user_get_args) { [api(api_partial_url, personal_access_token: other_user_token)] }
+
+ before do
+ settings_to_set[:"#{throttle_setting_prefix}_requests_per_period"] = requests_per_period
+ settings_to_set[:"#{throttle_setting_prefix}_period_in_seconds"] = period_in_seconds
+ settings_to_set[:"#{throttle_setting_prefix}_enabled"] = true
+ stub_application_setting(settings_to_set)
+
+ allow(Gitlab::Throttle)
+ .to receive(:omnibus_protected_paths_present?).and_return(true)
+ end
+
+ it 'allows requests over the rate limit' do
+ (1 + requests_per_period).times do
+ get(*get_args)
+ expect(response).to have_http_status 200
+ end
+ end
+ end
+ end
+
+ describe 'web requests authenticated with regular login' do
+ let(:throttle_setting_prefix) { 'throttle_protected_paths' }
+ let(:user) { create(:user) }
+ let(:url_that_requires_authentication) { '/dashboard/snippets' }
+
+ let(:protected_paths) do
+ [
+ url_that_requires_authentication
+ ]
+ end
+
+ before do
+ settings_to_set[:protected_paths] = protected_paths
+ stub_application_setting(settings_to_set)
+ end
+
+ it_behaves_like 'rate-limited web authenticated requests'
+
+ context 'when Omnibus throttle is present' do
+ before do
+ settings_to_set[:"#{throttle_setting_prefix}_requests_per_period"] = requests_per_period
+ settings_to_set[:"#{throttle_setting_prefix}_period_in_seconds"] = period_in_seconds
+ settings_to_set[:"#{throttle_setting_prefix}_enabled"] = true
+ stub_application_setting(settings_to_set)
- def expect_rejection(&block)
- yield
+ allow(Gitlab::Throttle)
+ .to receive(:omnibus_protected_paths_present?).and_return(true)
- expect(response).to have_http_status(429)
+ login_as(user)
+ end
+
+ it 'allows requests over the rate limit' do
+ (1 + requests_per_period).times do
+ get url_that_requires_authentication
+ expect(response).to have_http_status 200
+ end
+ end
+ end
+ end
end
end
diff --git a/spec/routing/project_routing_spec.rb b/spec/routing/project_routing_spec.rb
index 7e2d70d6eb5..acdbf064a73 100644
--- a/spec/routing/project_routing_spec.rb
+++ b/spec/routing/project_routing_spec.rb
@@ -276,6 +276,11 @@ describe 'project routing' do
expect(get('/gitlab/gitlabhq/refs/feature%2B45/logs_tree/foo/bar/baz')).to route_to('projects/refs#logs_tree', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'feature+45', path: 'foo/bar/baz')
expect(get('/gitlab/gitlabhq/refs/feature@45/logs_tree/foo/bar/baz')).to route_to('projects/refs#logs_tree', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'feature@45', path: 'foo/bar/baz')
expect(get('/gitlab/gitlabhq/refs/stable/logs_tree/files.scss')).to route_to('projects/refs#logs_tree', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'stable', path: 'files.scss')
+ assert_routing({ path: "/gitlab/gitlabhq/refs/stable/logs_tree/new%0A%0Aline.txt",
+ method: :get },
+ { controller: 'projects/refs', action: 'logs_tree',
+ namespace_id: 'gitlab', project_id: 'gitlabhq',
+ id: "stable", path: "new\n\nline.txt" })
end
end
diff --git a/spec/rubocop/cop/gitlab/const_get_inherit_false_spec.rb b/spec/rubocop/cop/gitlab/const_get_inherit_false_spec.rb
new file mode 100644
index 00000000000..0ff06b431eb
--- /dev/null
+++ b/spec/rubocop/cop/gitlab/const_get_inherit_false_spec.rb
@@ -0,0 +1,80 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require 'rubocop'
+require 'rubocop/rspec/support'
+require_relative '../../../../rubocop/cop/gitlab/const_get_inherit_false'
+
+describe RuboCop::Cop::Gitlab::ConstGetInheritFalse do
+ include CopHelper
+
+ subject(:cop) { described_class.new }
+
+ context 'Object.const_get' do
+ it 'registers an offense with no 2nd argument' do
+ expect_offense(<<~PATTERN.strip_indent)
+ Object.const_get(:CONSTANT)
+ ^^^^^^^^^ Use inherit=false when using const_get.
+ PATTERN
+ end
+
+ it 'autocorrects' do
+ expect(autocorrect_source('Object.const_get(:CONSTANT)')).to eq('Object.const_get(:CONSTANT, false)')
+ end
+
+ context 'inherit=false' do
+ it 'does not register an offense' do
+ expect_no_offenses(<<~PATTERN.strip_indent)
+ Object.const_get(:CONSTANT, false)
+ PATTERN
+ end
+ end
+
+ context 'inherit=true' do
+ it 'registers an offense' do
+ expect_offense(<<~PATTERN.strip_indent)
+ Object.const_get(:CONSTANT, true)
+ ^^^^^^^^^ Use inherit=false when using const_get.
+ PATTERN
+ end
+
+ it 'autocorrects' do
+ expect(autocorrect_source('Object.const_get(:CONSTANT, true)')).to eq('Object.const_get(:CONSTANT, false)')
+ end
+ end
+ end
+
+ context 'const_get for a nested class' do
+ it 'registers an offense on reload usage' do
+ expect_offense(<<~PATTERN.strip_indent)
+ Nested::Blog.const_get(:CONSTANT)
+ ^^^^^^^^^ Use inherit=false when using const_get.
+ PATTERN
+ end
+
+ it 'autocorrects' do
+ expect(autocorrect_source('Nested::Blag.const_get(:CONSTANT)')).to eq('Nested::Blag.const_get(:CONSTANT, false)')
+ end
+
+ context 'inherit=false' do
+ it 'does not register an offense' do
+ expect_no_offenses(<<~PATTERN.strip_indent)
+ Nested::Blog.const_get(:CONSTANT, false)
+ PATTERN
+ end
+ end
+
+ context 'inherit=true' do
+ it 'registers an offense if inherit is true' do
+ expect_offense(<<~PATTERN.strip_indent)
+ Nested::Blog.const_get(:CONSTANT, true)
+ ^^^^^^^^^ Use inherit=false when using const_get.
+ PATTERN
+ end
+
+ it 'autocorrects' do
+ expect(autocorrect_source('Nested::Blag.const_get(:CONSTANT, true)')).to eq('Nested::Blag.const_get(:CONSTANT, false)')
+ end
+ end
+ end
+end
diff --git a/spec/rubocop/cop/line_break_around_conditional_block_spec.rb b/spec/rubocop/cop/line_break_around_conditional_block_spec.rb
index 892b393c307..cc933ce12c8 100644
--- a/spec/rubocop/cop/line_break_around_conditional_block_spec.rb
+++ b/spec/rubocop/cop/line_break_around_conditional_block_spec.rb
@@ -132,6 +132,19 @@ describe RuboCop::Cop::LineBreakAroundConditionalBlock do
expect(cop.offenses).to be_empty
end
+ it "doesn't flag violation for #{conditional} preceded by a block definition with a comment" do
+ source = <<~RUBY
+ on_block(param_a) do |item| # a short comment
+ #{conditional} condition
+ do_something
+ end
+ end
+ RUBY
+ inspect_source(source)
+
+ expect(cop.offenses).to be_empty
+ end
+
it "doesn't flag violation for #{conditional} preceded by a block definition using brackets" do
source = <<~RUBY
on_block(param_a) { |item|
diff --git a/spec/rubocop/cop/migration/add_timestamps_spec.rb b/spec/rubocop/cop/migration/add_timestamps_spec.rb
index fae0177d5f5..33f1bb85af8 100644
--- a/spec/rubocop/cop/migration/add_timestamps_spec.rb
+++ b/spec/rubocop/cop/migration/add_timestamps_spec.rb
@@ -9,6 +9,7 @@ describe RuboCop::Cop::Migration::AddTimestamps do
include CopHelper
subject(:cop) { described_class.new }
+
let(:migration_with_add_timestamps) do
%q(
class Users < ActiveRecord::Migration[4.2]
diff --git a/spec/rubocop/cop/migration/timestamps_spec.rb b/spec/rubocop/cop/migration/timestamps_spec.rb
index 1812818692a..cafe255dc9a 100644
--- a/spec/rubocop/cop/migration/timestamps_spec.rb
+++ b/spec/rubocop/cop/migration/timestamps_spec.rb
@@ -9,6 +9,7 @@ describe RuboCop::Cop::Migration::Timestamps do
include CopHelper
subject(:cop) { described_class.new }
+
let(:migration_with_timestamps) do
%q(
class Users < ActiveRecord::Migration[4.2]
diff --git a/spec/rubocop/cop/scalability/file_uploads_spec.rb b/spec/rubocop/cop/scalability/file_uploads_spec.rb
index 2a94fde5ba2..a35d423581c 100644
--- a/spec/rubocop/cop/scalability/file_uploads_spec.rb
+++ b/spec/rubocop/cop/scalability/file_uploads_spec.rb
@@ -10,6 +10,7 @@ describe RuboCop::Cop::Scalability::FileUploads do
include ExpectOffense
subject(:cop) { described_class.new }
+
let(:message) { 'Do not upload files without workhorse acceleration. Please refer to https://docs.gitlab.com/ee/development/uploads.html' }
context 'with required params' do
diff --git a/spec/serializers/analytics_build_entity_spec.rb b/spec/serializers/analytics_build_entity_spec.rb
index 1ff4908972a..dfa16075d20 100644
--- a/spec/serializers/analytics_build_entity_spec.rb
+++ b/spec/serializers/analytics_build_entity_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe AnalyticsBuildEntity do
diff --git a/spec/serializers/analytics_build_serializer_spec.rb b/spec/serializers/analytics_build_serializer_spec.rb
index e3b1dd93dc2..04a387fd353 100644
--- a/spec/serializers/analytics_build_serializer_spec.rb
+++ b/spec/serializers/analytics_build_serializer_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe AnalyticsBuildSerializer do
diff --git a/spec/serializers/analytics_issue_entity_spec.rb b/spec/serializers/analytics_issue_entity_spec.rb
index c5b03bdd8c1..555efe136e6 100644
--- a/spec/serializers/analytics_issue_entity_spec.rb
+++ b/spec/serializers/analytics_issue_entity_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe AnalyticsIssueEntity do
diff --git a/spec/serializers/analytics_issue_serializer_spec.rb b/spec/serializers/analytics_issue_serializer_spec.rb
index 9cb2ce13d12..9b29739a8f2 100644
--- a/spec/serializers/analytics_issue_serializer_spec.rb
+++ b/spec/serializers/analytics_issue_serializer_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe AnalyticsIssueSerializer do
diff --git a/spec/serializers/analytics_merge_request_serializer_spec.rb b/spec/serializers/analytics_merge_request_serializer_spec.rb
index a864051b2a3..c82eb28a28b 100644
--- a/spec/serializers/analytics_merge_request_serializer_spec.rb
+++ b/spec/serializers/analytics_merge_request_serializer_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe AnalyticsMergeRequestSerializer do
diff --git a/spec/serializers/analytics_stage_serializer_spec.rb b/spec/serializers/analytics_stage_serializer_spec.rb
index 86a796a2d94..1f1a0180b1f 100644
--- a/spec/serializers/analytics_stage_serializer_spec.rb
+++ b/spec/serializers/analytics_stage_serializer_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe AnalyticsStageSerializer do
diff --git a/spec/serializers/analytics_summary_serializer_spec.rb b/spec/serializers/analytics_summary_serializer_spec.rb
index 8fa0574bfd6..33a41706794 100644
--- a/spec/serializers/analytics_summary_serializer_spec.rb
+++ b/spec/serializers/analytics_summary_serializer_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe AnalyticsSummarySerializer do
diff --git a/spec/serializers/blob_entity_spec.rb b/spec/serializers/blob_entity_spec.rb
index dde59ff72df..c0687d0232e 100644
--- a/spec/serializers/blob_entity_spec.rb
+++ b/spec/serializers/blob_entity_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe BlobEntity do
diff --git a/spec/serializers/build_action_entity_spec.rb b/spec/serializers/build_action_entity_spec.rb
index ea88951ebc6..7cd1fdcda22 100644
--- a/spec/serializers/build_action_entity_spec.rb
+++ b/spec/serializers/build_action_entity_spec.rb
@@ -1,11 +1,18 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe BuildActionEntity do
let(:job) { create(:ci_build, name: 'test_job') }
let(:request) { double('request') }
+ let(:user) { create(:user) }
let(:entity) do
- described_class.new(job, request: spy('request'))
+ described_class.new(job, request: request)
+ end
+
+ before do
+ allow(request).to receive(:current_user).and_return(user)
end
describe '#as_json' do
diff --git a/spec/serializers/build_artifact_entity_spec.rb b/spec/serializers/build_artifact_entity_spec.rb
index ad0d3d3839e..09fe094fff1 100644
--- a/spec/serializers/build_artifact_entity_spec.rb
+++ b/spec/serializers/build_artifact_entity_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe BuildArtifactEntity do
diff --git a/spec/serializers/build_details_entity_spec.rb b/spec/serializers/build_details_entity_spec.rb
index d922e8246c7..91c5fd6bf2c 100644
--- a/spec/serializers/build_details_entity_spec.rb
+++ b/spec/serializers/build_details_entity_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe BuildDetailsEntity do
@@ -121,6 +123,25 @@ describe BuildDetailsEntity do
end
it { is_expected.to include(failure_reason: 'unmet_prerequisites') }
+ it { is_expected.to include(callout_message: CommitStatusPresenter.callout_failure_messages[:unmet_prerequisites]) }
+ end
+
+ context 'when the build has failed due to a missing dependency' do
+ let!(:test1) { create(:ci_build, :success, :expired, pipeline: pipeline, name: 'test1', stage_idx: 0) }
+ let!(:test2) { create(:ci_build, :success, :expired, pipeline: pipeline, name: 'test2', stage_idx: 1) }
+ let!(:build) { create(:ci_build, :pending, pipeline: pipeline, stage_idx: 2, options: { dependencies: %w(test1 test2) }) }
+ let(:message) { subject[:callout_message] }
+
+ before do
+ build.drop!(:missing_dependency_failure)
+ end
+
+ it { is_expected.to include(failure_reason: 'missing_dependency_failure') }
+
+ it 'includes the failing dependencies in the callout message' do
+ expect(message).to include('test1')
+ expect(message).to include('test2')
+ end
end
context 'when a build has environment with latest deployment' do
diff --git a/spec/serializers/build_serializer_spec.rb b/spec/serializers/build_serializer_spec.rb
index 302ef147eb2..c7bb6864361 100644
--- a/spec/serializers/build_serializer_spec.rb
+++ b/spec/serializers/build_serializer_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe BuildSerializer do
diff --git a/spec/serializers/build_trace_entity_spec.rb b/spec/serializers/build_trace_entity_spec.rb
new file mode 100644
index 00000000000..bafead04a51
--- /dev/null
+++ b/spec/serializers/build_trace_entity_spec.rb
@@ -0,0 +1,63 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe BuildTraceEntity do
+ let(:build) { build_stubbed(:ci_build) }
+ let(:request) { double('request') }
+
+ let(:stream) do
+ Gitlab::Ci::Trace::Stream.new do
+ StringIO.new('the-trace')
+ end
+ end
+
+ let(:build_trace) do
+ Ci::BuildTrace.new(build: build, stream: stream, content_format: content_format, state: nil)
+ end
+
+ let(:entity) do
+ described_class.new(build_trace, request: request)
+ end
+
+ subject { entity.as_json }
+
+ shared_examples 'includes build and trace metadata' do
+ it 'includes build attributes' do
+ expect(subject[:id]).to eq(build.id)
+ expect(subject[:status]).to eq(build.status)
+ expect(subject[:complete]).to eq(build.complete?)
+ end
+
+ it 'includes trace metadata' do
+ expect(subject).to include(:state)
+ expect(subject).to include(:append)
+ expect(subject).to include(:truncated)
+ expect(subject).to include(:offset)
+ expect(subject).to include(:size)
+ expect(subject).to include(:total)
+ end
+ end
+
+ context 'when content format is :json' do
+ let(:content_format) { :json }
+
+ it_behaves_like 'includes build and trace metadata'
+
+ it 'includes the trace content in json' do
+ expect(subject[:lines]).to eq([
+ { offset: 0, content: [{ text: 'the-trace' }] }
+ ])
+ end
+ end
+
+ context 'when content format is :html' do
+ let(:content_format) { :html }
+
+ it_behaves_like 'includes build and trace metadata'
+
+ it 'includes the trace content in json' do
+ expect(subject[:html]).to eq('<span>the-trace</span>')
+ end
+ end
+end
diff --git a/spec/serializers/cluster_application_entity_spec.rb b/spec/serializers/cluster_application_entity_spec.rb
index 76ecca06522..cb9325986d7 100644
--- a/spec/serializers/cluster_application_entity_spec.rb
+++ b/spec/serializers/cluster_application_entity_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe ClusterApplicationEntity do
diff --git a/spec/serializers/cluster_basic_entity_spec.rb b/spec/serializers/cluster_basic_entity_spec.rb
index 6762eb6ab3d..be03ee91784 100644
--- a/spec/serializers/cluster_basic_entity_spec.rb
+++ b/spec/serializers/cluster_basic_entity_spec.rb
@@ -1,8 +1,11 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe ClusterBasicEntity do
describe '#as_json' do
subject { described_class.new(cluster, request: request).as_json }
+
let(:maintainer) { create(:user) }
let(:developer) { create(:user) }
let(:current_user) { maintainer }
diff --git a/spec/serializers/cluster_entity_spec.rb b/spec/serializers/cluster_entity_spec.rb
index d6a43fd0f00..22b9166f922 100644
--- a/spec/serializers/cluster_entity_spec.rb
+++ b/spec/serializers/cluster_entity_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe ClusterEntity do
diff --git a/spec/serializers/cluster_serializer_spec.rb b/spec/serializers/cluster_serializer_spec.rb
index 5e9f7a45891..db0e65ca0fa 100644
--- a/spec/serializers/cluster_serializer_spec.rb
+++ b/spec/serializers/cluster_serializer_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe ClusterSerializer do
diff --git a/spec/serializers/commit_entity_spec.rb b/spec/serializers/commit_entity_spec.rb
index b9995818e98..03e1c89a5e6 100644
--- a/spec/serializers/commit_entity_spec.rb
+++ b/spec/serializers/commit_entity_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe CommitEntity do
diff --git a/spec/serializers/container_repository_entity_spec.rb b/spec/serializers/container_repository_entity_spec.rb
index 15466bce514..799a8d5c122 100644
--- a/spec/serializers/container_repository_entity_spec.rb
+++ b/spec/serializers/container_repository_entity_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe ContainerRepositoryEntity do
@@ -23,6 +25,18 @@ describe ContainerRepositoryEntity do
expect(subject).to include(:id, :path, :location, :tags_path)
end
+ context 'when project is not preset in the request' do
+ before do
+ allow(request).to receive(:respond_to?).and_return(false)
+ allow(request).to receive(:project).and_return(nil)
+ end
+
+ it 'uses project from the object' do
+ expect(request.project).not_to equal(project)
+ expect(subject).to include(:tags_path)
+ end
+ end
+
context 'when user can manage repositories' do
before do
project.add_developer(user)
diff --git a/spec/serializers/container_tag_entity_spec.rb b/spec/serializers/container_tag_entity_spec.rb
index ceb828a1cc5..8a67a189761 100644
--- a/spec/serializers/container_tag_entity_spec.rb
+++ b/spec/serializers/container_tag_entity_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe ContainerTagEntity do
diff --git a/spec/serializers/deploy_key_entity_spec.rb b/spec/serializers/deploy_key_entity_spec.rb
index 01264cf7fb5..607adfc2488 100644
--- a/spec/serializers/deploy_key_entity_spec.rb
+++ b/spec/serializers/deploy_key_entity_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe DeployKeyEntity do
@@ -6,14 +8,15 @@ describe DeployKeyEntity do
let(:user) { create(:user) }
let(:project) { create(:project, :internal)}
let(:project_private) { create(:project, :private)}
- let!(:project_pending_delete) { create(:project, :internal, pending_delete: true) }
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!(:deploy_key_pending_delete) { create(:deploy_keys_project, project: project_pending_delete, deploy_key: deploy_key) }
let(:entity) { described_class.new(deploy_key, user: user) }
+ before do
+ project.deploy_keys << deploy_key
+ project_private.deploy_keys << deploy_key
+ end
+
describe 'returns deploy keys with projects a user can read' do
let(:expected_result) do
{
@@ -44,17 +47,30 @@ describe DeployKeyEntity do
it { expect(entity.as_json).to eq(expected_result) }
end
- describe 'returns can_edit true if user is a maintainer of project' do
+ context 'user is an admin' do
+ let(:user) { create(:user, :admin) }
+
+ it { expect(entity.as_json).to include(can_edit: true) }
+ end
+
+ context 'user is a project maintainer' do
before do
project.add_maintainer(user)
end
- it { expect(entity.as_json).to include(can_edit: true) }
- end
+ context 'project deploy key' do
+ it { expect(entity.as_json).to include(can_edit: true) }
+ end
- describe 'returns can_edit true if a user admin' do
- let(:user) { create(:user, :admin) }
+ context 'public deploy key' do
+ let(:deploy_key_public) { create(:deploy_key, public: true) }
+ let(:entity_public) { described_class.new(deploy_key_public, { user: user, project: project }) }
- it { expect(entity.as_json).to include(can_edit: true) }
+ before do
+ project.deploy_keys << deploy_key_public
+ end
+
+ it { expect(entity_public.as_json).to include(can_edit: true) }
+ end
end
end
diff --git a/spec/serializers/deployment_entity_spec.rb b/spec/serializers/deployment_entity_spec.rb
index 79f89dc1a9c..d7816a3503d 100644
--- a/spec/serializers/deployment_entity_spec.rb
+++ b/spec/serializers/deployment_entity_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe DeploymentEntity do
diff --git a/spec/serializers/detailed_status_entity_spec.rb b/spec/serializers/detailed_status_entity_spec.rb
index 62f57ca8689..a4b51f1e02e 100644
--- a/spec/serializers/detailed_status_entity_spec.rb
+++ b/spec/serializers/detailed_status_entity_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe DetailedStatusEntity do
diff --git a/spec/serializers/diff_file_entity_spec.rb b/spec/serializers/diff_file_entity_spec.rb
index 92b649f5b6c..0c2e7c1e3eb 100644
--- a/spec/serializers/diff_file_entity_spec.rb
+++ b/spec/serializers/diff_file_entity_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe DiffFileEntity do
diff --git a/spec/serializers/diff_line_serializer_spec.rb b/spec/serializers/diff_line_serializer_spec.rb
index 6dd8abd0579..bdfcb8e2459 100644
--- a/spec/serializers/diff_line_serializer_spec.rb
+++ b/spec/serializers/diff_line_serializer_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe DiffLineSerializer do
diff --git a/spec/serializers/diff_viewer_entity_spec.rb b/spec/serializers/diff_viewer_entity_spec.rb
index 66ac6ef2adc..76d2728c597 100644
--- a/spec/serializers/diff_viewer_entity_spec.rb
+++ b/spec/serializers/diff_viewer_entity_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe DiffViewerEntity do
diff --git a/spec/serializers/diffs_entity_spec.rb b/spec/serializers/diffs_entity_spec.rb
index 19a843b0cb7..59acbdac3d0 100644
--- a/spec/serializers/diffs_entity_spec.rb
+++ b/spec/serializers/diffs_entity_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe DiffsEntity do
diff --git a/spec/serializers/diffs_metadata_entity_spec.rb b/spec/serializers/diffs_metadata_entity_spec.rb
new file mode 100644
index 00000000000..aaca393ec27
--- /dev/null
+++ b/spec/serializers/diffs_metadata_entity_spec.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe DiffsMetadataEntity do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :repository) }
+ let(:request) { EntityRequest.new(project: project, current_user: user) }
+ let(:merge_request) { create(:merge_request_with_diffs, target_project: project, source_project: project) }
+ let(:merge_request_diffs) { merge_request.merge_request_diffs }
+ let(:merge_request_diff) { merge_request_diffs.last }
+
+ let(:entity) do
+ described_class.new(merge_request_diff.diffs,
+ request: request,
+ merge_request: merge_request,
+ merge_request_diffs: merge_request_diffs)
+ end
+
+ context 'as json' do
+ subject { entity.as_json }
+
+ it 'contain only required attributes' do
+ expect(subject.keys).to contain_exactly(
+ # Inherited attributes
+ :real_size, :size, :branch_name,
+ :target_branch_name, :commit, :merge_request_diff,
+ :start_version, :latest_diff, :latest_version_path,
+ :added_lines, :removed_lines, :render_overflow_warning,
+ :email_patch_path, :plain_diff_path,
+ :merge_request_diffs,
+ # Attributes
+ :diff_files
+ )
+ end
+
+ describe 'diff_files' do
+ it 'returns diff files metadata' do
+ payload =
+ DiffFileMetadataEntity.represent(merge_request_diff.diffs.diff_files).as_json
+
+ expect(subject[:diff_files]).to eq(payload)
+ end
+ end
+ end
+end
diff --git a/spec/serializers/discussion_entity_spec.rb b/spec/serializers/discussion_entity_spec.rb
index 138749b0fdf..b194623099d 100644
--- a/spec/serializers/discussion_entity_spec.rb
+++ b/spec/serializers/discussion_entity_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe DiscussionEntity do
diff --git a/spec/serializers/entity_date_helper_spec.rb b/spec/serializers/entity_date_helper_spec.rb
index 73506954965..6f99074c3a2 100644
--- a/spec/serializers/entity_date_helper_spec.rb
+++ b/spec/serializers/entity_date_helper_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe EntityDateHelper do
diff --git a/spec/serializers/entity_request_spec.rb b/spec/serializers/entity_request_spec.rb
index 86654adfd54..947c4b165d3 100644
--- a/spec/serializers/entity_request_spec.rb
+++ b/spec/serializers/entity_request_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe EntityRequest do
diff --git a/spec/serializers/environment_entity_spec.rb b/spec/serializers/environment_entity_spec.rb
index 906449f470b..b8910fd6eee 100644
--- a/spec/serializers/environment_entity_spec.rb
+++ b/spec/serializers/environment_entity_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe EnvironmentEntity do
diff --git a/spec/serializers/environment_serializer_spec.rb b/spec/serializers/environment_serializer_spec.rb
index 375a28a8c72..304457d83a1 100644
--- a/spec/serializers/environment_serializer_spec.rb
+++ b/spec/serializers/environment_serializer_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe EnvironmentSerializer do
diff --git a/spec/serializers/environment_status_entity_spec.rb b/spec/serializers/environment_status_entity_spec.rb
index cb4749f019f..0687751fd67 100644
--- a/spec/serializers/environment_status_entity_spec.rb
+++ b/spec/serializers/environment_status_entity_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe EnvironmentStatusEntity do
diff --git a/spec/serializers/evidences/evidence_entity_spec.rb b/spec/serializers/evidences/evidence_entity_spec.rb
new file mode 100644
index 00000000000..531708e3be6
--- /dev/null
+++ b/spec/serializers/evidences/evidence_entity_spec.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Evidences::EvidenceEntity do
+ let(:evidence) { build(:evidence) }
+ let(:entity) { described_class.new(evidence) }
+
+ subject { entity.as_json }
+
+ it 'exposes the expected fields' do
+ expect(subject.keys).to contain_exactly(:release)
+ end
+end
diff --git a/spec/serializers/evidences/evidence_serializer_spec.rb b/spec/serializers/evidences/evidence_serializer_spec.rb
new file mode 100644
index 00000000000..5322f6a43fc
--- /dev/null
+++ b/spec/serializers/evidences/evidence_serializer_spec.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Evidences::EvidenceSerializer do
+ it 'represents an EvidenceEntity entity' do
+ expect(described_class.entity_class).to eq(Evidences::EvidenceEntity)
+ end
+end
diff --git a/spec/serializers/evidences/issue_entity_spec.rb b/spec/serializers/evidences/issue_entity_spec.rb
new file mode 100644
index 00000000000..915df986887
--- /dev/null
+++ b/spec/serializers/evidences/issue_entity_spec.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Evidences::IssueEntity do
+ let(:entity) { described_class.new(build(:issue)) }
+
+ subject { entity.as_json }
+
+ it 'exposes the expected fields' do
+ expect(subject.keys).to contain_exactly(:id, :title, :description, :state, :iid, :confidential, :created_at, :due_date)
+ end
+end
diff --git a/spec/serializers/evidences/milestone_entity_spec.rb b/spec/serializers/evidences/milestone_entity_spec.rb
new file mode 100644
index 00000000000..68eb12093da
--- /dev/null
+++ b/spec/serializers/evidences/milestone_entity_spec.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Evidences::MilestoneEntity do
+ let(:milestone) { build(:milestone) }
+ let(:entity) { described_class.new(milestone) }
+
+ subject { entity.as_json }
+
+ it 'exposes the expected fields' do
+ expect(subject.keys).to contain_exactly(:id, :title, :description, :state, :iid, :created_at, :due_date, :issues)
+ end
+
+ context 'when there are issues linked to this milestone' do
+ let(:issue_1) { build(:issue) }
+ let(:issue_2) { build(:issue) }
+ let(:milestone) { build(:milestone, issues: [issue_1, issue_2]) }
+
+ it 'exposes these issues' do
+ expect(subject[:issues]).to contain_exactly(
+ Evidences::IssueEntity.new(issue_1).as_json,
+ Evidences::IssueEntity.new(issue_2).as_json
+ )
+ end
+ end
+
+ context 'when the release has no milestone' do
+ let(:milestone) { build(:milestone, issues: []) }
+
+ it 'exposes an empty array for milestones' do
+ expect(subject[:issues]).to be_empty
+ end
+ end
+end
diff --git a/spec/serializers/evidences/project_entity_spec.rb b/spec/serializers/evidences/project_entity_spec.rb
new file mode 100644
index 00000000000..01c160425a8
--- /dev/null
+++ b/spec/serializers/evidences/project_entity_spec.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Evidences::ProjectEntity do
+ let(:entity) { described_class.new(build(:project)) }
+
+ subject { entity.as_json }
+
+ it 'exposes the expected fields' do
+ expect(subject.keys).to contain_exactly(:id, :name, :description, :created_at)
+ end
+end
diff --git a/spec/serializers/evidences/release_entity_spec.rb b/spec/serializers/evidences/release_entity_spec.rb
new file mode 100644
index 00000000000..8e2be748169
--- /dev/null
+++ b/spec/serializers/evidences/release_entity_spec.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Evidences::ReleaseEntity do
+ let(:release) { build(:release) }
+ let(:entity) { described_class.new(release) }
+
+ subject { entity.as_json }
+
+ it 'exposes the expected fields' do
+ expect(subject.keys).to contain_exactly(:id, :tag_name, :name, :description, :created_at, :project, :milestones)
+ end
+
+ context 'when the release has milestones' do
+ let(:project) { create(:project) }
+ let(:milestone_1) { build(:milestone, project: project) }
+ let(:milestone_2) { build(:milestone, project: project) }
+ let(:release) { build(:release, project: project, milestones: [milestone_1, milestone_2]) }
+
+ it 'exposes these milestones' do
+ expect(subject[:milestones]).to contain_exactly(
+ Evidences::MilestoneEntity.new(milestone_1).as_json,
+ Evidences::MilestoneEntity.new(milestone_2).as_json
+ )
+ end
+ end
+
+ context 'when the release has no milestone' do
+ let(:release) { build(:release, milestones: []) }
+
+ it 'exposes an empty array for milestones' do
+ expect(subject[:milestones]).to be_empty
+ end
+ end
+end
diff --git a/spec/serializers/evidences/release_serializer_spec.rb b/spec/serializers/evidences/release_serializer_spec.rb
new file mode 100644
index 00000000000..a0dbf50137c
--- /dev/null
+++ b/spec/serializers/evidences/release_serializer_spec.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Evidences::ReleaseSerializer do
+ it 'represents an Evidence::ReleaseEntity entity' do
+ expect(described_class.entity_class).to eq(Evidences::ReleaseEntity)
+ end
+end
diff --git a/spec/serializers/group_child_entity_spec.rb b/spec/serializers/group_child_entity_spec.rb
index 00e2f931549..198a55d5433 100644
--- a/spec/serializers/group_child_entity_spec.rb
+++ b/spec/serializers/group_child_entity_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe GroupChildEntity do
diff --git a/spec/serializers/group_child_serializer_spec.rb b/spec/serializers/group_child_serializer_spec.rb
index c9e8535585b..128e06cd172 100644
--- a/spec/serializers/group_child_serializer_spec.rb
+++ b/spec/serializers/group_child_serializer_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe GroupChildSerializer do
diff --git a/spec/serializers/group_variable_entity_spec.rb b/spec/serializers/group_variable_entity_spec.rb
index f6de7d01f98..22c98b418dd 100644
--- a/spec/serializers/group_variable_entity_spec.rb
+++ b/spec/serializers/group_variable_entity_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe GroupVariableEntity do
diff --git a/spec/serializers/issue_entity_spec.rb b/spec/serializers/issue_entity_spec.rb
index 53278062de9..224ed0b402f 100644
--- a/spec/serializers/issue_entity_spec.rb
+++ b/spec/serializers/issue_entity_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe IssueEntity do
diff --git a/spec/serializers/issue_serializer_spec.rb b/spec/serializers/issue_serializer_spec.rb
index b8255e004d0..e25becc4709 100644
--- a/spec/serializers/issue_serializer_spec.rb
+++ b/spec/serializers/issue_serializer_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe IssueSerializer do
diff --git a/spec/serializers/job_entity_spec.rb b/spec/serializers/job_entity_spec.rb
index 8de61d4d466..4b2d4701334 100644
--- a/spec/serializers/job_entity_spec.rb
+++ b/spec/serializers/job_entity_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe JobEntity do
diff --git a/spec/serializers/label_serializer_spec.rb b/spec/serializers/label_serializer_spec.rb
index c58c7da1f9e..7e1bb2cc127 100644
--- a/spec/serializers/label_serializer_spec.rb
+++ b/spec/serializers/label_serializer_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe LabelSerializer do
diff --git a/spec/serializers/lfs_file_lock_entity_spec.rb b/spec/serializers/lfs_file_lock_entity_spec.rb
index 5919f473a90..4ffffad7d5a 100644
--- a/spec/serializers/lfs_file_lock_entity_spec.rb
+++ b/spec/serializers/lfs_file_lock_entity_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe LfsFileLockEntity do
diff --git a/spec/serializers/merge_request_diff_entity_spec.rb b/spec/serializers/merge_request_diff_entity_spec.rb
index 84f6833d88a..062f17963c0 100644
--- a/spec/serializers/merge_request_diff_entity_spec.rb
+++ b/spec/serializers/merge_request_diff_entity_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe MergeRequestDiffEntity do
diff --git a/spec/serializers/merge_request_for_pipeline_entity_spec.rb b/spec/serializers/merge_request_for_pipeline_entity_spec.rb
index e49b45bc7d7..b39fbce8c3a 100644
--- a/spec/serializers/merge_request_for_pipeline_entity_spec.rb
+++ b/spec/serializers/merge_request_for_pipeline_entity_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe MergeRequestForPipelineEntity do
diff --git a/spec/serializers/merge_request_serializer_spec.rb b/spec/serializers/merge_request_serializer_spec.rb
index cf0b8ea9b40..a99f11168c0 100644
--- a/spec/serializers/merge_request_serializer_spec.rb
+++ b/spec/serializers/merge_request_serializer_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe MergeRequestSerializer do
diff --git a/spec/serializers/merge_request_user_entity_spec.rb b/spec/serializers/merge_request_user_entity_spec.rb
index c91ea4aa681..47b9b0a57ab 100644
--- a/spec/serializers/merge_request_user_entity_spec.rb
+++ b/spec/serializers/merge_request_user_entity_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe MergeRequestUserEntity do
diff --git a/spec/serializers/merge_request_widget_entity_spec.rb b/spec/serializers/merge_request_widget_entity_spec.rb
index ffbfac9b326..4872b23d26b 100644
--- a/spec/serializers/merge_request_widget_entity_spec.rb
+++ b/spec/serializers/merge_request_widget_entity_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe MergeRequestWidgetEntity do
diff --git a/spec/serializers/note_entity_spec.rb b/spec/serializers/note_entity_spec.rb
index 13cda781cda..cec07cda063 100644
--- a/spec/serializers/note_entity_spec.rb
+++ b/spec/serializers/note_entity_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe NoteEntity do
diff --git a/spec/serializers/paginated_diff_entity_spec.rb b/spec/serializers/paginated_diff_entity_spec.rb
new file mode 100644
index 00000000000..7432e072318
--- /dev/null
+++ b/spec/serializers/paginated_diff_entity_spec.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe PaginatedDiffEntity do
+ let(:user) { create(:user) }
+ let(:request) { double('request', current_user: user) }
+ let(:merge_request) { create(:merge_request, :with_diffs) }
+ let(:diff_batch) { merge_request.merge_request_diff.diffs_in_batch(2, 3, diff_options: nil) }
+ let(:options) do
+ {
+ request: request,
+ merge_request: merge_request,
+ pagination_data: diff_batch.pagination_data
+ }
+ end
+ let(:entity) { described_class.new(diff_batch, options) }
+
+ subject { entity.as_json }
+
+ it 'exposes diff_files' do
+ expect(subject[:diff_files]).to be_present
+ end
+
+ it 'exposes pagination data' do
+ expect(subject[:pagination]).to eq(
+ current_page: 2,
+ next_page: 3,
+ next_page_href: "/#{merge_request.project.full_path}/merge_requests/#{merge_request.iid}/diffs_batch.json?page=3",
+ total_pages: 7
+ )
+ end
+end
diff --git a/spec/serializers/pipeline_details_entity_spec.rb b/spec/serializers/pipeline_details_entity_spec.rb
index 8e73a3e67c6..b180ede51eb 100644
--- a/spec/serializers/pipeline_details_entity_spec.rb
+++ b/spec/serializers/pipeline_details_entity_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe PipelineDetailsEntity do
@@ -136,5 +138,40 @@ describe PipelineDetailsEntity do
expect(subject[:flags][:yaml_errors]).to be false
end
end
+
+ context 'when pipeline is triggered by other pipeline' do
+ let(:pipeline) { create(:ci_empty_pipeline) }
+
+ before do
+ create(:ci_sources_pipeline, pipeline: pipeline)
+ end
+
+ it 'contains an information about depedent pipeline' do
+ expect(subject[:triggered_by]).to be_a(Hash)
+ expect(subject[:triggered_by][:path]).not_to be_nil
+ expect(subject[:triggered_by][:details]).not_to be_nil
+ expect(subject[:triggered_by][:details][:status]).not_to be_nil
+ expect(subject[:triggered_by][:project]).not_to be_nil
+ end
+ end
+
+ context 'when pipeline triggered other pipeline' do
+ let(:pipeline) { create(:ci_empty_pipeline) }
+ let(:build) { create(:ci_build, pipeline: pipeline) }
+
+ before do
+ create(:ci_sources_pipeline, source_job: build)
+ create(:ci_sources_pipeline, source_job: build)
+ end
+
+ it 'contains an information about depedent pipeline' do
+ expect(subject[:triggered]).to be_a(Array)
+ expect(subject[:triggered].length).to eq(2)
+ expect(subject[:triggered].first[:path]).not_to be_nil
+ expect(subject[:triggered].first[:details]).not_to be_nil
+ expect(subject[:triggered].first[:details][:status]).not_to be_nil
+ expect(subject[:triggered].first[:project]).not_to be_nil
+ end
+ end
end
end
diff --git a/spec/serializers/pipeline_entity_spec.rb b/spec/serializers/pipeline_entity_spec.rb
index eb9972d3e4d..02c5b817ea4 100644
--- a/spec/serializers/pipeline_entity_spec.rb
+++ b/spec/serializers/pipeline_entity_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe PipelineEntity do
diff --git a/spec/serializers/pipeline_serializer_spec.rb b/spec/serializers/pipeline_serializer_spec.rb
index 0ccdea34f74..ce5264ec8bb 100644
--- a/spec/serializers/pipeline_serializer_spec.rb
+++ b/spec/serializers/pipeline_serializer_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe PipelineSerializer do
@@ -137,6 +139,7 @@ describe PipelineSerializer do
describe 'number of queries when preloaded' do
subject { serializer.represent(resource, preload: true) }
+
let(:resource) { Ci::Pipeline.all }
before do
@@ -156,7 +159,7 @@ describe PipelineSerializer do
it 'verifies number of queries', :request_store do
recorded = ActiveRecord::QueryRecorder.new { subject }
- expected_queries = Gitlab.ee? ? 38 : 31
+ expected_queries = Gitlab.ee? ? 38 : 35
expect(recorded.count).to be_within(2).of(expected_queries)
expect(recorded.cached_count).to eq(0)
@@ -177,7 +180,8 @@ describe PipelineSerializer do
# pipeline. With the same ref this check is cached but if refs are
# different then there is an extra query per ref
# https://gitlab.com/gitlab-org/gitlab-foss/issues/46368
- expected_queries = Gitlab.ee? ? 44 : 38
+ expected_queries = Gitlab.ee? ? 44 : 41
+
expect(recorded.count).to be_within(2).of(expected_queries)
expect(recorded.cached_count).to eq(0)
end
diff --git a/spec/serializers/project_mirror_entity_spec.rb b/spec/serializers/project_mirror_entity_spec.rb
index ad0a8bbdff0..0d64199ecf6 100644
--- a/spec/serializers/project_mirror_entity_spec.rb
+++ b/spec/serializers/project_mirror_entity_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe ProjectMirrorEntity do
diff --git a/spec/serializers/project_mirror_serializer_spec.rb b/spec/serializers/project_mirror_serializer_spec.rb
index 5e47163532a..b50c2267ced 100644
--- a/spec/serializers/project_mirror_serializer_spec.rb
+++ b/spec/serializers/project_mirror_serializer_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe ProjectMirrorSerializer do
diff --git a/spec/serializers/project_note_entity_spec.rb b/spec/serializers/project_note_entity_spec.rb
index dafd1cf603e..7c76f230781 100644
--- a/spec/serializers/project_note_entity_spec.rb
+++ b/spec/serializers/project_note_entity_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe ProjectNoteEntity do
diff --git a/spec/serializers/remote_mirror_entity_spec.rb b/spec/serializers/remote_mirror_entity_spec.rb
index 885b0b9b423..5f4aac213be 100644
--- a/spec/serializers/remote_mirror_entity_spec.rb
+++ b/spec/serializers/remote_mirror_entity_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe RemoteMirrorEntity do
diff --git a/spec/serializers/request_aware_entity_spec.rb b/spec/serializers/request_aware_entity_spec.rb
index aa666b961dc..8ddb35f5f61 100644
--- a/spec/serializers/request_aware_entity_spec.rb
+++ b/spec/serializers/request_aware_entity_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe RequestAwareEntity do
diff --git a/spec/serializers/runner_entity_spec.rb b/spec/serializers/runner_entity_spec.rb
index ba99d568eba..11a6aba431b 100644
--- a/spec/serializers/runner_entity_spec.rb
+++ b/spec/serializers/runner_entity_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe RunnerEntity do
diff --git a/spec/serializers/stage_entity_spec.rb b/spec/serializers/stage_entity_spec.rb
index 6b1185d1283..58573df31e7 100644
--- a/spec/serializers/stage_entity_spec.rb
+++ b/spec/serializers/stage_entity_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe StageEntity do
diff --git a/spec/serializers/test_case_entity_spec.rb b/spec/serializers/test_case_entity_spec.rb
index cc5f086ca4e..84203adea2c 100644
--- a/spec/serializers/test_case_entity_spec.rb
+++ b/spec/serializers/test_case_entity_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe TestCaseEntity do
diff --git a/spec/serializers/test_report_entity_spec.rb b/spec/serializers/test_report_entity_spec.rb
new file mode 100644
index 00000000000..5913d1c0208
--- /dev/null
+++ b/spec/serializers/test_report_entity_spec.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe TestReportEntity do
+ let(:pipeline) { create(:ci_pipeline, :with_test_reports) }
+ let(:entity) { described_class.new(pipeline.test_reports) }
+
+ describe '#as_json' do
+ subject(:as_json) { entity.as_json }
+
+ it 'contains the total time' do
+ expect(as_json).to include(:total_time)
+ end
+
+ it 'contains the counts' do
+ expect(as_json).to include(:total_count, :success_count, :failed_count, :skipped_count, :error_count)
+ end
+
+ it 'contains the test suites' do
+ expect(as_json).to include(:test_suites)
+ expect(as_json[:test_suites].count).to eq(1)
+ end
+ end
+end
diff --git a/spec/serializers/test_reports_comparer_entity_spec.rb b/spec/serializers/test_reports_comparer_entity_spec.rb
index 4a951bbbde4..2627ad536e4 100644
--- a/spec/serializers/test_reports_comparer_entity_spec.rb
+++ b/spec/serializers/test_reports_comparer_entity_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe TestReportsComparerEntity do
diff --git a/spec/serializers/test_reports_comparer_serializer_spec.rb b/spec/serializers/test_reports_comparer_serializer_spec.rb
index 62dc6f486c5..0d833afe9e4 100644
--- a/spec/serializers/test_reports_comparer_serializer_spec.rb
+++ b/spec/serializers/test_reports_comparer_serializer_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe TestReportsComparerSerializer do
diff --git a/spec/serializers/test_suite_comparer_entity_spec.rb b/spec/serializers/test_suite_comparer_entity_spec.rb
index 4b2cca2c68c..e22387130a1 100644
--- a/spec/serializers/test_suite_comparer_entity_spec.rb
+++ b/spec/serializers/test_suite_comparer_entity_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe TestSuiteComparerEntity do
diff --git a/spec/serializers/test_suite_entity_spec.rb b/spec/serializers/test_suite_entity_spec.rb
new file mode 100644
index 00000000000..54dca3214b7
--- /dev/null
+++ b/spec/serializers/test_suite_entity_spec.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe TestSuiteEntity do
+ let(:pipeline) { create(:ci_pipeline, :with_test_reports) }
+ let(:entity) { described_class.new(pipeline.test_reports.test_suites.values.first) }
+
+ describe '#as_json' do
+ subject(:as_json) { entity.as_json }
+
+ it 'contains the suite name' do
+ expect(as_json).to include(:name)
+ end
+
+ it 'contains the total time' do
+ expect(as_json).to include(:total_time)
+ end
+
+ it 'contains the counts' do
+ expect(as_json).to include(:total_count, :success_count, :failed_count, :skipped_count, :error_count)
+ end
+
+ it 'contains the test cases' do
+ expect(as_json).to include(:test_cases)
+ expect(as_json[:test_cases].count).to eq(4)
+ end
+ end
+end
diff --git a/spec/serializers/trigger_variable_entity_spec.rb b/spec/serializers/trigger_variable_entity_spec.rb
index 66567c05f52..f5a21f943d8 100644
--- a/spec/serializers/trigger_variable_entity_spec.rb
+++ b/spec/serializers/trigger_variable_entity_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe TriggerVariableEntity do
diff --git a/spec/serializers/user_entity_spec.rb b/spec/serializers/user_entity_spec.rb
index cd778e49107..edb49757b38 100644
--- a/spec/serializers/user_entity_spec.rb
+++ b/spec/serializers/user_entity_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe UserEntity do
diff --git a/spec/serializers/variable_entity_spec.rb b/spec/serializers/variable_entity_spec.rb
index 10664ff66ec..742b14fb3d3 100644
--- a/spec/serializers/variable_entity_spec.rb
+++ b/spec/serializers/variable_entity_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe VariableEntity do
diff --git a/spec/services/application_settings/update_service_spec.rb b/spec/services/application_settings/update_service_spec.rb
index 51fb43907a6..6e1fdb7aad0 100644
--- a/spec/services/application_settings/update_service_spec.rb
+++ b/spec/services/application_settings/update_service_spec.rb
@@ -147,35 +147,44 @@ describe ApplicationSettings::UpdateService do
using RSpec::Parameterized::TableSyntax
where(:params_performance_bar_enabled,
- :params_performance_bar_allowed_group_path,
- :previous_performance_bar_allowed_group_id,
- :expected_performance_bar_allowed_group_id) do
- true | '' | nil | nil
- true | '' | 42_000_000 | nil
- true | nil | nil | nil
- true | nil | 42_000_000 | nil
- true | 'foo' | nil | nil
- true | 'foo' | 42_000_000 | nil
- true | 'group_a' | nil | 42_000_000
- true | 'group_b' | 42_000_000 | 43_000_000
- true | 'group_a' | 42_000_000 | 42_000_000
- false | '' | nil | nil
- false | '' | 42_000_000 | nil
- false | nil | nil | nil
- false | nil | 42_000_000 | nil
- false | 'foo' | nil | nil
- false | 'foo' | 42_000_000 | nil
- false | 'group_a' | nil | nil
- false | 'group_b' | 42_000_000 | nil
- false | 'group_a' | 42_000_000 | nil
+ :params_performance_bar_allowed_group_path,
+ :previous_performance_bar_allowed_group_id,
+ :expected_performance_bar_allowed_group_id,
+ :expected_valid) do
+ true | '' | nil | nil | true
+ true | '' | 42_000_000 | nil | true
+ true | nil | nil | nil | true
+ true | nil | 42_000_000 | nil | true
+ true | 'foo' | nil | nil | false
+ true | 'foo' | 42_000_000 | 42_000_000 | false
+ true | 'group_a' | nil | 42_000_000 | true
+ true | 'group_b' | 42_000_000 | 43_000_000 | true
+ true | 'group_b/' | 42_000_000 | 43_000_000 | true
+ true | 'group_a' | 42_000_000 | 42_000_000 | true
+ false | '' | nil | nil | true
+ false | '' | 42_000_000 | nil | true
+ false | nil | nil | nil | true
+ false | nil | 42_000_000 | nil | true
+ false | 'foo' | nil | nil | true
+ false | 'foo' | 42_000_000 | nil | true
+ false | 'group_a' | nil | nil | true
+ false | 'group_b' | 42_000_000 | nil | true
+ false | 'group_a' | 42_000_000 | nil | true
+ nil | '' | nil | nil | true
+ nil | 'foo' | nil | nil | false
+ nil | 'group_a' | nil | 42_000_000 | true
end
with_them do
let(:params) do
{
- performance_bar_enabled: params_performance_bar_enabled,
performance_bar_allowed_group_path: params_performance_bar_allowed_group_path
- }
+ }.tap do |params_hash|
+ # Treat nil in the table as missing
+ unless params_performance_bar_enabled.nil?
+ params_hash[:performance_bar_enabled] = params_performance_bar_enabled
+ end
+ end
end
before do
@@ -202,6 +211,14 @@ describe ApplicationSettings::UpdateService do
.not_to change(application_settings, :performance_bar_allowed_group_id)
end
end
+
+ it 'adds errors to the model for invalid params' do
+ expect(subject.execute).to eq(expected_valid)
+
+ unless expected_valid
+ expect(application_settings.errors[:performance_bar_allowed_group_id]).to be_present
+ end
+ end
end
context 'when :performance_bar_allowed_group_path is not present' do
@@ -221,7 +238,7 @@ describe ApplicationSettings::UpdateService do
let(:group) { create(:group) }
let(:params) { { performance_bar_allowed_group_path: group.full_path } }
- it 'implicitely defaults to true' do
+ it 'implicitly defaults to true' do
expect { subject.execute }
.to change(application_settings, :performance_bar_allowed_group_id)
.from(nil).to(group.id)
@@ -295,4 +312,26 @@ describe ApplicationSettings::UpdateService do
expect(application_settings.raw_blob_request_limit).to eq(600)
end
end
+
+ context 'when protected path settings are passed' do
+ let(:params) do
+ {
+ throttle_protected_paths_enabled: 1,
+ throttle_protected_paths_period_in_seconds: 600,
+ throttle_protected_paths_requests_per_period: 100,
+ protected_paths_raw: "/users/password\r\n/users/sign_in\r\n"
+ }
+ end
+
+ it 'updates protected path settings' do
+ subject.execute
+
+ application_settings.reload
+
+ expect(application_settings.throttle_protected_paths_enabled).to be_truthy
+ expect(application_settings.throttle_protected_paths_period_in_seconds).to eq(600)
+ expect(application_settings.throttle_protected_paths_requests_per_period).to eq(100)
+ expect(application_settings.protected_paths).to eq(['/users/password', '/users/sign_in'])
+ end
+ end
end
diff --git a/spec/services/boards/issues/create_service_spec.rb b/spec/services/boards/issues/create_service_spec.rb
index 33637419f83..ef7b7fdbaac 100644
--- a/spec/services/boards/issues/create_service_spec.rb
+++ b/spec/services/boards/issues/create_service_spec.rb
@@ -10,7 +10,7 @@ describe Boards::Issues::CreateService do
let(:label) { create(:label, project: project, name: 'in-progress') }
let!(:list) { create(:list, board: board, label: label, position: 0) }
- subject(:service) { described_class.new(board.parent, project, user, board_id: board.id, list_id: list.id, title: 'New issue') }
+ subject(:service) { described_class.new(board.resource_parent, project, user, board_id: board.id, list_id: list.id, title: 'New issue') }
before do
project.add_developer(user)
diff --git a/spec/services/boards/lists/update_service_spec.rb b/spec/services/boards/lists/update_service_spec.rb
index a5411a2fb3a..243e0fc50ad 100644
--- a/spec/services/boards/lists/update_service_spec.rb
+++ b/spec/services/boards/lists/update_service_spec.rb
@@ -9,9 +9,9 @@ describe Boards::Lists::UpdateService do
shared_examples 'moving list' do
context 'when user can admin list' do
it 'calls Lists::MoveService to update list position' do
- board.parent.add_developer(user)
+ board.resource_parent.add_developer(user)
- expect(Boards::Lists::MoveService).to receive(:new).with(board.parent, user, params).and_call_original
+ expect(Boards::Lists::MoveService).to receive(:new).with(board.resource_parent, user, params).and_call_original
expect_any_instance_of(Boards::Lists::MoveService).to receive(:execute).with(list)
service.execute(list)
@@ -30,7 +30,7 @@ describe Boards::Lists::UpdateService do
shared_examples 'updating list preferences' do
context 'when user can read list' do
it 'updates list preference for user' do
- board.parent.add_guest(user)
+ board.resource_parent.add_guest(user)
service.execute(list)
@@ -48,7 +48,7 @@ describe Boards::Lists::UpdateService do
end
describe '#execute' do
- let(:service) { described_class.new(board.parent, user, params) }
+ let(:service) { described_class.new(board.resource_parent, user, params) }
context 'when position parameter is present' do
let(:params) { { position: 1 } }
diff --git a/spec/services/boards/visits/create_service_spec.rb b/spec/services/boards/visits/create_service_spec.rb
index 6baf7ac9deb..203c287f396 100644
--- a/spec/services/boards/visits/create_service_spec.rb
+++ b/spec/services/boards/visits/create_service_spec.rb
@@ -10,7 +10,7 @@ describe Boards::Visits::CreateService do
let(:project) { create(:project) }
let(:project_board) { create(:board, project: project) }
- subject(:service) { described_class.new(project_board.parent, user) }
+ subject(:service) { described_class.new(project_board.resource_parent, user) }
it 'returns nil when there is no user' do
service.current_user = nil
@@ -35,7 +35,7 @@ describe Boards::Visits::CreateService do
let(:group) { create(:group) }
let(:group_board) { create(:board, group: group) }
- subject(:service) { described_class.new(group_board.parent, user) }
+ subject(:service) { described_class.new(group_board.resource_parent, user) }
it 'returns nil when there is no user' do
service.current_user = nil
diff --git a/spec/services/bulk_push_event_payload_service_spec.rb b/spec/services/bulk_push_event_payload_service_spec.rb
new file mode 100644
index 00000000000..661c3540aa0
--- /dev/null
+++ b/spec/services/bulk_push_event_payload_service_spec.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe BulkPushEventPayloadService do
+ let(:event) { create(:push_event) }
+
+ let(:push_data) do
+ {
+ action: :created,
+ ref_count: 4,
+ ref_type: :branch
+ }
+ end
+
+ subject { described_class.new(event, push_data) }
+
+ it 'creates a PushEventPayload' do
+ push_event_payload = subject.execute
+
+ expect(push_event_payload).to be_persisted
+ expect(push_event_payload.action).to eq(push_data[:action].to_s)
+ expect(push_event_payload.commit_count).to eq(0)
+ expect(push_event_payload.ref_count).to eq(push_data[:ref_count])
+ expect(push_event_payload.ref_type).to eq(push_data[:ref_type].to_s)
+ end
+end
diff --git a/spec/services/ci/create_pipeline_service/rules_spec.rb b/spec/services/ci/create_pipeline_service/rules_spec.rb
new file mode 100644
index 00000000000..40a3b115cb5
--- /dev/null
+++ b/spec/services/ci/create_pipeline_service/rules_spec.rb
@@ -0,0 +1,94 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Ci::CreatePipelineService do
+ context 'rules' do
+ let(:user) { create(:admin) }
+ let(:ref) { 'refs/heads/master' }
+ let(:source) { :push }
+ let(:service) { described_class.new(project, user, { ref: ref }) }
+ let(:pipeline) { service.execute(source) }
+ let(:build_names) { pipeline.builds.pluck(:name) }
+
+ before do
+ stub_ci_pipeline_yaml_file(config)
+ allow_any_instance_of(Ci::BuildScheduleWorker).to receive(:perform).and_return(true)
+ end
+
+ context 'exists:' do
+ let(:config) do
+ <<-EOY
+ regular-job:
+ script: 'echo Hello, World!'
+
+ rules-job:
+ script: "echo hello world, $CI_COMMIT_REF_NAME"
+ rules:
+ - exists:
+ - README.md
+ when: manual
+ - exists:
+ - app.rb
+ when: on_success
+
+ delayed-job:
+ script: "echo See you later, World!"
+ rules:
+ - exists:
+ - README.md
+ when: delayed
+ start_in: 4 hours
+ EOY
+ end
+ let(:regular_job) { pipeline.builds.find_by(name: 'regular-job') }
+ let(:rules_job) { pipeline.builds.find_by(name: 'rules-job') }
+ let(:delayed_job) { pipeline.builds.find_by(name: 'delayed-job') }
+
+ context 'with matches' do
+ let(:project) { create(:project, :custom_repo, files: { 'README.md' => '' }) }
+
+ it 'creates two jobs' do
+ expect(pipeline).to be_persisted
+ expect(build_names).to contain_exactly('regular-job', 'rules-job', 'delayed-job')
+ end
+
+ it 'sets when: for all jobs' do
+ expect(regular_job.when).to eq('on_success')
+ expect(rules_job.when).to eq('manual')
+ expect(delayed_job.when).to eq('delayed')
+ expect(delayed_job.options[:start_in]).to eq('4 hours')
+ end
+ end
+
+ context 'with matches on the second rule' do
+ let(:project) { create(:project, :custom_repo, files: { 'app.rb' => '' }) }
+
+ it 'includes both jobs' do
+ expect(pipeline).to be_persisted
+ expect(build_names).to contain_exactly('regular-job', 'rules-job')
+ end
+
+ it 'sets when: for the created rules job based on the second clause' do
+ expect(regular_job.when).to eq('on_success')
+ expect(rules_job.when).to eq('on_success')
+ end
+ end
+
+ context 'without matches' do
+ let(:project) { create(:project, :custom_repo, files: { 'useless_script.rb' => '' }) }
+
+ it 'only persists the job without rules' do
+ expect(pipeline).to be_persisted
+ expect(regular_job).to be_persisted
+ expect(rules_job).to be_nil
+ expect(delayed_job).to be_nil
+ end
+
+ it 'sets when: for the created job' do
+ expect(regular_job.when).to eq('on_success')
+ end
+ end
+ end
+ end
+end
diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb
index 1a5fdac1c95..fd5f72c4c46 100644
--- a/spec/services/ci/create_pipeline_service_spec.rb
+++ b/spec/services/ci/create_pipeline_service_spec.rb
@@ -71,6 +71,7 @@ describe Ci::CreatePipelineService do
expect(Gitlab::Metrics).to receive(:counter)
.with(:pipelines_created_total, "Counter of pipelines created")
.and_call_original
+ allow(Gitlab::Metrics).to receive(:counter).and_call_original # allow other counters
pipeline
end
@@ -735,6 +736,28 @@ describe Ci::CreatePipelineService do
end
end
+ context 'when environment with duplicate names' do
+ let(:ci_yaml) do
+ {
+ deploy: { environment: { name: 'production' }, script: 'ls' },
+ deploy_2: { environment: { name: 'production' }, script: 'ls' }
+ }
+ end
+
+ before do
+ stub_ci_pipeline_yaml_file(YAML.dump(ci_yaml))
+ end
+
+ it 'creates a pipeline with the environment' do
+ result = execute_service
+
+ expect(result).to be_persisted
+ expect(Environment.find_by(name: 'production')).to be_present
+ expect(result.builds.first.deployment).to be_persisted
+ expect(result.builds.first.deployment.deployable).to be_a(Ci::Build)
+ end
+ end
+
context 'when builds with auto-retries are configured' do
let(:pipeline) { execute_service }
let(:rspec_job) { pipeline.builds.find_by(name: 'rspec') }
diff --git a/spec/services/ci/pipeline_trigger_service_spec.rb b/spec/services/ci/pipeline_trigger_service_spec.rb
index 76251b5b557..24d42f402f4 100644
--- a/spec/services/ci/pipeline_trigger_service_spec.rb
+++ b/spec/services/ci/pipeline_trigger_service_spec.rb
@@ -11,76 +11,158 @@ describe Ci::PipelineTriggerService do
describe '#execute' do
let(:user) { create(:user) }
- let(:trigger) { create(:ci_trigger, project: project, owner: user) }
let(:result) { described_class.new(project, user, params).execute }
before do
project.add_developer(user)
end
- context 'when trigger belongs to a different project' do
- let(:params) { { token: trigger.token, ref: 'master', variables: nil } }
- let(:trigger) { create(:ci_trigger, project: create(:project), owner: user) }
+ context 'with a trigger token' do
+ let(:trigger) { create(:ci_trigger, project: project, owner: user) }
- it 'does nothing' do
- expect { result }.not_to change { Ci::Pipeline.count }
- end
- end
-
- context 'when params have an existsed trigger token' do
- context 'when params have an existsed ref' do
+ context 'when trigger belongs to a different project' do
let(:params) { { token: trigger.token, ref: 'master', variables: nil } }
+ let(:trigger) { create(:ci_trigger, project: create(:project), owner: user) }
- it 'triggers a pipeline' do
- expect { result }.to change { Ci::Pipeline.count }.by(1)
- expect(result[:pipeline].ref).to eq('master')
- expect(result[:pipeline].project).to eq(project)
- expect(result[:pipeline].user).to eq(trigger.owner)
- expect(result[:pipeline].trigger_requests.to_a)
- .to eq(result[:pipeline].builds.map(&:trigger_request).uniq)
- expect(result[:status]).to eq(:success)
+ it 'does nothing' do
+ expect { result }.not_to change { Ci::Pipeline.count }
end
+ end
- context 'when commit message has [ci skip]' do
- before do
- allow_any_instance_of(Ci::Pipeline).to receive(:git_commit_message) { '[ci skip]' }
- end
+ context 'when params have an existsed trigger token' do
+ context 'when params have an existsed ref' do
+ let(:params) { { token: trigger.token, ref: 'master', variables: nil } }
- it 'ignores [ci skip] and create as general' do
+ it 'triggers a pipeline' do
expect { result }.to change { Ci::Pipeline.count }.by(1)
+ expect(result[:pipeline].ref).to eq('master')
+ expect(result[:pipeline].project).to eq(project)
+ expect(result[:pipeline].user).to eq(trigger.owner)
+ expect(result[:pipeline].trigger_requests.to_a)
+ .to eq(result[:pipeline].builds.map(&:trigger_request).uniq)
expect(result[:status]).to eq(:success)
end
+
+ context 'when commit message has [ci skip]' do
+ before do
+ allow_any_instance_of(Ci::Pipeline).to receive(:git_commit_message) { '[ci skip]' }
+ end
+
+ it 'ignores [ci skip] and create as general' do
+ expect { result }.to change { Ci::Pipeline.count }.by(1)
+ expect(result[:status]).to eq(:success)
+ end
+ end
+
+ context 'when params have a variable' do
+ let(:params) { { token: trigger.token, ref: 'master', variables: variables } }
+ let(:variables) { { 'AAA' => 'AAA123' } }
+
+ it 'has a variable' do
+ expect { result }.to change { Ci::PipelineVariable.count }.by(1)
+ .and change { Ci::TriggerRequest.count }.by(1)
+ expect(result[:pipeline].variables.map { |v| { v.key => v.value } }.first).to eq(variables)
+ expect(result[:pipeline].trigger_requests.last.variables).to be_nil
+ end
+ end
end
- context 'when params have a variable' do
- let(:params) { { token: trigger.token, ref: 'master', variables: variables } }
- let(:variables) { { 'AAA' => 'AAA123' } }
+ context 'when params have a non-existsed ref' do
+ let(:params) { { token: trigger.token, ref: 'invalid-ref', variables: nil } }
- it 'has a variable' do
- expect { result }.to change { Ci::PipelineVariable.count }.by(1)
- .and change { Ci::TriggerRequest.count }.by(1)
- expect(result[:pipeline].variables.map { |v| { v.key => v.value } }.first).to eq(variables)
- expect(result[:pipeline].trigger_requests.last.variables).to be_nil
+ it 'does not trigger a pipeline' do
+ expect { result }.not_to change { Ci::Pipeline.count }
+ expect(result[:http_status]).to eq(400)
end
end
end
- context 'when params have a non-existsed ref' do
- let(:params) { { token: trigger.token, ref: 'invalid-ref', variables: nil } }
+ context 'when params have a non-existsed trigger token' do
+ let(:params) { { token: 'invalid-token', ref: nil, variables: nil } }
it 'does not trigger a pipeline' do
expect { result }.not_to change { Ci::Pipeline.count }
- expect(result[:http_status]).to eq(400)
+ expect(result).to be_nil
end
end
end
- context 'when params have a non-existsed trigger token' do
- let(:params) { { token: 'invalid-token', ref: nil, variables: nil } }
+ context 'with a pipeline job token' do
+ let!(:pipeline) { create(:ci_empty_pipeline, project: project) }
+ let(:job) { create(:ci_build, :running, pipeline: pipeline, user: user) }
+
+ context 'when job user does not have a permission to read a project' do
+ let(:params) { { token: job.token, ref: 'master', variables: nil } }
+ let(:job) { create(:ci_build, pipeline: pipeline, user: create(:user)) }
+
+ it 'does nothing' do
+ expect { result }.not_to change { Ci::Pipeline.count }
+ end
+ end
+
+ context 'when job is not running' do
+ let(:params) { { token: job.token, ref: 'master', variables: nil } }
+ let(:job) { create(:ci_build, :success, pipeline: pipeline, user: user) }
+
+ it 'does nothing' do
+ expect { result }.not_to change { Ci::Pipeline.count }
+ expect(result[:message]).to eq('400 Job has to be running')
+ end
+ end
- it 'does not trigger a pipeline' do
- expect { result }.not_to change { Ci::Pipeline.count }
- expect(result).to be_nil
+ context 'when params have an existsed job token' do
+ context 'when params have an existsed ref' do
+ let(:params) { { token: job.token, ref: 'master', variables: nil } }
+
+ it 'triggers a pipeline' do
+ expect { result }.to change { Ci::Pipeline.count }.by(1)
+ expect(result[:pipeline].ref).to eq('master')
+ expect(result[:pipeline].project).to eq(project)
+ expect(result[:pipeline].user).to eq(job.user)
+ expect(result[:status]).to eq(:success)
+ end
+
+ context 'when commit message has [ci skip]' do
+ before do
+ allow_any_instance_of(Ci::Pipeline).to receive(:git_commit_message) { '[ci skip]' }
+ end
+
+ it 'ignores [ci skip] and create as general' do
+ expect { result }.to change { Ci::Pipeline.count }.by(1)
+ expect(result[:status]).to eq(:success)
+ end
+ end
+
+ context 'when params have a variable' do
+ let(:params) { { token: job.token, ref: 'master', variables: variables } }
+ let(:variables) { { 'AAA' => 'AAA123' } }
+
+ it 'has a variable' do
+ expect { result }.to change { Ci::PipelineVariable.count }.by(1)
+ .and change { Ci::Sources::Pipeline.count }.by(1)
+ expect(result[:pipeline].variables.map { |v| { v.key => v.value } }.first).to eq(variables)
+ expect(job.sourced_pipelines.last.pipeline_id).to eq(result[:pipeline].id)
+ end
+ end
+ end
+
+ context 'when params have a non-existsed ref' do
+ let(:params) { { token: job.token, ref: 'invalid-ref', variables: nil } }
+
+ it 'does not job a pipeline' do
+ expect { result }.not_to change { Ci::Pipeline.count }
+ expect(result[:http_status]).to eq(400)
+ end
+ end
+ end
+
+ context 'when params have a non-existsed trigger token' do
+ let(:params) { { token: 'invalid-token', ref: nil, variables: nil } }
+
+ it 'does not trigger a pipeline' do
+ expect { result }.not_to change { Ci::Pipeline.count }
+ expect(result).to be_nil
+ end
end
end
end
diff --git a/spec/services/ci/process_pipeline_service_spec.rb b/spec/services/ci/process_pipeline_service_spec.rb
index 1b28d2d4d02..05adec8b745 100644
--- a/spec/services/ci/process_pipeline_service_spec.rb
+++ b/spec/services/ci/process_pipeline_service_spec.rb
@@ -422,6 +422,18 @@ describe Ci::ProcessPipelineService, '#execute' do
end
end
+ context 'when an exception is raised during a persistent ref creation' do
+ before do
+ successful_build('test', stage_idx: 0)
+
+ allow_any_instance_of(Ci::PersistentRef).to receive(:delete_refs) { raise ArgumentError }
+ end
+
+ it 'process the pipeline' do
+ expect { process_pipeline }.not_to raise_error
+ end
+ end
+
context 'when there are manual action in earlier stages' do
context 'when first stage has only optional manual actions' do
before do
@@ -907,6 +919,10 @@ describe Ci::ProcessPipelineService, '#execute' do
create(:ci_build, :created, pipeline: pipeline, name: name, **opts)
end
+ def successful_build(name, **opts)
+ create(:ci_build, :success, pipeline: pipeline, name: name, **opts)
+ end
+
def delayed_options
{ when: 'delayed', options: { script: %w(echo), start_in: '1 minute' } }
end
diff --git a/spec/services/ci/register_job_service_spec.rb b/spec/services/ci/register_job_service_spec.rb
index 874945c8585..2f2c525ccc4 100644
--- a/spec/services/ci/register_job_service_spec.rb
+++ b/spec/services/ci/register_job_service_spec.rb
@@ -501,6 +501,22 @@ module Ci
expect(pending_job).to be_archived_failure
end
end
+
+ context 'when an exception is raised during a persistent ref creation' do
+ before do
+ allow_any_instance_of(Ci::PersistentRef).to receive(:exist?) { false }
+ allow_any_instance_of(Ci::PersistentRef).to receive(:create_ref) { raise ArgumentError }
+ end
+
+ subject { execute(specific_runner, {}) }
+
+ it 'picks the build' do
+ expect(subject).to eq(pending_job)
+
+ pending_job.reload
+ expect(pending_job).to be_running
+ end
+ end
end
describe '#register_success' do
diff --git a/spec/services/ci/retry_build_service_spec.rb b/spec/services/ci/retry_build_service_spec.rb
index 281c7438eee..b1368f7776b 100644
--- a/spec/services/ci/retry_build_service_spec.rb
+++ b/spec/services/ci/retry_build_service_spec.rb
@@ -204,6 +204,16 @@ describe Ci::RetryBuildService do
expect(build).to be_retried
expect(build.reload).to be_retried
end
+
+ context 'when build with deployment is retried' do
+ let!(:build) do
+ create(:ci_build, :with_deployment, :deploy_to_production, pipeline: pipeline, stage_id: stage.id)
+ end
+
+ it 'creates a new deployment' do
+ expect { new_build }.to change { Deployment.count }.by(1)
+ end
+ end
end
context 'when user does not have ability to execute build' do
diff --git a/spec/services/ci/stop_environments_service_spec.rb b/spec/services/ci/stop_environments_service_spec.rb
index 890fa5bc009..ed92625a2cc 100644
--- a/spec/services/ci/stop_environments_service_spec.rb
+++ b/spec/services/ci/stop_environments_service_spec.rb
@@ -121,8 +121,8 @@ describe Ci::StopEnvironmentsService do
merge_requests_as_head_pipeline: [merge_request])
end
- let!(:review_job) { create(:ci_build, :start_review_app, pipeline: pipeline, project: project) }
- let!(:stop_review_job) { create(:ci_build, :stop_review_app, :manual, pipeline: pipeline, project: project) }
+ let!(:review_job) { create(:ci_build, :with_deployment, :start_review_app, pipeline: pipeline, project: project) }
+ let!(:stop_review_job) { create(:ci_build, :with_deployment, :stop_review_app, :manual, pipeline: pipeline, project: project) }
before do
review_job.deployment.success!
diff --git a/spec/services/clusters/gcp/finalize_creation_service_spec.rb b/spec/services/clusters/gcp/finalize_creation_service_spec.rb
index 5f91acb8e84..43dbea959a2 100644
--- a/spec/services/clusters/gcp/finalize_creation_service_spec.rb
+++ b/spec/services/clusters/gcp/finalize_creation_service_spec.rb
@@ -107,6 +107,9 @@ describe Clusters::Gcp::FinalizeCreationService, '#execute' do
namespace: 'default'
}
)
+
+ stub_kubeclient_get_cluster_role_binding_error(api_url, 'gitlab-admin')
+ stub_kubeclient_create_cluster_role_binding(api_url)
end
end
@@ -133,9 +136,6 @@ describe Clusters::Gcp::FinalizeCreationService, '#execute' do
context 'With an RBAC cluster' do
before do
provider.legacy_abac = false
-
- stub_kubeclient_get_cluster_role_binding_error(api_url, 'gitlab-admin')
- stub_kubeclient_create_cluster_role_binding(api_url)
end
include_context 'kubernetes information successfully fetched'
@@ -152,4 +152,22 @@ describe Clusters::Gcp::FinalizeCreationService, '#execute' do
it_behaves_like 'kubernetes information not successfully fetched'
end
+
+ context 'With a Cloud Run cluster' do
+ before do
+ provider.cloud_run = true
+ end
+
+ include_context 'kubernetes information successfully fetched'
+
+ it_behaves_like 'success'
+
+ it 'has knative pre-installed' do
+ subject
+ cluster.reload
+
+ expect(cluster.application_knative).to be_present
+ expect(cluster.application_knative).to be_pre_installed
+ end
+ end
end
diff --git a/spec/services/create_snippet_service_spec.rb b/spec/services/create_snippet_service_spec.rb
index 7d2491b3a49..1751029a78c 100644
--- a/spec/services/create_snippet_service_spec.rb
+++ b/spec/services/create_snippet_service_spec.rb
@@ -3,26 +3,28 @@
require 'spec_helper'
describe CreateSnippetService do
- before do
- @user = create :user
- @admin = create :user, admin: true
- @opts = {
+ let(:user) { create(:user) }
+ let(:admin) { create(:user, :admin) }
+ let(:opts) { base_opts.merge(extra_opts) }
+ let(:base_opts) do
+ {
title: 'Test snippet',
file_name: 'snippet.rb',
content: 'puts "hello world"',
visibility_level: Gitlab::VisibilityLevel::PRIVATE
}
end
+ let(:extra_opts) { {} }
context 'When public visibility is restricted' do
+ let(:extra_opts) { { visibility_level: Gitlab::VisibilityLevel::PUBLIC } }
+
before do
stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC])
-
- @opts.merge!(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
end
it 'non-admins are not able to create a public snippet' do
- snippet = create_snippet(nil, @user, @opts)
+ snippet = create_snippet(nil, user, opts)
expect(snippet.errors.messages).to have_key(:visibility_level)
expect(snippet.errors.messages[:visibility_level].first).to(
match('has been restricted')
@@ -30,37 +32,81 @@ describe CreateSnippetService do
end
it 'admins are able to create a public snippet' do
- snippet = create_snippet(nil, @admin, @opts)
+ snippet = create_snippet(nil, admin, opts)
expect(snippet.errors.any?).to be_falsey
expect(snippet.visibility_level).to eq(Gitlab::VisibilityLevel::PUBLIC)
end
describe "when visibility level is passed as a string" do
+ let(:extra_opts) { { visibility: 'internal' } }
+
before do
- @opts[:visibility] = 'internal'
- @opts.delete(:visibility_level)
+ base_opts.delete(:visibility_level)
end
it "assigns the correct visibility level" do
- snippet = create_snippet(nil, @user, @opts)
+ snippet = create_snippet(nil, user, opts)
expect(snippet.errors.any?).to be_falsey
expect(snippet.visibility_level).to eq(Gitlab::VisibilityLevel::INTERNAL)
end
end
end
+ context 'checking spam' do
+ shared_examples 'marked as spam' do
+ let(:snippet) { create_snippet(nil, admin, opts) }
+
+ it 'marks a snippet as a spam ' do
+ expect(snippet).to be_spam
+ end
+
+ it 'invalidates the snippet' do
+ expect(snippet).to be_invalid
+ end
+
+ it 'creates a new spam_log' do
+ expect { snippet }
+ .to log_spam(title: snippet.title, noteable_type: 'PersonalSnippet')
+ end
+
+ it 'assigns a spam_log to an issue' do
+ expect(snippet.spam_log).to eq(SpamLog.last)
+ end
+ end
+
+ let(:extra_opts) do
+ { visibility_level: Gitlab::VisibilityLevel::PUBLIC, request: double(:request, env: {}) }
+ end
+
+ before do
+ expect_next_instance_of(AkismetService) do |akismet_service|
+ expect(akismet_service).to receive_messages(spam?: true)
+ end
+ end
+
+ [true, false, nil].each do |allow_possible_spam|
+ context "when recaptcha_disabled flag is #{allow_possible_spam.inspect}" do
+ before do
+ stub_feature_flags(allow_possible_spam: allow_possible_spam) unless allow_possible_spam.nil?
+ end
+
+ it_behaves_like 'marked as spam'
+ end
+ end
+ end
+
describe 'usage counter' do
let(:counter) { Gitlab::UsageDataCounters::SnippetCounter }
it 'increments count' do
expect do
- create_snippet(nil, @admin, @opts)
+ create_snippet(nil, admin, opts)
end.to change { counter.read(:create) }.by 1
end
it 'does not increment count if create fails' do
expect do
- create_snippet(nil, @admin, {})
+ create_snippet(nil, admin, {})
end.not_to change { counter.read(:create) }
end
end
diff --git a/spec/services/update_deployment_service_spec.rb b/spec/services/deployments/after_create_service_spec.rb
index 7dc52f6816a..b34483ea85b 100644
--- a/spec/services/update_deployment_service_spec.rb
+++ b/spec/services/deployments/after_create_service_spec.rb
@@ -2,13 +2,14 @@
require 'spec_helper'
-describe UpdateDeploymentService do
+describe Deployments::AfterCreateService do
let(:user) { create(:user) }
let(:project) { create(:project, :repository) }
let(:options) { { name: 'production' } }
let(:job) do
create(:ci_build,
+ :with_deployment,
ref: 'master',
tag: false,
environment: 'production',
@@ -114,6 +115,7 @@ describe UpdateDeploymentService do
context 'when yaml environment uses $CI_COMMIT_REF_NAME' do
let(:job) do
create(:ci_build,
+ :with_deployment,
ref: 'master',
environment: 'production',
project: project,
@@ -126,6 +128,7 @@ describe UpdateDeploymentService do
context 'when yaml environment uses $CI_ENVIRONMENT_SLUG' do
let(:job) do
create(:ci_build,
+ :with_deployment,
ref: 'master',
environment: 'prod-slug',
project: project,
@@ -138,6 +141,7 @@ describe UpdateDeploymentService do
context 'when yaml environment uses yaml_variables containing symbol keys' do
let(:job) do
create(:ci_build,
+ :with_deployment,
yaml_variables: [{ key: :APP_HOST, value: 'host' }],
environment: 'production',
project: project,
@@ -148,7 +152,7 @@ describe UpdateDeploymentService do
end
context 'when yaml environment does not have url' do
- let(:job) { create(:ci_build, environment: 'staging', project: project) }
+ let(:job) { create(:ci_build, :with_deployment, environment: 'staging', project: project) }
it 'returns the external_url from persisted environment' do
is_expected.to be_nil
@@ -174,6 +178,7 @@ describe UpdateDeploymentService do
context 'when job deploys to staging' do
let(:job) do
create(:ci_build,
+ :with_deployment,
ref: 'master',
tag: false,
environment: 'staging',
diff --git a/spec/services/deployments/create_service_spec.rb b/spec/services/deployments/create_service_spec.rb
new file mode 100644
index 00000000000..e41c8259ea9
--- /dev/null
+++ b/spec/services/deployments/create_service_spec.rb
@@ -0,0 +1,92 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Deployments::CreateService do
+ let(:environment) do
+ double(
+ :environment,
+ deployment_platform: double(:platform, cluster_id: 1),
+ project_id: 2,
+ id: 3
+ )
+ end
+
+ let(:user) { double(:user) }
+
+ describe '#execute' do
+ let(:service) { described_class.new(environment, user, {}) }
+
+ it 'does not run the AfterCreateService service if the deployment is not persisted' do
+ deploy = double(:deployment, persisted?: false)
+
+ expect(service)
+ .to receive(:create_deployment)
+ .and_return(deploy)
+
+ expect(Deployments::AfterCreateService)
+ .not_to receive(:new)
+
+ expect(service.execute).to eq(deploy)
+ end
+
+ it 'runs the AfterCreateService service if the deployment is persisted' do
+ deploy = double(:deployment, persisted?: true)
+ after_service = double(:after_create_service)
+
+ expect(service)
+ .to receive(:create_deployment)
+ .and_return(deploy)
+
+ expect(Deployments::AfterCreateService)
+ .to receive(:new)
+ .with(deploy)
+ .and_return(after_service)
+
+ expect(after_service)
+ .to receive(:execute)
+
+ expect(service.execute).to eq(deploy)
+ end
+ end
+
+ describe '#create_deployment' do
+ it 'creates a deployment' do
+ environment = build(:environment)
+ service = described_class.new(environment, user, {})
+
+ expect(environment.deployments)
+ .to receive(:create)
+ .with(an_instance_of(Hash))
+
+ service.create_deployment
+ end
+ end
+
+ describe '#deployment_attributes' do
+ it 'only includes attributes that we want to persist' do
+ service = described_class.new(
+ environment,
+ user,
+ ref: 'master',
+ tag: true,
+ sha: '123',
+ foo: 'bar',
+ on_stop: 'stop',
+ status: 'running'
+ )
+
+ expect(service.deployment_attributes).to eq(
+ cluster_id: 1,
+ project_id: 2,
+ environment_id: 3,
+ ref: 'master',
+ tag: true,
+ sha: '123',
+ user: user,
+ on_stop: 'stop',
+ status: 'running'
+ )
+ end
+ end
+end
diff --git a/spec/services/deployments/update_service_spec.rb b/spec/services/deployments/update_service_spec.rb
new file mode 100644
index 00000000000..a923099b82c
--- /dev/null
+++ b/spec/services/deployments/update_service_spec.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Deployments::UpdateService do
+ let(:deploy) { create(:deployment, :running) }
+ let(:service) { described_class.new(deploy, status: 'success') }
+
+ describe '#execute' do
+ it 'updates the status of a deployment' do
+ expect(service.execute).to eq(true)
+ expect(deploy.status).to eq('success')
+ end
+ end
+end
diff --git a/spec/services/event_create_service_spec.rb b/spec/services/event_create_service_spec.rb
index 9f2c3fec62c..eb738ac80b1 100644
--- a/spec/services/event_create_service_spec.rb
+++ b/spec/services/event_create_service_spec.rb
@@ -113,40 +113,21 @@ describe EventCreateService do
end
end
- describe '#push', :clean_gitlab_redis_shared_state do
- let(:project) { create(:project) }
- let(:user) { create(:user) }
-
- let(:push_data) do
- {
- commits: [
- {
- id: '1cf19a015df3523caf0a1f9d40c98a267d6a2fc2',
- message: 'This is a commit'
- }
- ],
- before: '0000000000000000000000000000000000000000',
- after: '1cf19a015df3523caf0a1f9d40c98a267d6a2fc2',
- total_commits_count: 1,
- ref: 'refs/heads/my-branch'
- }
- end
-
+ shared_examples_for 'service for creating a push event' do |service_class|
it 'creates a new event' do
- expect { service.push(project, user, push_data) }.to change { Event.count }
+ expect { subject }.to change { Event.count }
end
it 'creates the push event payload' do
- expect(PushEventPayloadService).to receive(:new)
+ expect(service_class).to receive(:new)
.with(an_instance_of(PushEvent), push_data)
.and_call_original
- service.push(project, user, push_data)
+ subject
end
it 'updates user last activity' do
- expect { service.push(project, user, push_data) }
- .to change { user.last_activity_on }.to(Date.today)
+ expect { subject }.to change { user.last_activity_on }.to(Date.today)
end
it 'caches the last push event for the user' do
@@ -154,7 +135,7 @@ describe EventCreateService do
.to receive(:cache_last_push_event)
.with(an_instance_of(PushEvent))
- service.push(project, user, push_data)
+ subject
end
it 'does not create any event data when an error is raised' do
@@ -163,17 +144,56 @@ describe EventCreateService do
allow(payload_service).to receive(:execute)
.and_raise(RuntimeError)
- allow(PushEventPayloadService).to receive(:new)
+ allow(service_class).to receive(:new)
.and_return(payload_service)
- expect { service.push(project, user, push_data) }
- .to raise_error(RuntimeError)
-
+ expect { subject }.to raise_error(RuntimeError)
expect(Event.count).to eq(0)
expect(PushEventPayload.count).to eq(0)
end
end
+ describe '#push', :clean_gitlab_redis_shared_state do
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+
+ let(:push_data) do
+ {
+ commits: [
+ {
+ id: '1cf19a015df3523caf0a1f9d40c98a267d6a2fc2',
+ message: 'This is a commit'
+ }
+ ],
+ before: '0000000000000000000000000000000000000000',
+ after: '1cf19a015df3523caf0a1f9d40c98a267d6a2fc2',
+ total_commits_count: 1,
+ ref: 'refs/heads/my-branch'
+ }
+ end
+
+ subject { service.push(project, user, push_data) }
+
+ it_behaves_like 'service for creating a push event', PushEventPayloadService
+ end
+
+ describe '#bulk_push', :clean_gitlab_redis_shared_state do
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+
+ let(:push_data) do
+ {
+ action: :created,
+ ref_count: 4,
+ ref_type: :branch
+ }
+ end
+
+ subject { service.bulk_push(project, user, push_data) }
+
+ it_behaves_like 'service for creating a push event', BulkPushEventPayloadService
+ end
+
describe 'Project' do
let(:user) { create :user }
let(:project) { create(:project) }
diff --git a/spec/services/git/base_hooks_service_spec.rb b/spec/services/git/base_hooks_service_spec.rb
index 874df9a68cd..f3f6b36a18d 100644
--- a/spec/services/git/base_hooks_service_spec.rb
+++ b/spec/services/git/base_hooks_service_spec.rb
@@ -8,14 +8,12 @@ describe Git::BaseHooksService do
let(:user) { create(:user) }
let(:project) { create(:project, :repository) }
- let(:service) { described_class.new(project, user, oldrev: oldrev, newrev: newrev, ref: ref) }
-
let(:oldrev) { Gitlab::Git::BLANK_SHA }
let(:newrev) { "8a2a6eb295bb170b34c24c76c49ed0e9b2eaf34b" } # gitlab-test: git rev-parse refs/tags/v1.1.0
let(:ref) { 'refs/tags/v1.1.0' }
- describe '#execute_project_hooks' do
- class TestService < described_class
+ let(:test_service) do
+ Class.new(described_class) do
def hook_name
:push_hooks
end
@@ -24,12 +22,44 @@ describe Git::BaseHooksService do
[]
end
end
+ end
- let(:project) { create(:project, :repository) }
+ subject { test_service.new(project, user, params) }
+
+ let(:params) do
+ {
+ change: {
+ oldrev: oldrev,
+ newrev: newrev,
+ ref: ref
+ }
+ }
+ end
- subject { TestService.new(project, user, oldrev: oldrev, newrev: newrev, ref: ref) }
+ describe 'push event' do
+ it 'creates push event' do
+ expect_next_instance_of(EventCreateService) do |service|
+ expect(service).to receive(:push)
+ end
+
+ subject.execute
+ end
- context '#execute_hooks' do
+ context 'create_push_event is set to false' do
+ before do
+ params[:create_push_event] = false
+ end
+
+ it 'does not create push event' do
+ expect(EventCreateService).not_to receive(:new)
+
+ subject.execute
+ end
+ end
+ end
+
+ describe 'project hooks and services' do
+ context 'hooks' do
before do
expect(project).to receive(:has_active_hooks?).and_return(active)
end
@@ -57,7 +87,7 @@ describe Git::BaseHooksService do
end
end
- context '#execute_services' do
+ context 'services' do
before do
expect(project).to receive(:has_active_services?).and_return(active)
end
@@ -84,78 +114,20 @@ describe Git::BaseHooksService do
end
end
end
- end
-
- describe 'with remote mirrors' do
- class TestService < described_class
- def commits
- []
- end
- end
-
- let(:project) { create(:project, :repository, :remote_mirror) }
-
- subject { TestService.new(project, user, oldrev: oldrev, newrev: newrev, ref: ref) }
-
- before do
- expect(subject).to receive(:execute_project_hooks)
- end
-
- context 'when remote mirror feature is enabled' do
- it 'fails stuck remote mirrors' do
- allow(project).to receive(:update_remote_mirrors).and_return(project.remote_mirrors)
- expect(project).to receive(:mark_stuck_remote_mirrors_as_failed!)
-
- subject.execute
- end
-
- it 'updates remote mirrors' do
- expect(project).to receive(:update_remote_mirrors)
-
- subject.execute
- end
- end
- context 'when remote mirror feature is disabled' do
+ context 'execute_project_hooks param set to false' do
before do
- stub_application_setting(mirror_available: false)
- end
-
- context 'with remote mirrors global setting overridden' do
- before do
- project.remote_mirror_available_overridden = true
- end
+ params[:execute_project_hooks] = false
- it 'fails stuck remote mirrors' do
- allow(project).to receive(:update_remote_mirrors).and_return(project.remote_mirrors)
- expect(project).to receive(:mark_stuck_remote_mirrors_as_failed!)
-
- subject.execute
- end
-
- it 'updates remote mirrors' do
- expect(project).to receive(:update_remote_mirrors)
-
- subject.execute
- end
+ allow(project).to receive(:has_active_hooks?).and_return(true)
+ allow(project).to receive(:has_active_services?).and_return(true)
end
- context 'without remote mirrors global setting overridden' do
- before do
- project.remote_mirror_available_overridden = false
- end
-
- it 'does not fails stuck remote mirrors' do
- expect(project).not_to receive(:mark_stuck_remote_mirrors_as_failed!)
+ it 'does not execute hooks and services' do
+ expect(project).not_to receive(:execute_hooks)
+ expect(project).not_to receive(:execute_services)
- subject.execute
- end
-
- it 'does not updates remote mirrors' do
- expect(project).not_to receive(:update_remote_mirrors)
-
- subject.execute
- end
+ subject.execute
end
end
end
diff --git a/spec/services/git/branch_hooks_service_spec.rb b/spec/services/git/branch_hooks_service_spec.rb
index 2bf7dc32436..085b49f31ab 100644
--- a/spec/services/git/branch_hooks_service_spec.rb
+++ b/spec/services/git/branch_hooks_service_spec.rb
@@ -16,13 +16,7 @@ describe Git::BranchHooksService do
let(:newrev) { commit.id }
let(:service) do
- described_class.new(project, user, oldrev: oldrev, newrev: newrev, ref: ref)
- end
-
- it 'update remote mirrors' do
- expect(service).to receive(:update_remote_mirrors).and_call_original
-
- service.execute
+ described_class.new(project, user, change: { oldrev: oldrev, newrev: newrev, ref: ref })
end
describe "Git Push Data" do
@@ -356,7 +350,7 @@ describe Git::BranchHooksService do
let(:forked_project) { fork_project(upstream_project, user, repository: true) }
let!(:forked_service) do
- described_class.new(forked_project, user, oldrev: oldrev, newrev: newrev, ref: ref)
+ described_class.new(forked_project, user, change: { oldrev: oldrev, newrev: newrev, ref: ref })
end
context 'when commits already exists in the upstream project' do
diff --git a/spec/services/git/branch_push_service_spec.rb b/spec/services/git/branch_push_service_spec.rb
index f4d1a1e34cd..bf68eb0af20 100644
--- a/spec/services/git/branch_push_service_spec.rb
+++ b/spec/services/git/branch_push_service_spec.rb
@@ -19,7 +19,7 @@ describe Git::BranchPushService, services: true do
describe 'Push branches' do
subject do
- execute_service(project, user, oldrev, newrev, ref)
+ execute_service(project, user, oldrev: oldrev, newrev: newrev, ref: ref)
end
context 'new branch' do
@@ -70,7 +70,7 @@ describe Git::BranchPushService, services: true do
end
describe "Pipelines" do
- subject { execute_service(project, user, oldrev, newrev, ref) }
+ subject { execute_service(project, user, oldrev: oldrev, newrev: newrev, ref: ref) }
before do
stub_ci_pipeline_to_return_yaml_file
@@ -121,7 +121,7 @@ describe Git::BranchPushService, services: true do
.to receive(:perform_async)
.with(project.id, user.id, blankrev, 'newrev', ref)
- execute_service(project, user, blankrev, 'newrev', ref )
+ execute_service(project, user, oldrev: blankrev, newrev: 'newrev', ref: ref)
end
end
@@ -130,13 +130,13 @@ describe Git::BranchPushService, services: true do
it "calls the copy attributes method for the first push to the default branch" do
expect(project.repository).to receive(:copy_gitattributes).with('master')
- execute_service(project, user, blankrev, 'newrev', ref)
+ execute_service(project, user, oldrev: blankrev, newrev: 'newrev', ref: ref)
end
it "calls the copy attributes method for changes to the default branch" do
expect(project.repository).to receive(:copy_gitattributes).with(ref)
- execute_service(project, user, 'oldrev', 'newrev', ref)
+ execute_service(project, user, oldrev: 'oldrev', newrev: 'newrev', ref: ref)
end
end
@@ -149,7 +149,7 @@ describe Git::BranchPushService, services: true do
it "does not call copy attributes method" do
expect(project.repository).not_to receive(:copy_gitattributes)
- execute_service(project, user, oldrev, newrev, ref)
+ execute_service(project, user, oldrev: oldrev, newrev: newrev, ref: ref)
end
end
end
@@ -163,7 +163,7 @@ describe Git::BranchPushService, services: true do
it "when pushing a branch for the first time" do
expect(project).to receive(:execute_hooks)
expect(project.default_branch).to eq("master")
- execute_service(project, user, blankrev, 'newrev', ref)
+ execute_service(project, user, oldrev: blankrev, newrev: 'newrev', ref: ref)
expect(project.protected_branches).not_to be_empty
expect(project.protected_branches.first.push_access_levels.map(&:access_level)).to eq([Gitlab::Access::MAINTAINER])
expect(project.protected_branches.first.merge_access_levels.map(&:access_level)).to eq([Gitlab::Access::MAINTAINER])
@@ -174,7 +174,7 @@ describe Git::BranchPushService, services: true do
expect(project).to receive(:execute_hooks)
expect(project.default_branch).to eq("master")
- execute_service(project, user, blankrev, 'newrev', ref)
+ execute_service(project, user, oldrev: blankrev, newrev: 'newrev', ref: ref)
expect(project.protected_branches).to be_empty
end
@@ -184,7 +184,7 @@ describe Git::BranchPushService, services: true do
expect(project).to receive(:execute_hooks)
expect(project.default_branch).to eq("master")
- execute_service(project, user, blankrev, 'newrev', ref)
+ execute_service(project, user, oldrev: blankrev, newrev: 'newrev', ref: ref)
expect(project.protected_branches).not_to be_empty
expect(project.protected_branches.last.push_access_levels.map(&:access_level)).to eq([Gitlab::Access::DEVELOPER])
@@ -199,7 +199,7 @@ describe Git::BranchPushService, services: true do
expect(project.default_branch).to eq("master")
expect(ProtectedBranches::CreateService).not_to receive(:new)
- execute_service(project, user, blankrev, 'newrev', ref)
+ execute_service(project, user, oldrev: blankrev, newrev: 'newrev', ref: ref)
expect(project.protected_branches).not_to be_empty
expect(project.protected_branches.last.push_access_levels.map(&:access_level)).to eq([Gitlab::Access::NO_ACCESS])
@@ -211,7 +211,7 @@ describe Git::BranchPushService, services: true do
expect(project).to receive(:execute_hooks)
expect(project.default_branch).to eq("master")
- execute_service(project, user, blankrev, 'newrev', ref)
+ execute_service(project, user, oldrev: blankrev, newrev: 'newrev', ref: ref)
expect(project.protected_branches).not_to be_empty
expect(project.protected_branches.first.push_access_levels.map(&:access_level)).to eq([Gitlab::Access::MAINTAINER])
expect(project.protected_branches.first.merge_access_levels.map(&:access_level)).to eq([Gitlab::Access::DEVELOPER])
@@ -219,7 +219,7 @@ describe Git::BranchPushService, services: true do
it "when pushing new commits to existing branch" do
expect(project).to receive(:execute_hooks)
- execute_service(project, user, 'oldrev', 'newrev', ref)
+ execute_service(project, user, oldrev: 'oldrev', newrev: 'newrev', ref: ref)
end
end
end
@@ -249,7 +249,7 @@ describe Git::BranchPushService, services: true do
it "creates a note if a pushed commit mentions an issue" do
expect(SystemNoteService).to receive(:cross_reference).with(issue, commit, commit_author)
- execute_service(project, user, oldrev, newrev, ref)
+ execute_service(project, user, oldrev: oldrev, newrev: newrev, ref: ref)
end
it "only creates a cross-reference note if one doesn't already exist" do
@@ -257,7 +257,7 @@ describe Git::BranchPushService, services: true do
expect(SystemNoteService).not_to receive(:cross_reference).with(issue, commit, commit_author)
- execute_service(project, user, oldrev, newrev, ref)
+ execute_service(project, user, oldrev: oldrev, newrev: newrev, ref: ref)
end
it "defaults to the pushing user if the commit's author is not known" do
@@ -267,7 +267,7 @@ describe Git::BranchPushService, services: true do
)
expect(SystemNoteService).to receive(:cross_reference).with(issue, commit, user)
- execute_service(project, user, oldrev, newrev, ref)
+ execute_service(project, user, oldrev: oldrev, newrev: newrev, ref: ref)
end
it "finds references in the first push to a non-default branch" do
@@ -276,7 +276,7 @@ describe Git::BranchPushService, services: true do
expect(SystemNoteService).to receive(:cross_reference).with(issue, commit, commit_author)
- execute_service(project, user, blankrev, newrev, 'refs/heads/other')
+ execute_service(project, user, oldrev: blankrev, newrev: newrev, ref: 'refs/heads/other')
end
end
@@ -306,14 +306,14 @@ describe Git::BranchPushService, services: true do
context "while saving the 'first_mentioned_in_commit_at' metric for an issue" do
it 'sets the metric for referenced issues' do
- execute_service(project, user, oldrev, newrev, ref)
+ execute_service(project, user, oldrev: oldrev, newrev: newrev, ref: ref)
expect(issue.reload.metrics.first_mentioned_in_commit_at).to be_like_time(commit_time)
end
it 'does not set the metric for non-referenced issues' do
non_referenced_issue = create(:issue, project: project)
- execute_service(project, user, oldrev, newrev, ref)
+ execute_service(project, user, oldrev: oldrev, newrev: newrev, ref: ref)
expect(non_referenced_issue.reload.metrics.first_mentioned_in_commit_at).to be_nil
end
@@ -345,18 +345,18 @@ describe Git::BranchPushService, services: true do
context "to default branches" do
it "closes issues" do
- execute_service(project, commit_author, oldrev, newrev, ref)
+ execute_service(project, commit_author, oldrev: oldrev, newrev: newrev, ref: ref)
expect(Issue.find(issue.id)).to be_closed
end
it "adds a note indicating that the issue is now closed" do
expect(SystemNoteService).to receive(:change_status).with(issue, project, commit_author, "closed", closing_commit)
- execute_service(project, commit_author, oldrev, newrev, ref)
+ execute_service(project, commit_author, oldrev: oldrev, newrev: newrev, ref: ref)
end
it "doesn't create additional cross-reference notes" do
expect(SystemNoteService).not_to receive(:cross_reference)
- execute_service(project, commit_author, oldrev, newrev, ref)
+ execute_service(project, commit_author, oldrev: oldrev, newrev: newrev, ref: ref)
end
end
@@ -368,11 +368,11 @@ describe Git::BranchPushService, services: true do
it "creates cross-reference notes" do
expect(SystemNoteService).to receive(:cross_reference).with(issue, closing_commit, commit_author)
- execute_service(project, user, oldrev, newrev, ref)
+ execute_service(project, user, oldrev: oldrev, newrev: newrev, ref: ref)
end
it "doesn't close issues" do
- execute_service(project, user, oldrev, newrev, ref)
+ execute_service(project, user, oldrev: oldrev, newrev: newrev, ref: ref)
expect(Issue.find(issue.id)).to be_opened
end
end
@@ -408,7 +408,7 @@ describe Git::BranchPushService, services: true do
let(:message) { "this is some work.\n\nrelated to JIRA-1" }
it "initiates one api call to jira server to mention the issue" do
- execute_service(project, user, oldrev, newrev, ref)
+ execute_service(project, user, oldrev: oldrev, newrev: newrev, ref: ref)
expect(WebMock).to have_requested(:post, jira_api_comment_url('JIRA-1')).with(
body: /mentioned this issue in/
@@ -436,13 +436,13 @@ describe Git::BranchPushService, services: true do
context "using right markdown" do
it "initiates one api call to jira server to close the issue" do
- execute_service(project, commit_author, oldrev, newrev, ref)
+ execute_service(project, commit_author, oldrev: oldrev, newrev: newrev, ref: ref)
expect(WebMock).to have_requested(:post, jira_api_transition_url('JIRA-1')).once
end
it "initiates one api call to jira server to comment on the issue" do
- execute_service(project, commit_author, oldrev, newrev, ref)
+ execute_service(project, commit_author, oldrev: oldrev, newrev: newrev, ref: ref)
expect(WebMock).to have_requested(:post, jira_api_comment_url('JIRA-1')).with(
body: comment_body
@@ -459,13 +459,13 @@ describe Git::BranchPushService, services: true do
let(:message) { "this is some work.\n\ncloses #1" }
it "does not initiates one api call to jira server to close the issue" do
- execute_service(project, commit_author, oldrev, newrev, ref)
+ execute_service(project, commit_author, oldrev: oldrev, newrev: newrev, ref: ref)
expect(WebMock).not_to have_requested(:post, jira_api_transition_url('JIRA-1'))
end
it "does not initiates one api call to jira server to comment on the issue" do
- execute_service(project, commit_author, oldrev, newrev, ref)
+ execute_service(project, commit_author, oldrev: oldrev, newrev: newrev, ref: ref)
expect(WebMock).not_to have_requested(:post, jira_api_comment_url('JIRA-1')).with(
body: comment_body
@@ -478,13 +478,13 @@ describe Git::BranchPushService, services: true do
let(:message) { "this is some work.\n\ncloses JIRA-1 \n\n closes #{issue.to_reference}" }
it "initiates one api call to jira server to close the jira issue" do
- execute_service(project, commit_author, oldrev, newrev, ref)
+ execute_service(project, commit_author, oldrev: oldrev, newrev: newrev, ref: ref)
expect(WebMock).to have_requested(:post, jira_api_transition_url('JIRA-1')).once
end
it "initiates one api call to jira server to comment on the jira issue" do
- execute_service(project, commit_author, oldrev, newrev, ref)
+ execute_service(project, commit_author, oldrev: oldrev, newrev: newrev, ref: ref)
expect(WebMock).to have_requested(:post, jira_api_comment_url('JIRA-1')).with(
body: comment_body
@@ -492,14 +492,14 @@ describe Git::BranchPushService, services: true do
end
it "closes the internal issue" do
- execute_service(project, commit_author, oldrev, newrev, ref)
+ execute_service(project, commit_author, oldrev: oldrev, newrev: newrev, ref: ref)
expect(issue.reload).to be_closed
end
it "adds a note indicating that the issue is now closed" do
expect(SystemNoteService).to receive(:change_status)
.with(issue, project, commit_author, "closed", closing_commit)
- execute_service(project, commit_author, oldrev, newrev, ref)
+ execute_service(project, commit_author, oldrev: oldrev, newrev: newrev, ref: ref)
end
end
end
@@ -517,7 +517,7 @@ describe Git::BranchPushService, services: true do
end
it 'push to first branch updates HEAD' do
- execute_service(project, user, blankrev, newrev, new_ref)
+ execute_service(project, user, oldrev: blankrev, newrev: newrev, ref: new_ref)
end
end
@@ -542,7 +542,7 @@ describe Git::BranchPushService, services: true do
it 'does not perform housekeeping when not needed' do
expect(housekeeping).not_to receive(:execute)
- execute_service(project, user, oldrev, newrev, ref)
+ execute_service(project, user, oldrev: oldrev, newrev: newrev, ref: ref)
end
context 'when housekeeping is needed' do
@@ -553,20 +553,20 @@ describe Git::BranchPushService, services: true do
it 'performs housekeeping' do
expect(housekeeping).to receive(:execute)
- execute_service(project, user, oldrev, newrev, ref)
+ execute_service(project, user, oldrev: oldrev, newrev: newrev, ref: ref)
end
it 'does not raise an exception' do
allow(housekeeping).to receive(:try_obtain_lease).and_return(false)
- execute_service(project, user, oldrev, newrev, ref)
+ execute_service(project, user, oldrev: oldrev, newrev: newrev, ref: ref)
end
end
it 'increments the push counter' do
expect(housekeeping).to receive(:increment!)
- execute_service(project, user, oldrev, newrev, ref)
+ execute_service(project, user, oldrev: oldrev, newrev: newrev, ref: ref)
end
end
@@ -577,7 +577,7 @@ describe Git::BranchPushService, services: true do
it 'does nothing' do
expect(::Ci::StopEnvironmentsService).not_to receive(:new)
- execute_service(project, user, oldrev, newrev, ref)
+ execute_service(project, user, oldrev: oldrev, newrev: newrev, ref: ref)
end
end
@@ -585,7 +585,7 @@ describe Git::BranchPushService, services: true do
it 'does nothing' do
expect(::Ci::StopEnvironmentsService).not_to receive(:new)
- execute_service(project, user, oldrev, newrev, ref)
+ execute_service(project, user, oldrev: oldrev, newrev: newrev, ref: ref)
end
end
@@ -599,7 +599,7 @@ describe Git::BranchPushService, services: true do
expect(stop_service).to receive(:execute).with(branch)
end
- execute_service(project, user, oldrev, newrev, ref)
+ execute_service(project, user, oldrev: oldrev, newrev: newrev, ref: ref)
end
end
end
@@ -611,15 +611,17 @@ describe Git::BranchPushService, services: true do
expect(hooks_service.project).to eq(project)
expect(hooks_service.current_user).to eq(user)
expect(hooks_service.params).to include(
- oldrev: oldrev,
- newrev: newrev,
- ref: ref
+ change: {
+ oldrev: oldrev,
+ newrev: newrev,
+ ref: ref
+ }
)
expect(hooks_service).to receive(:execute)
end
- execute_service(project, user, oldrev, newrev, ref)
+ execute_service(project, user, oldrev: oldrev, newrev: newrev, ref: ref)
end
end
@@ -629,13 +631,13 @@ describe Git::BranchPushService, services: true do
it 'does nothing' do
expect(::Git::BranchHooksService).not_to receive(:new)
- execute_service(project, user, oldrev, newrev, ref)
+ execute_service(project, user, oldrev: oldrev, newrev: newrev, ref: ref)
end
end
end
- def execute_service(project, user, oldrev, newrev, ref)
- service = described_class.new(project, user, oldrev: oldrev, newrev: newrev, ref: ref)
+ def execute_service(project, user, change)
+ service = described_class.new(project, user, change: change)
service.execute
service
end
diff --git a/spec/services/git/process_ref_changes_service_spec.rb b/spec/services/git/process_ref_changes_service_spec.rb
new file mode 100644
index 00000000000..35ddf95b5f6
--- /dev/null
+++ b/spec/services/git/process_ref_changes_service_spec.rb
@@ -0,0 +1,193 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Git::ProcessRefChangesService do
+ let(:project) { create(:project, :repository) }
+ let(:user) { project.owner }
+ let(:params) { { changes: git_changes } }
+
+ subject { described_class.new(project, user, params) }
+
+ shared_examples_for 'service for processing ref changes' do |push_service_class|
+ let(:service) { double(execute: true) }
+ let(:git_changes) { double(branch_changes: [], tag_changes: []) }
+
+ def multiple_changes(change, count)
+ Array.new(count).map.with_index do |n, index|
+ { index: index, oldrev: change[:oldrev], newrev: change[:newrev], ref: "#{change[:ref]}#{n}" }
+ end
+ end
+
+ let(:changes) do
+ [
+ { index: 0, oldrev: Gitlab::Git::BLANK_SHA, newrev: '789012', ref: "#{ref_prefix}/create" },
+ { index: 1, oldrev: '123456', newrev: '789012', ref: "#{ref_prefix}/update" },
+ { index: 2, oldrev: '123456', newrev: Gitlab::Git::BLANK_SHA, ref: "#{ref_prefix}/delete" }
+ ]
+ end
+
+ before do
+ allow(git_changes).to receive(changes_method).and_return(changes)
+ end
+
+ it "calls #{push_service_class}" do
+ expect(push_service_class)
+ .to receive(:new)
+ .with(project, project.owner, hash_including(execute_project_hooks: true, create_push_event: true))
+ .exactly(changes.count).times
+ .and_return(service)
+
+ subject.execute
+ end
+
+ context 'changes exceed push_event_hooks_limit' do
+ let(:push_event_hooks_limit) { 3 }
+
+ let(:changes) do
+ multiple_changes(
+ { oldrev: '123456', newrev: '789012', ref: "#{ref_prefix}/test" },
+ push_event_hooks_limit + 1
+ )
+ end
+
+ before do
+ stub_application_setting(push_event_hooks_limit: push_event_hooks_limit)
+ end
+
+ context 'git_push_execute_all_project_hooks is disabled' do
+ before do
+ stub_feature_flags(git_push_execute_all_project_hooks: false)
+ end
+
+ it "calls #{push_service_class} with execute_project_hooks set to false" do
+ expect(push_service_class)
+ .to receive(:new)
+ .with(project, project.owner, hash_including(execute_project_hooks: false))
+ .exactly(changes.count).times
+ .and_return(service)
+
+ subject.execute
+ end
+ end
+
+ context 'git_push_execute_all_project_hooks is enabled' do
+ before do
+ stub_feature_flags(git_push_execute_all_project_hooks: true)
+ end
+
+ it "calls #{push_service_class} with execute_project_hooks set to true" do
+ expect(push_service_class)
+ .to receive(:new)
+ .with(project, project.owner, hash_including(execute_project_hooks: true))
+ .exactly(changes.count).times
+ .and_return(service)
+
+ subject.execute
+ end
+ end
+ end
+
+ context 'changes exceed push_event_activities_limit per action' do
+ let(:push_event_activities_limit) { 3 }
+
+ let(:changes) do
+ [
+ { oldrev: Gitlab::Git::BLANK_SHA, newrev: '789012', ref: "#{ref_prefix}/create" },
+ { oldrev: '123456', newrev: '789012', ref: "#{ref_prefix}/update" },
+ { oldrev: '123456', newrev: Gitlab::Git::BLANK_SHA, ref: "#{ref_prefix}/delete" }
+ ].map do |change|
+ multiple_changes(change, push_event_activities_limit + 1)
+ end.flatten
+ end
+
+ before do
+ stub_application_setting(push_event_activities_limit: push_event_activities_limit)
+ end
+
+ it "calls #{push_service_class} with create_push_event set to false" do
+ expect(push_service_class)
+ .to receive(:new)
+ .with(project, project.owner, hash_including(create_push_event: false))
+ .exactly(changes.count).times
+ .and_return(service)
+
+ subject.execute
+ end
+
+ it 'creates events per action' do
+ allow(push_service_class).to receive(:new).and_return(service)
+
+ expect { subject.execute }.to change { Event.count }.by(3)
+ end
+ end
+
+ context 'pipeline creation' do
+ context 'with valid .gitlab-ci.yml' do
+ before do
+ stub_ci_pipeline_to_return_yaml_file
+
+ allow_any_instance_of(Project)
+ .to receive(:commit)
+ .and_return(project.commit)
+
+ allow_any_instance_of(Repository)
+ .to receive(:branch_exists?)
+ .and_return(true)
+ end
+
+ context 'when git_push_create_all_pipelines is disabled' do
+ before do
+ stub_feature_flags(git_push_create_all_pipelines: false)
+ end
+
+ it 'creates pipeline for branches and tags' do
+ subject.execute
+
+ expect(Ci::Pipeline.pluck(:ref)).to contain_exactly('create', 'update', 'delete')
+ end
+
+ it "creates exactly #{described_class::PIPELINE_PROCESS_LIMIT} pipelines" do
+ stub_const("#{described_class}::PIPELINE_PROCESS_LIMIT", changes.count - 1)
+
+ expect { subject.execute }.to change { Ci::Pipeline.count }.by(described_class::PIPELINE_PROCESS_LIMIT)
+ end
+ end
+
+ context 'when git_push_create_all_pipelines is enabled' do
+ before do
+ stub_feature_flags(git_push_create_all_pipelines: true)
+ end
+
+ it 'creates all pipelines' do
+ expect { subject.execute }.to change { Ci::Pipeline.count }.by(changes.count)
+ end
+ end
+ end
+
+ context 'with invalid .gitlab-ci.yml' do
+ before do
+ stub_ci_pipeline_yaml_file(nil)
+ end
+
+ it 'does not create a pipeline' do
+ expect { subject.execute }.not_to change { Ci::Pipeline.count }
+ end
+ end
+ end
+ end
+
+ context 'branch changes' do
+ let(:changes_method) { :branch_changes }
+ let(:ref_prefix) { 'refs/heads' }
+
+ it_behaves_like 'service for processing ref changes', Git::BranchPushService
+ end
+
+ context 'tag changes' do
+ let(:changes_method) { :tag_changes }
+ let(:ref_prefix) { 'refs/tags' }
+
+ it_behaves_like 'service for processing ref changes', Git::TagPushService
+ end
+end
diff --git a/spec/services/git/tag_hooks_service_spec.rb b/spec/services/git/tag_hooks_service_spec.rb
index e362577d289..abb5b9b130b 100644
--- a/spec/services/git/tag_hooks_service_spec.rb
+++ b/spec/services/git/tag_hooks_service_spec.rb
@@ -15,13 +15,7 @@ describe Git::TagHooksService, :service do
let(:commit) { tag.dereferenced_target }
let(:service) do
- described_class.new(project, user, oldrev: oldrev, newrev: newrev, ref: ref)
- end
-
- it 'update remote mirrors' do
- expect(service).to receive(:update_remote_mirrors).and_call_original
-
- service.execute
+ described_class.new(project, user, change: { oldrev: oldrev, newrev: newrev, ref: ref })
end
describe 'System hooks' do
@@ -64,6 +58,7 @@ describe Git::TagHooksService, :service do
describe 'Push data' do
shared_examples_for 'tag push data expectations' do
subject(:push_data) { service.send(:push_data) }
+
it 'has expected push data attributes' do
is_expected.to match a_hash_including(
object_kind: 'tag_push',
diff --git a/spec/services/git/tag_push_service_spec.rb b/spec/services/git/tag_push_service_spec.rb
index 7e008637182..9688041c08c 100644
--- a/spec/services/git/tag_push_service_spec.rb
+++ b/spec/services/git/tag_push_service_spec.rb
@@ -8,7 +8,7 @@ describe Git::TagPushService do
let(:user) { create(:user) }
let(:project) { create(:project, :repository) }
- let(:service) { described_class.new(project, user, oldrev: oldrev, newrev: newrev, ref: ref) }
+ let(:service) { described_class.new(project, user, change: { oldrev: oldrev, newrev: newrev, ref: ref }) }
let(:oldrev) { Gitlab::Git::BLANK_SHA }
let(:newrev) { "8a2a6eb295bb170b34c24c76c49ed0e9b2eaf34b" } # gitlab-test: git rev-parse refs/tags/v1.1.0
diff --git a/spec/services/grafana/proxy_service_spec.rb b/spec/services/grafana/proxy_service_spec.rb
new file mode 100644
index 00000000000..694d531c9fc
--- /dev/null
+++ b/spec/services/grafana/proxy_service_spec.rb
@@ -0,0 +1,139 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Grafana::ProxyService do
+ include ReactiveCachingHelpers
+
+ let_it_be(:project) { create(:project) }
+ let_it_be(:grafana_integration) { create(:grafana_integration, project: project) }
+
+ let(:proxy_path) { 'api/v1/query_range' }
+ let(:datasource_id) { '1' }
+ let(:query_params) do
+ {
+ 'query' => 'rate(relevant_metric)',
+ 'start' => '1570441248',
+ 'end' => '1570444848',
+ 'step' => '900'
+ }
+ end
+
+ let(:cache_params) { [project.id, datasource_id, proxy_path, query_params] }
+
+ let(:service) do
+ described_class.new(project, datasource_id, proxy_path, query_params)
+ end
+
+ shared_examples_for 'initializes an instance' do
+ it 'initializes an instance of ProxyService class' do
+ expect(subject).to be_an_instance_of(described_class)
+ expect(subject.project).to eq(project)
+ expect(subject.datasource_id).to eq('1')
+ expect(subject.proxy_path).to eq('api/v1/query_range')
+ expect(subject.query_params).to eq(query_params)
+ end
+ end
+
+ describe '.from_cache' do
+ subject { described_class.from_cache(*cache_params) }
+
+ it_behaves_like 'initializes an instance'
+ end
+
+ describe '#initialize' do
+ subject { service }
+
+ it_behaves_like 'initializes an instance'
+ end
+
+ describe '#execute' do
+ subject(:result) { service.execute }
+
+ context 'when grafana integration is not configured' do
+ before do
+ allow(project).to receive(:grafana_integration).and_return(nil)
+ end
+
+ it 'returns error' do
+ expect(result).to eq(
+ status: :error,
+ message: 'Proxy support for this API is not available currently'
+ )
+ end
+ end
+
+ context 'with caching', :use_clean_rails_memory_store_caching do
+ context 'when value not present in cache' do
+ it 'returns nil' do
+ expect(ReactiveCachingWorker)
+ .to receive(:perform_async)
+ .with(service.class, service.id, *cache_params)
+
+ expect(result).to eq(nil)
+ end
+ end
+
+ context 'when value present in cache' do
+ let(:return_value) { { 'http_status' => 200, 'body' => 'body' } }
+
+ before do
+ stub_reactive_cache(service, return_value, cache_params)
+ end
+
+ it 'returns cached value' do
+ expect(ReactiveCachingWorker)
+ .not_to receive(:perform_async)
+ .with(service.class, service.id, *cache_params)
+
+ expect(result[:http_status]).to eq(return_value[:http_status])
+ expect(result[:body]).to eq(return_value[:body])
+ end
+ end
+ end
+
+ context 'call prometheus api' do
+ let(:client) { service.send(:client) }
+
+ before do
+ synchronous_reactive_cache(service)
+ end
+
+ context 'connection to grafana datasource succeeds' do
+ let(:response) { instance_double(Gitlab::HTTP::Response) }
+ let(:status_code) { 400 }
+ let(:body) { 'body' }
+
+ before do
+ allow(client).to receive(:proxy_datasource).and_return(response)
+
+ allow(response).to receive(:code).and_return(status_code)
+ allow(response).to receive(:body).and_return(body)
+ end
+
+ it 'returns the http status code and body from prometheus' do
+ expect(result).to eq(
+ http_status: status_code,
+ body: body,
+ status: :success
+ )
+ end
+ end
+
+ context 'connection to grafana datasource fails' do
+ before do
+ allow(client).to receive(:proxy_datasource)
+ .and_raise(Grafana::Client::Error, 'Network connection error')
+ end
+
+ it 'returns error' do
+ expect(result).to eq(
+ status: :error,
+ message: 'Network connection error',
+ http_status: :service_unavailable
+ )
+ end
+ end
+ end
+ end
+end
diff --git a/spec/services/groups/destroy_service_spec.rb b/spec/services/groups/destroy_service_spec.rb
index 1ab2e994b7e..d13739cefd9 100644
--- a/spec/services/groups/destroy_service_spec.rb
+++ b/spec/services/groups/destroy_service_spec.rb
@@ -8,7 +8,7 @@ describe Groups::DestroyService do
let!(:user) { create(:user) }
let!(:group) { create(:group) }
let!(:nested_group) { create(:group, parent: group) }
- let!(:project) { create(:project, :legacy_storage, namespace: group) }
+ let!(:project) { create(:project, :repository, :legacy_storage, namespace: group) }
let!(:notification_setting) { create(:notification_setting, source: group)}
let(:gitlab_shell) { Gitlab::Shell.new }
let(:remove_path) { group.path + "+#{group.id}+deleted" }
@@ -119,7 +119,7 @@ describe Groups::DestroyService do
let!(:project) { create(:project, :legacy_storage, :empty_repo, namespace: group) }
it 'removes repository' do
- expect(gitlab_shell.exists?(project.repository_storage, "#{project.disk_path}.git")).to be_falsey
+ expect(gitlab_shell.repository_exists?(project.repository_storage, "#{project.disk_path}.git")).to be_falsey
end
end
@@ -127,7 +127,7 @@ describe Groups::DestroyService do
let!(:project) { create(:project, :empty_repo, namespace: group) }
it 'removes repository' do
- expect(gitlab_shell.exists?(project.repository_storage, "#{project.disk_path}.git")).to be_falsey
+ expect(gitlab_shell.repository_exists?(project.repository_storage, "#{project.disk_path}.git")).to be_falsey
end
end
end
diff --git a/spec/services/groups/transfer_service_spec.rb b/spec/services/groups/transfer_service_spec.rb
index 0cbb3122bb0..5ef1fb1932f 100644
--- a/spec/services/groups/transfer_service_spec.rb
+++ b/spec/services/groups/transfer_service_spec.rb
@@ -426,5 +426,22 @@ describe Groups::TransferService do
end
end
end
+
+ context 'when a project in group has container images' do
+ let(:group) { create(:group, :public, :nested) }
+ let!(:project) { create(:project, :repository, :public, namespace: group) }
+
+ before do
+ stub_container_registry_tags(repository: /image/, tags: %w[rc1])
+ create(:container_repository, project: project, name: :image)
+ create(:group_member, :owner, group: new_parent_group, user: user)
+ end
+
+ it 'does not allow group to be transferred' do
+ transfer_service.execute(new_parent_group)
+
+ expect(transfer_service.error).to match(/Docker images in their Container Registry/)
+ end
+ end
end
end
diff --git a/spec/services/groups/update_service_spec.rb b/spec/services/groups/update_service_spec.rb
index 12e9c2b2f3a..ca8eaf4c970 100644
--- a/spec/services/groups/update_service_spec.rb
+++ b/spec/services/groups/update_service_spec.rb
@@ -148,6 +148,30 @@ describe Groups::UpdateService do
end
end
+ context 'projects in group have container images' do
+ let(:service) { described_class.new(public_group, user, path: SecureRandom.hex) }
+ let(:project) { create(:project, :internal, group: public_group) }
+
+ before do
+ stub_container_registry_tags(repository: /image/, tags: %w[rc1])
+ create(:container_repository, project: project, name: :image)
+ end
+
+ it 'does not allow path to be changed' do
+ result = described_class.new(public_group, user, path: 'new-path').execute
+
+ expect(result).to eq false
+ expect(public_group.errors[:base].first).to match(/Docker images in their Container Registry/)
+ end
+
+ it 'allows other settings to be changed' do
+ result = described_class.new(public_group, user, name: 'new-name').execute
+
+ expect(result).to eq true
+ expect(public_group.reload.name).to eq('new-name')
+ end
+ end
+
context 'for a subgroup' do
let(:subgroup) { create(:group, :private, parent: private_group) }
diff --git a/spec/services/issues/close_service_spec.rb b/spec/services/issues/close_service_spec.rb
index 642a49d57d5..1f7d564b6ec 100644
--- a/spec/services/issues/close_service_spec.rb
+++ b/spec/services/issues/close_service_spec.rb
@@ -8,6 +8,7 @@ describe Issues::CloseService do
let(:user2) { create(:user, email: "user2@example.com") }
let(:guest) { create(:user) }
let(:issue) { create(:issue, title: "My issue", project: project, assignees: [user2], author: create(:user)) }
+ let(:external_issue) { ExternalIssue.new('JIRA-123', project) }
let(:closing_merge_request) { create(:merge_request, source_project: project) }
let(:closing_commit) { create(:commit, project: project) }
let!(:todo) { create(:todo, :assigned, user: user, project: project, target: issue, author: user2) }
@@ -36,6 +37,16 @@ describe Issues::CloseService do
expect(service.execute(issue)).to eq(issue)
end
+ it 'closes the external issue even when the user is not authorized to do so' do
+ allow(service).to receive(:can?).with(user, :update_issue, external_issue)
+ .and_return(false)
+
+ expect(service).to receive(:close_issue)
+ .with(external_issue, closed_via: nil, notifications: true, system_note: true)
+
+ service.execute(external_issue)
+ end
+
it 'closes the issue when the user is authorized to do so' do
allow(service).to receive(:can?).with(user, :update_issue, issue)
.and_return(true)
diff --git a/spec/services/issues/create_service_spec.rb b/spec/services/issues/create_service_spec.rb
index b7bedc2f97e..5dc6b6176ee 100644
--- a/spec/services/issues/create_service_spec.rb
+++ b/spec/services/issues/create_service_spec.rb
@@ -344,7 +344,7 @@ describe Issues::CreateService do
end
before do
- allow_any_instance_of(SpamService).to receive(:check_for_spam?).and_return(true)
+ stub_feature_flags(allow_possible_spam: false)
end
context 'when recaptcha was verified' do
@@ -384,31 +384,67 @@ describe Issues::CreateService do
end
context 'when recaptcha was not verified' do
+ before do
+ expect_next_instance_of(SpamService) do |spam_service|
+ expect(spam_service).to receive_messages(check_for_spam?: true)
+ end
+ end
+
context 'when akismet detects spam' do
before do
- allow_any_instance_of(AkismetService).to receive(:spam?).and_return(true)
+ expect_next_instance_of(AkismetService) do |akismet_service|
+ expect(akismet_service).to receive_messages(spam?: true)
+ end
end
- it 'marks an issue as a spam ' do
- expect(issue).to be_spam
- end
+ context 'when issuables_recaptcha_enabled feature flag is true' do
+ it 'marks an issue as a spam ' do
+ expect(issue).to be_spam
+ end
- it 'an issue is not valid ' do
- expect(issue.valid?).to be_falsey
- end
+ it 'invalidates the issue' do
+ expect(issue).to be_invalid
+ end
+
+ it 'creates a new spam_log' do
+ expect { issue }
+ .to log_spam(title: issue.title, description: issue.description, user_id: user.id, noteable_type: 'Issue')
+ end
- it 'creates a new spam_log' do
- expect {issue}.to change {SpamLog.count}.from(0).to(1)
+ it 'assigns a spam_log to an issue' do
+ expect(issue.spam_log).to eq(SpamLog.last)
+ end
end
- it 'assigns a spam_log to an issue' do
- expect(issue.spam_log).to eq(SpamLog.last)
+ context 'when issuable_recaptcha_enabled feature flag is false' do
+ before do
+ stub_feature_flags(allow_possible_spam: true)
+ end
+
+ it 'does not mark an issue as a spam ' do
+ expect(issue).not_to be_spam
+ end
+
+ it 'accepts the ​issue as valid' do
+ expect(issue).to be_valid
+ end
+
+ it 'creates a new spam_log' do
+ expect { issue }
+ .to log_spam(title: issue.title, description: issue.description, user_id: user.id, noteable_type: 'Issue')
+ end
+
+ it 'assigns a spam_log to an issue' do
+ expect(issue.spam_log).to eq(SpamLog.last)
+ end
end
end
context 'when akismet does not detect spam' do
before do
- allow_any_instance_of(AkismetService).to receive(:spam?).and_return(false)
+ expect_next_instance_of(AkismetService) do |akismet_service|
+ expect(akismet_service).to receive_messages(spam?: false)
+ end
end
it 'does not mark an issue as a spam ' do
diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb
index 55254b61ac8..154bfec0da2 100644
--- a/spec/services/issues/update_service_spec.rb
+++ b/spec/services/issues/update_service_spec.rb
@@ -6,7 +6,8 @@ describe Issues::UpdateService, :mailer do
let(:user) { create(:user) }
let(:user2) { create(:user) }
let(:user3) { create(:user) }
- let(:project) { create(:project) }
+ let(:group) { create(:group, :public) }
+ let(:project) { create(:project, :repository, group: group) }
let(:label) { create(:label, project: project) }
let(:label2) { create(:label) }
@@ -667,6 +668,7 @@ describe Issues::UpdateService, :mailer do
context 'updating mentions' do
let(:mentionable) { issue }
+
include_examples 'updating mentions', described_class
end
diff --git a/spec/services/issues/zoom_link_service_spec.rb b/spec/services/issues/zoom_link_service_spec.rb
index baa6d774864..ba3f007c917 100644
--- a/spec/services/issues/zoom_link_service_spec.rb
+++ b/spec/services/issues/zoom_link_service_spec.rb
@@ -38,12 +38,6 @@ describe Issues::ZoomLinkService do
end
end
- shared_context 'feature flag disabled' do
- before do
- stub_feature_flags(issue_zoom_integration: false)
- end
- end
-
shared_context 'insufficient permissions' do
before do
project.add_guest(user)
@@ -57,6 +51,12 @@ describe Issues::ZoomLinkService do
expect(result.payload[:description])
.to eq("#{issue.description}\n\n#{zoom_link}")
end
+
+ it 'tracks the add event' do
+ expect(Gitlab::Tracking).to receive(:event)
+ .with('IncidentManagement::ZoomIntegration', 'add_zoom_meeting', label: 'Issue ID', value: issue.id)
+ result
+ end
end
shared_examples 'cannot add link' do
@@ -78,11 +78,6 @@ describe Issues::ZoomLinkService do
include_examples 'cannot add link'
end
- context 'when feature flag is disabled' do
- include_context 'feature flag disabled'
- include_examples 'cannot add link'
- end
-
context 'with insufficient permissions' do
include_context 'insufficient permissions'
include_examples 'cannot add link'
@@ -113,12 +108,6 @@ describe Issues::ZoomLinkService do
it { is_expected.to eq(true) }
- context 'when feature flag is disabled' do
- include_context 'feature flag disabled'
-
- it { is_expected.to eq(false) }
- end
-
context 'with insufficient permissions' do
include_context 'insufficient permissions'
@@ -152,9 +141,11 @@ describe Issues::ZoomLinkService do
.to eq(issue.description.delete_suffix("\n\n#{zoom_link}"))
end
- context 'when feature flag is disabled' do
- include_context 'feature flag disabled'
- include_examples 'cannot remove link'
+ it 'tracks the remove event' do
+ expect(Gitlab::Tracking).to receive(:event)
+ .with('IncidentManagement::ZoomIntegration', 'remove_zoom_meeting', label: 'Issue ID', value: issue.id)
+
+ result
end
context 'with insufficient permissions' do
@@ -187,12 +178,6 @@ describe Issues::ZoomLinkService do
it { is_expected.to eq(true) }
- context 'when feature flag is disabled' do
- include_context 'feature flag disabled'
-
- it { is_expected.to eq(false) }
- end
-
context 'with insufficient permissions' do
include_context 'insufficient permissions'
diff --git a/spec/services/members/approve_access_request_service_spec.rb b/spec/services/members/approve_access_request_service_spec.rb
index f56c31e51f6..5bbceac3dd0 100644
--- a/spec/services/members/approve_access_request_service_spec.rb
+++ b/spec/services/members/approve_access_request_service_spec.rb
@@ -3,8 +3,8 @@
require 'spec_helper'
describe Members::ApproveAccessRequestService do
- let(:project) { create(:project, :public, :access_requestable) }
- let(:group) { create(:group, :public, :access_requestable) }
+ let(:project) { create(:project, :public) }
+ let(:group) { create(:group, :public) }
let(:current_user) { create(:user) }
let(:access_requester_user) { create(:user) }
let(:access_requester) { source.requesters.find_by!(user_id: access_requester_user.id) }
diff --git a/spec/services/members/request_access_service_spec.rb b/spec/services/members/request_access_service_spec.rb
index 2e5275eb3f2..a0f7ae91bdb 100644
--- a/spec/services/members/request_access_service_spec.rb
+++ b/spec/services/members/request_access_service_spec.rb
@@ -41,7 +41,7 @@ describe Members::RequestAccessService do
context 'when access requests are disabled' do
%i[project group].each do |source_type|
it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do
- let(:source) { create(source_type, :public) }
+ let(:source) { create(source_type, :public, :request_access_disabled) }
end
end
end
@@ -49,7 +49,7 @@ describe Members::RequestAccessService do
context 'when current user can request access to the project' do
%i[project group].each do |source_type|
it_behaves_like 'a service creating a access request' do
- let(:source) { create(source_type, :public, :access_requestable) }
+ let(:source) { create(source_type, :public) }
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
index 07e0218e1df..51a5c51f6c3 100644
--- a/spec/services/merge_requests/create_from_issue_service_spec.rb
+++ b/spec/services/merge_requests/create_from_issue_service_spec.rb
@@ -13,6 +13,7 @@ describe MergeRequests::CreateFromIssueService do
let(:custom_source_branch) { 'custom-source-branch' }
subject(:service) { described_class.new(project, user, service_params) }
+
subject(:service_with_custom_source_branch) { described_class.new(project, user, branch_name: custom_source_branch, **service_params) }
before do
diff --git a/spec/services/merge_requests/post_merge_service_spec.rb b/spec/services/merge_requests/post_merge_service_spec.rb
index ffc86f68469..fff6ddf3928 100644
--- a/spec/services/merge_requests/post_merge_service_spec.rb
+++ b/spec/services/merge_requests/post_merge_service_spec.rb
@@ -56,9 +56,11 @@ describe MergeRequests::PostMergeService do
issue = create(:issue, project: project)
allow(merge_request).to receive(:visible_closing_issues_for).and_return([issue])
- allow_any_instance_of(Issues::CloseService).to receive(:execute).with(issue, commit: merge_request).and_raise
+ expect_next_instance_of(Issues::CloseService) do |service|
+ allow(service).to receive(:execute).with(issue, commit: merge_request).and_raise(RuntimeError)
+ end
- expect { described_class.new(project, user, {}).execute(merge_request) }.to raise_error
+ expect { described_class.new(project, user).execute(merge_request) }.to raise_error(RuntimeError)
expect(merge_request.reload).to be_merged
end
diff --git a/spec/services/merge_requests/refresh_service_spec.rb b/spec/services/merge_requests/refresh_service_spec.rb
index 6ba67c7165c..2dc932c9f2c 100644
--- a/spec/services/merge_requests/refresh_service_spec.rb
+++ b/spec/services/merge_requests/refresh_service_spec.rb
@@ -4,6 +4,7 @@ require 'spec_helper'
describe MergeRequests::RefreshService do
include ProjectForksHelper
+ include ProjectHelpers
let(:project) { create(:project, :repository) }
let(:user) { create(:user) }
@@ -756,4 +757,127 @@ describe MergeRequests::RefreshService do
end
end
end
+
+ describe '#abort_ff_merge_requests_with_when_pipeline_succeeds' do
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:source_project) { project }
+ let_it_be(:target_project) { project }
+ let_it_be(:author) { create_user_from_membership(target_project, :developer) }
+ let_it_be(:user) { create(:user) }
+
+ let_it_be(:forked_project) do
+ fork_project(target_project, author, repository: true)
+ end
+
+ let_it_be(:merge_request) do
+ create(:merge_request,
+ author: author,
+ source_project: source_project,
+ source_branch: 'feature',
+ target_branch: 'master',
+ target_project: target_project,
+ auto_merge_enabled: true,
+ auto_merge_strategy: AutoMergeService::STRATEGY_MERGE_WHEN_PIPELINE_SUCCEEDS,
+ merge_user: user)
+ end
+
+ let_it_be(:newrev) do
+ target_project
+ .repository
+ .create_file(user, 'test1.txt', 'Test data',
+ message: 'Test commit', branch_name: 'master')
+ end
+
+ let_it_be(:oldrev) do
+ target_project
+ .repository
+ .commit(newrev)
+ .parent_id
+ end
+
+ let(:refresh_service) { service.new(project, user) }
+
+ before do
+ target_project.merge_method = merge_method
+ target_project.save!
+
+ refresh_service.execute(oldrev, newrev, 'refs/heads/master')
+ merge_request.reload
+ end
+
+ let(:aborted_message) do
+ /aborted the automatic merge because target branch was updated/
+ end
+
+ shared_examples 'aborted MWPS' do
+ it 'aborts auto_merge' do
+ expect(merge_request.auto_merge_enabled?).to be_falsey
+ expect(merge_request.notes.last.note).to match(aborted_message)
+ end
+
+ it 'removes merge_user' do
+ expect(merge_request.merge_user).to be_nil
+ end
+
+ it 'does not add todos for merge user' do
+ expect(user.todos.for_target(merge_request)).to be_empty
+ end
+
+ it 'adds todos for merge author' do
+ expect(author.todos.for_target(merge_request)).to be_present.and be_all(&:pending?)
+ end
+ end
+
+ context 'when Project#merge_method is set to FF' do
+ let(:merge_method) { :ff }
+
+ it_behaves_like 'aborted MWPS'
+
+ context 'with forked project' do
+ let(:source_project) { forked_project }
+
+ it_behaves_like 'aborted MWPS'
+ end
+ end
+
+ context 'when Project#merge_method is set to rebase_merge' do
+ let(:merge_method) { :rebase_merge }
+
+ it_behaves_like 'aborted MWPS'
+
+ context 'with forked project' do
+ let(:source_project) { forked_project }
+
+ it_behaves_like 'aborted MWPS'
+ end
+ end
+
+ context 'when Project#merge_method is set to merge' do
+ let(:merge_method) { :merge }
+
+ shared_examples 'maintained MWPS' do
+ it 'does not cancel auto merge' do
+ expect(merge_request.auto_merge_enabled?).to be_truthy
+ expect(merge_request.notes).to be_empty
+ end
+
+ it 'does not change merge_user' do
+ expect(merge_request.merge_user).to eq(user)
+ end
+
+ it 'does not add todos' do
+ expect(author.todos.for_target(merge_request)).to be_empty
+ expect(user.todos.for_target(merge_request)).to be_empty
+ end
+ end
+
+ it_behaves_like 'maintained MWPS'
+
+ context 'with forked project' do
+ let(:source_project) { forked_project }
+
+ it_behaves_like 'maintained MWPS'
+ end
+ end
+ end
end
diff --git a/spec/services/merge_requests/update_service_spec.rb b/spec/services/merge_requests/update_service_spec.rb
index 9688e02d6ac..d3c4c436901 100644
--- a/spec/services/merge_requests/update_service_spec.rb
+++ b/spec/services/merge_requests/update_service_spec.rb
@@ -5,7 +5,8 @@ require 'spec_helper'
describe MergeRequests::UpdateService, :mailer do
include ProjectForksHelper
- let(:project) { create(:project, :repository) }
+ let(:group) { create(:group, :public) }
+ let(:project) { create(:project, :repository, group: group) }
let(:user) { create(:user) }
let(:user2) { create(:user) }
let(:user3) { create(:user) }
@@ -472,6 +473,7 @@ describe MergeRequests::UpdateService, :mailer do
context 'updating mentions' do
let(:mentionable) { merge_request }
+
include_examples 'updating mentions', described_class
end
diff --git a/spec/services/note_summary_spec.rb b/spec/services/note_summary_spec.rb
index e59731207a5..aa4e41f4d8c 100644
--- a/spec/services/note_summary_spec.rb
+++ b/spec/services/note_summary_spec.rb
@@ -46,5 +46,17 @@ describe NoteSummary do
it 'returns metadata hash' do
expect(create_note_summary.metadata).to eq(action: 'icon', commit_count: 5)
end
+
+ context 'description action and noteable has saved_description_version' do
+ before do
+ noteable.saved_description_version = 1
+ end
+
+ subject { described_class.new(noteable, project, user, 'note', action: 'description') }
+
+ it 'sets the description_version metadata' do
+ expect(subject.metadata).to include(description_version: 1)
+ end
+ end
end
end
diff --git a/spec/services/notes/update_service_spec.rb b/spec/services/notes/update_service_spec.rb
index 717eb97fa5a..73fcdd787aa 100644
--- a/spec/services/notes/update_service_spec.rb
+++ b/spec/services/notes/update_service_spec.rb
@@ -3,17 +3,25 @@
require 'spec_helper'
describe Notes::UpdateService do
- let(:project) { create(:project) }
+ let(:group) { create(:group, :public) }
+ let(:project) { create(:project, :public, group: group) }
+ let(:private_group) { create(:group, :private) }
+ let(:private_project) { create(:project, :private, group: private_group) }
let(:user) { create(:user) }
let(:user2) { create(:user) }
let(:user3) { create(:user) }
let(:issue) { create(:issue, project: project) }
+ let(:issue2) { create(:issue, project: private_project) }
let(:note) { create(:note, project: project, noteable: issue, author: user, note: "Old note #{user2.to_reference}") }
before do
project.add_maintainer(user)
project.add_developer(user2)
project.add_developer(user3)
+ group.add_developer(user3)
+ private_group.add_developer(user)
+ private_group.add_developer(user2)
+ private_project.add_developer(user3)
end
describe '#execute' do
@@ -46,13 +54,17 @@ describe Notes::UpdateService do
end
context 'todos' do
- let!(:todo) { create(:todo, :assigned, user: user, project: project, target: issue, author: user2) }
+ shared_examples 'does not update todos' do
+ it 'keep todos' do
+ expect(todo.reload).to be_pending
+ end
- context 'when the note change' do
- before do
- update_note({ note: "New note #{user2.to_reference} #{user3.to_reference}" })
+ it 'does not create any new todos' do
+ expect(Todo.count).to eq(1)
end
+ end
+ shared_examples 'creates one todo' do
it 'marks todos as done' do
expect(todo.reload).to be_done
end
@@ -62,17 +74,75 @@ describe Notes::UpdateService do
end
end
- context 'when the note does not change' do
- before do
- update_note({ note: "Old note #{user2.to_reference}" })
+ context 'when note includes a user mention' do
+ let!(:todo) { create(:todo, :assigned, user: user, project: project, target: issue, author: user2) }
+
+ context 'when the note does not change mentions' do
+ before do
+ update_note({ note: "Old note #{user2.to_reference}" })
+ end
+
+ it_behaves_like 'does not update todos'
end
- it 'keep todos' do
- expect(todo.reload).to be_pending
+ context 'when the note changes to include one more user mention' do
+ before do
+ update_note({ note: "New note #{user2.to_reference} #{user3.to_reference}" })
+ end
+
+ it_behaves_like 'creates one todo'
end
- it 'does not create any new todos' do
- expect(Todo.count).to eq(1)
+ context 'when the note changes to include a group mentions' do
+ before do
+ update_note({ note: "New note #{private_group.to_reference}" })
+ end
+
+ it_behaves_like 'creates one todo'
+ end
+ end
+
+ context 'when note includes a group mention' do
+ context 'when the group is public' do
+ let(:note) { create(:note, project: project, noteable: issue, author: user, note: "Old note #{group.to_reference}") }
+ let!(:todo) { create(:todo, :assigned, user: user, project: project, target: issue, author: user2) }
+
+ context 'when the note does not change mentions' do
+ before do
+ update_note({ note: "Old note #{group.to_reference}" })
+ end
+
+ it_behaves_like 'does not update todos'
+ end
+
+ context 'when the note changes mentions' do
+ before do
+ update_note({ note: "New note #{user2.to_reference} #{user3.to_reference}" })
+ end
+
+ it_behaves_like 'creates one todo'
+ end
+ end
+
+ context 'when the group is private' do
+ let(:note) { create(:note, project: project, noteable: issue, author: user, note: "Old note #{private_group.to_reference}") }
+ let!(:todo) { create(:todo, :assigned, user: user, project: project, target: issue, author: user2) }
+
+ context 'when the note does not change mentions' do
+ before do
+ update_note({ note: "Old note #{private_group.to_reference}" })
+ end
+
+ it_behaves_like 'does not update todos'
+ end
+
+ context 'when the note changes mentions' do
+ before do
+ update_note({ note: "New note #{user2.to_reference} #{user3.to_reference}" })
+ end
+
+ it_behaves_like 'creates one todo'
+ end
end
end
end
diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb
index bd6734634cb..aa67b87a645 100644
--- a/spec/services/notification_service_spec.rb
+++ b/spec/services/notification_service_spec.rb
@@ -678,6 +678,27 @@ describe NotificationService, :mailer do
end
end
+ describe '#send_new_release_notifications' do
+ context 'when recipients for a new release exist' do
+ let(:release) { create(:release) }
+
+ it 'calls new_release_email for each relevant recipient' do
+ user_1 = create(:user)
+ user_2 = create(:user)
+ user_3 = create(:user)
+ recipient_1 = NotificationRecipient.new(user_1, :custom, custom_action: :new_release)
+ recipient_2 = NotificationRecipient.new(user_2, :custom, custom_action: :new_release)
+ allow(NotificationRecipientService).to receive(:build_new_release_recipients).and_return([recipient_1, recipient_2])
+
+ release
+
+ should_email(user_1)
+ should_email(user_2)
+ should_not_email(user_3)
+ end
+ end
+ end
+
describe 'Participating project notification settings have priority over group and global settings if available' do
let!(:group) { create(:group) }
let!(:maintainer) { group.add_owner(create(:user, username: 'maintainer')).user }
@@ -1942,7 +1963,7 @@ describe NotificationService, :mailer do
let(:developer) { create(:user) }
let!(:group) do
- create(:group, :public, :access_requestable) do |group|
+ create(:group, :public) do |group|
group.add_owner(owner)
group.add_maintainer(maintainer)
group.add_developer(developer)
@@ -1968,7 +1989,7 @@ describe NotificationService, :mailer do
end
it_behaves_like 'sends notification only to a maximum of ten, most recently active group owners' do
- let(:group) { create(:group, :public, :access_requestable) }
+ let(:group) { create(:group, :public) }
let(:notification_trigger) { group.request_access(added_user) }
end
end
@@ -2029,7 +2050,7 @@ describe NotificationService, :mailer do
let(:maintainer) { create(:user) }
let!(:project) do
- create(:project, :public, :access_requestable) do |project|
+ create(:project, :public) do |project|
project.add_developer(developer)
project.add_maintainer(maintainer)
end
@@ -2053,7 +2074,7 @@ describe NotificationService, :mailer do
end
it_behaves_like 'sends notification only to a maximum of ten, most recently active project maintainers' do
- let(:project) { create(:project, :public, :access_requestable) }
+ let(:project) { create(:project, :public) }
let(:notification_trigger) { project.request_access(added_user) }
end
end
@@ -2064,7 +2085,7 @@ describe NotificationService, :mailer do
context 'when the project has no maintainers' do
context 'when the group has at least one owner' do
- let!(:project) { create(:project, :public, :access_requestable, namespace: group) }
+ let!(:project) { create(:project, :public, namespace: group) }
before do
reset_delivered_emails!
@@ -2079,14 +2100,14 @@ describe NotificationService, :mailer do
end
it_behaves_like 'sends notification only to a maximum of ten, most recently active group owners' do
- let(:group) { create(:group, :public, :access_requestable) }
+ let(:group) { create(:group, :public) }
let(:notification_trigger) { project.request_access(added_user) }
end
end
context 'when the group does not have any owners' do
let(:group) { create(:group) }
- let!(:project) { create(:project, :public, :access_requestable, namespace: group) }
+ let!(:project) { create(:project, :public, namespace: group) }
context 'recipients' do
before do
@@ -2107,7 +2128,7 @@ describe NotificationService, :mailer do
let(:developer) { create(:user) }
let!(:project) do
- create(:project, :public, :access_requestable, namespace: group) do |project|
+ create(:project, :public, namespace: group) do |project|
project.add_maintainer(maintainer)
project.add_developer(developer)
end
@@ -2128,7 +2149,7 @@ describe NotificationService, :mailer do
end
it_behaves_like 'sends notification only to a maximum of ten, most recently active project maintainers' do
- let(:project) { create(:project, :public, :access_requestable, namespace: group) }
+ let(:project) { create(:project, :public, namespace: group) }
let(:notification_trigger) { project.request_access(added_user) }
end
end
diff --git a/spec/services/projects/after_import_service_spec.rb b/spec/services/projects/after_import_service_spec.rb
index 51d3fd18881..27e8f3c45ba 100644
--- a/spec/services/projects/after_import_service_spec.rb
+++ b/spec/services/projects/after_import_service_spec.rb
@@ -19,6 +19,8 @@ describe Projects::AfterImportService do
allow(housekeeping_service)
.to receive(:execute).and_yield
+
+ expect(housekeeping_service).to receive(:increment!)
end
it 'performs housekeeping' do
diff --git a/spec/services/projects/container_repository/cleanup_tags_service_spec.rb b/spec/services/projects/container_repository/cleanup_tags_service_spec.rb
index 14247f1c71e..14772d172e8 100644
--- a/spec/services/projects/container_repository/cleanup_tags_service_spec.rb
+++ b/spec/services/projects/container_repository/cleanup_tags_service_spec.rb
@@ -157,6 +157,6 @@ describe Projects::ContainerRepository::CleanupTagsService do
def expect_delete(digest)
expect_any_instance_of(ContainerRegistry::Client)
.to receive(:delete_repository_tag)
- .with(repository.path, digest)
+ .with(repository.path, digest) { true }
end
end
diff --git a/spec/services/projects/container_repository/delete_tags_service_spec.rb b/spec/services/projects/container_repository/delete_tags_service_spec.rb
new file mode 100644
index 00000000000..f296ef3a776
--- /dev/null
+++ b/spec/services/projects/container_repository/delete_tags_service_spec.rb
@@ -0,0 +1,135 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Projects::ContainerRepository::DeleteTagsService do
+ set(:user) { create(:user) }
+ set(:project) { create(:project, :private) }
+ set(:repository) { create(:container_repository, :root, project: project) }
+
+ let(:params) { { tags: tags } }
+ let(:service) { described_class.new(project, user, params) }
+
+ before do
+ stub_container_registry_config(enabled: true,
+ api_url: 'http://registry.gitlab',
+ host_port: 'registry.gitlab')
+
+ stub_container_registry_tags(
+ repository: repository.path,
+ tags: %w(latest A Ba Bb C D E))
+
+ stub_tag_digest('latest', 'sha256:configA')
+ stub_tag_digest('A', 'sha256:configA')
+ stub_tag_digest('Ba', 'sha256:configB')
+ end
+
+ describe '#execute' do
+ let(:tags) { %w[A] }
+ subject { service.execute(repository) }
+
+ context 'without permissions' do
+ it { is_expected.to include(status: :error) }
+ end
+
+ context 'with permissions' do
+ before do
+ project.add_developer(user)
+ end
+
+ context 'when no params are specified' do
+ let(:params) { {} }
+
+ it 'does not remove anything' do
+ expect_any_instance_of(ContainerRegistry::Client).not_to receive(:delete_repository_tag)
+
+ is_expected.to include(status: :error)
+ end
+ end
+
+ context 'with empty tags' do
+ let(:tags) { [] }
+
+ it 'does not remove anything' do
+ expect_any_instance_of(ContainerRegistry::Client).not_to receive(:delete_repository_tag)
+
+ is_expected.to include(status: :error)
+ end
+ end
+
+ context 'with dummy tags disabled' do
+ let(:tags) { %w[A Ba] }
+
+ before do
+ stub_feature_flags(container_registry_smart_delete: false)
+ end
+
+ it 'deletes tags one by one' do
+ expect_delete_tag('sha256:configA')
+ expect_delete_tag('sha256:configB')
+ is_expected.to include(status: :success)
+ end
+ end
+
+ context 'with dummy tags enabled' do
+ let(:tags) { %w[A Ba] }
+
+ it 'deletes the tags using a dummy image' do
+ stub_upload("{\n \"config\": {\n }\n}", 'sha256:4435000728ee66e6a80e55637fc22725c256b61de344a2ecdeaac6bdb36e8bc3')
+
+ stub_request(:put, "http://registry.gitlab/v2/#{repository.path}/manifests/A")
+ .to_return(status: 200, body: "", headers: { 'docker-content-digest' => 'sha256:dummy' })
+
+ stub_request(:put, "http://registry.gitlab/v2/#{repository.path}/manifests/Ba")
+ .to_return(status: 200, body: "", headers: { 'docker-content-digest' => 'sha256:dummy' })
+
+ expect_delete_tag('sha256:dummy')
+
+ is_expected.to include(status: :success)
+ end
+
+ it 'succedes when tag delete returns 404' do
+ stub_upload("{\n \"config\": {\n }\n}", 'sha256:4435000728ee66e6a80e55637fc22725c256b61de344a2ecdeaac6bdb36e8bc3')
+
+ stub_request(:put, "http://registry.gitlab/v2/#{repository.path}/manifests/A")
+ .to_return(status: 200, body: "", headers: { 'docker-content-digest' => 'sha256:dummy' })
+
+ stub_request(:put, "http://registry.gitlab/v2/#{repository.path}/manifests/Ba")
+ .to_return(status: 200, body: "", headers: { 'docker-content-digest' => 'sha256:dummy' })
+
+ stub_request(:delete, "http://registry.gitlab/v2/#{repository.path}/manifests/sha256:dummy")
+ .to_return(status: 404, body: "", headers: {})
+
+ is_expected.to include(status: :success)
+ end
+ end
+ end
+ end
+
+ private
+
+ def stub_tag_digest(tag, digest)
+ stub_request(:head, "http://registry.gitlab/v2/#{repository.path}/manifests/#{tag}")
+ .to_return(status: 200, body: "", headers: { 'docker-content-digest' => digest })
+ end
+
+ def stub_digest_config(digest, created_at)
+ allow_any_instance_of(ContainerRegistry::Client)
+ .to receive(:blob)
+ .with(repository.path, digest, nil) do
+ { 'created' => created_at.to_datetime.rfc3339 }.to_json if created_at
+ end
+ end
+
+ def stub_upload(content, digest)
+ expect_any_instance_of(ContainerRegistry::Client)
+ .to receive(:upload_blob)
+ .with(repository.path, content, digest) { double(success?: true ) }
+ end
+
+ def expect_delete_tag(digest)
+ expect_any_instance_of(ContainerRegistry::Client)
+ .to receive(:delete_repository_tag)
+ .with(repository.path, digest) { true }
+ end
+end
diff --git a/spec/services/projects/destroy_service_spec.rb b/spec/services/projects/destroy_service_spec.rb
index 9a6f64b825a..2331281bd8e 100644
--- a/spec/services/projects/destroy_service_spec.rb
+++ b/spec/services/projects/destroy_service_spec.rb
@@ -24,8 +24,8 @@ describe Projects::DestroyService do
it 'deletes the project' do
expect(Project.unscoped.all).not_to include(project)
- expect(project.gitlab_shell.exists?(project.repository_storage, path + '.git')).to be_falsey
- expect(project.gitlab_shell.exists?(project.repository_storage, remove_path + '.git')).to be_falsey
+ expect(project.gitlab_shell.repository_exists?(project.repository_storage, path + '.git')).to be_falsey
+ expect(project.gitlab_shell.repository_exists?(project.repository_storage, remove_path + '.git')).to be_falsey
end
end
diff --git a/spec/services/projects/fork_service_spec.rb b/spec/services/projects/fork_service_spec.rb
index b759830d603..7e7e80ca240 100644
--- a/spec/services/projects/fork_service_spec.rb
+++ b/spec/services/projects/fork_service_spec.rb
@@ -50,6 +50,7 @@ describe Projects::ForkService do
it { expect(to_project.star_count).to be_zero }
it { expect(to_project.description).to eq(@from_project.description) }
it { expect(to_project.avatar.file).to be_exists }
+ it { expect(to_project.ci_config_path).to eq(@from_project.ci_config_path) }
# This test is here because we had a bug where the from-project lost its
# avatar after being forked.
@@ -215,7 +216,8 @@ describe Projects::ForkService do
@project = create(:project, :repository,
creator_id: @group_owner.id,
star_count: 777,
- description: 'Wow, such a cool project!')
+ description: 'Wow, such a cool project!',
+ ci_config_path: 'debian/salsa-ci.yml')
@group = create(:group)
@group.add_user(@group_owner, GroupMember::OWNER)
@group.add_user(@developer, GroupMember::DEVELOPER)
@@ -228,14 +230,15 @@ describe Projects::ForkService do
it 'group owner successfully forks project into the group' do
to_project = fork_project(@project, @group_owner, @opts)
- expect(to_project).to be_persisted
- expect(to_project.errors).to be_empty
- expect(to_project.owner).to eq(@group)
- expect(to_project.namespace).to eq(@group)
- expect(to_project.name).to eq(@project.name)
- expect(to_project.path).to eq(@project.path)
- expect(to_project.description).to eq(@project.description)
- expect(to_project.star_count).to be_zero
+ expect(to_project).to be_persisted
+ expect(to_project.errors).to be_empty
+ expect(to_project.owner).to eq(@group)
+ expect(to_project.namespace).to eq(@group)
+ expect(to_project.name).to eq(@project.name)
+ expect(to_project.path).to eq(@project.path)
+ expect(to_project.description).to eq(@project.description)
+ expect(to_project.ci_config_path).to eq(@project.ci_config_path)
+ expect(to_project.star_count).to be_zero
end
end
diff --git a/spec/services/projects/hashed_storage/migrate_repository_service_spec.rb b/spec/services/projects/hashed_storage/migrate_repository_service_spec.rb
index 5b778f16b5a..70785c606a5 100644
--- a/spec/services/projects/hashed_storage/migrate_repository_service_spec.rb
+++ b/spec/services/projects/hashed_storage/migrate_repository_service_spec.rb
@@ -48,8 +48,8 @@ describe Projects::HashedStorage::MigrateRepositoryService do
it 'renames project and wiki repositories' do
service.execute
- expect(gitlab_shell.exists?(project.repository_storage, "#{new_disk_path}.git")).to be_truthy
- expect(gitlab_shell.exists?(project.repository_storage, "#{new_disk_path}.wiki.git")).to be_truthy
+ expect(gitlab_shell.repository_exists?(project.repository_storage, "#{new_disk_path}.git")).to be_truthy
+ expect(gitlab_shell.repository_exists?(project.repository_storage, "#{new_disk_path}.wiki.git")).to be_truthy
end
it 'updates project to be hashed and not read-only' do
@@ -84,14 +84,13 @@ describe Projects::HashedStorage::MigrateRepositoryService do
service.execute
- expect(gitlab_shell.exists?(project.repository_storage, "#{new_disk_path}.git")).to be_falsey
- expect(gitlab_shell.exists?(project.repository_storage, "#{new_disk_path}.wiki.git")).to be_falsey
+ expect(gitlab_shell.repository_exists?(project.repository_storage, "#{new_disk_path}.git")).to be_falsey
+ expect(gitlab_shell.repository_exists?(project.repository_storage, "#{new_disk_path}.wiki.git")).to be_falsey
expect(project.repository_read_only?).to be_falsey
end
context 'when rollback fails' do
before do
- hashed_storage.ensure_storage_path_exists
gitlab_shell.mv_repository(project.repository_storage, old_disk_path, new_disk_path)
end
diff --git a/spec/services/projects/hashed_storage/rollback_repository_service_spec.rb b/spec/services/projects/hashed_storage/rollback_repository_service_spec.rb
index bd4354a7df3..3ca9ee5bee5 100644
--- a/spec/services/projects/hashed_storage/rollback_repository_service_spec.rb
+++ b/spec/services/projects/hashed_storage/rollback_repository_service_spec.rb
@@ -48,8 +48,8 @@ describe Projects::HashedStorage::RollbackRepositoryService, :clean_gitlab_redis
it 'renames project and wiki repositories' do
service.execute
- expect(gitlab_shell.exists?(project.repository_storage, "#{new_disk_path}.git")).to be_truthy
- expect(gitlab_shell.exists?(project.repository_storage, "#{new_disk_path}.wiki.git")).to be_truthy
+ expect(gitlab_shell.repository_exists?(project.repository_storage, "#{new_disk_path}.git")).to be_truthy
+ expect(gitlab_shell.repository_exists?(project.repository_storage, "#{new_disk_path}.wiki.git")).to be_truthy
end
it 'updates project to be legacy and not read-only' do
@@ -84,14 +84,13 @@ describe Projects::HashedStorage::RollbackRepositoryService, :clean_gitlab_redis
service.execute
- expect(gitlab_shell.exists?(project.repository_storage, "#{new_disk_path}.git")).to be_falsey
- expect(gitlab_shell.exists?(project.repository_storage, "#{new_disk_path}.wiki.git")).to be_falsey
+ expect(gitlab_shell.repository_exists?(project.repository_storage, "#{new_disk_path}.git")).to be_falsey
+ expect(gitlab_shell.repository_exists?(project.repository_storage, "#{new_disk_path}.wiki.git")).to be_falsey
expect(project.repository_read_only?).to be_falsey
end
context 'when rollback fails' do
before do
- legacy_storage.ensure_storage_path_exists
gitlab_shell.mv_repository(project.repository_storage, old_disk_path, new_disk_path)
end
diff --git a/spec/services/projects/housekeeping_service_spec.rb b/spec/services/projects/housekeeping_service_spec.rb
index f651db70cbd..c99054d9fd5 100644
--- a/spec/services/projects/housekeeping_service_spec.rb
+++ b/spec/services/projects/housekeeping_service_spec.rb
@@ -4,6 +4,7 @@ require 'spec_helper'
describe Projects::HousekeepingService do
subject { described_class.new(project) }
+
set(:project) { create(:project, :repository) }
before do
diff --git a/spec/services/projects/import_export/export_service_spec.rb b/spec/services/projects/import_export/export_service_spec.rb
index 404bb55629a..146d656c909 100644
--- a/spec/services/projects/import_export/export_service_spec.rb
+++ b/spec/services/projects/import_export/export_service_spec.rb
@@ -35,20 +35,27 @@ describe Projects::ImportExport::ExportService do
end
it 'saves the repo' do
+ # This spec errors when run against the EE codebase as there will be a third repository
+ # saved (the EE-specific design repository).
+ #
+ # Instead, skip this test when run within EE. There is a spec for the EE-specific design repo
+ # in the corresponding EE spec.
+ skip if Gitlab.ee?
+
# once for the normal repo, once for the wiki
expect(Gitlab::ImportExport::RepoSaver).to receive(:new).twice.and_call_original
service.execute
end
- it 'saves the lfs objects' do
- expect(Gitlab::ImportExport::LfsSaver).to receive(:new).and_call_original
+ it 'saves the wiki repo' do
+ expect(Gitlab::ImportExport::WikiRepoSaver).to receive(:new).and_call_original
service.execute
end
- it 'saves the wiki repo' do
- expect(Gitlab::ImportExport::WikiRepoSaver).to receive(:new).and_call_original
+ it 'saves the lfs objects' do
+ expect(Gitlab::ImportExport::LfsSaver).to receive(:new).and_call_original
service.execute
end
@@ -98,9 +105,9 @@ describe Projects::ImportExport::ExportService do
end
end
- context 'when saver services fail' do
+ context 'when saving services fail' do
before do
- allow(service).to receive(:save_services).and_return(false)
+ allow(service).to receive(:save_exporters).and_return(false)
end
after do
@@ -122,7 +129,7 @@ describe Projects::ImportExport::ExportService do
expect(Rails.logger).to receive(:error)
end
- it 'the after export strategy is not called' do
+ it 'does not call the export strategy' do
expect(service).not_to receive(:execute_after_export_action)
end
end
diff --git a/spec/services/projects/operations/update_service_spec.rb b/spec/services/projects/operations/update_service_spec.rb
index 7e765659b9d..b2f9fd6df79 100644
--- a/spec/services/projects/operations/update_service_spec.rb
+++ b/spec/services/projects/operations/update_service_spec.rb
@@ -170,5 +170,61 @@ describe Projects::Operations::UpdateService do
expect(project.reload.name).to eq(original_name)
end
end
+
+ context 'grafana integration' do
+ let(:params) do
+ {
+ grafana_integration_attributes: {
+ grafana_url: 'http://new.grafana.com',
+ token: 'VerySecureToken='
+ }
+ }
+ end
+
+ context 'without existing grafana integration' do
+ it 'creates an integration' do
+ expect(result[:status]).to eq(:success)
+
+ expected_attrs = params[:grafana_integration_attributes]
+ integration = project.reload.grafana_integration
+
+ expect(integration.grafana_url).to eq(expected_attrs[:grafana_url])
+ expect(integration.token).to eq(expected_attrs[:token])
+ end
+ end
+
+ context 'with an existing grafana integration' do
+ before do
+ create(:grafana_integration, project: project)
+ end
+
+ it 'updates the settings' do
+ expect(result[:status]).to eq(:success)
+
+ expected_attrs = params[:grafana_integration_attributes]
+ integration = project.reload.grafana_integration
+
+ expect(integration.grafana_url).to eq(expected_attrs[:grafana_url])
+ expect(integration.token).to eq(expected_attrs[:token])
+ end
+
+ context 'with all grafana attributes blank in params' do
+ let(:params) do
+ {
+ grafana_integration_attributes: {
+ grafana_url: '',
+ token: ''
+ }
+ }
+ end
+
+ it 'destroys the metrics_setting entry in DB' do
+ expect(result[:status]).to eq(:success)
+
+ expect(project.reload.grafana_integration).to be_nil
+ end
+ end
+ end
+ end
end
end
diff --git a/spec/services/projects/transfer_service_spec.rb b/spec/services/projects/transfer_service_spec.rb
index 6b906f9372c..26d8ac9b479 100644
--- a/spec/services/projects/transfer_service_spec.rb
+++ b/spec/services/projects/transfer_service_spec.rb
@@ -103,7 +103,7 @@ describe Projects::TransferService do
it 'rolls back repo location' do
attempt_project_transfer
- expect(gitlab_shell.exists?(project.repository_storage, "#{project.disk_path}.git")).to be(true)
+ expect(gitlab_shell.repository_exists?(project.repository_storage, "#{project.disk_path}.git")).to be(true)
expect(original_path).to eq current_path
end
diff --git a/spec/services/projects/update_pages_service_spec.rb b/spec/services/projects/update_pages_service_spec.rb
index b597717c347..fe92b53cd91 100644
--- a/spec/services/projects/update_pages_service_spec.rb
+++ b/spec/services/projects/update_pages_service_spec.rb
@@ -40,6 +40,7 @@ describe Projects::UpdatePagesService do
it "doesn't delete artifacts after deploying" do
expect(execute).to eq(:success)
+ expect(project.pages_metadatum).to be_deployed
expect(build.artifacts?).to eq(true)
end
end
@@ -47,6 +48,7 @@ describe Projects::UpdatePagesService do
it 'succeeds' do
expect(project.pages_deployed?).to be_falsey
expect(execute).to eq(:success)
+ expect(project.pages_metadatum).to be_deployed
expect(project.pages_deployed?).to be_truthy
# Check that all expected files are extracted
@@ -63,16 +65,23 @@ describe Projects::UpdatePagesService do
it 'removes pages after destroy' do
expect(PagesWorker).to receive(:perform_in)
expect(project.pages_deployed?).to be_falsey
+
expect(execute).to eq(:success)
+
+ expect(project.pages_metadatum).to be_deployed
expect(project.pages_deployed?).to be_truthy
+
project.destroy
+
expect(project.pages_deployed?).to be_falsey
+ expect(ProjectPagesMetadatum.find_by_project_id(project)).to be_nil
end
it 'fails if sha on branch is not latest' do
build.update(ref: 'feature')
expect(execute).not_to eq(:success)
+ expect(project.pages_metadatum).not_to be_deployed
end
context 'when using empty file' do
@@ -94,6 +103,7 @@ describe Projects::UpdatePagesService do
it 'succeeds to extract' do
expect(execute).to eq(:success)
+ expect(project.pages_metadatum).to be_deployed
end
end
end
@@ -109,6 +119,7 @@ describe Projects::UpdatePagesService do
build.reload
expect(deploy_status).to be_failed
+ expect(project.pages_metadatum).not_to be_deployed
end
end
@@ -125,6 +136,7 @@ describe Projects::UpdatePagesService do
build.reload
expect(deploy_status).to be_failed
+ expect(project.pages_metadatum).not_to be_deployed
end
end
@@ -138,6 +150,7 @@ describe Projects::UpdatePagesService do
build.reload
expect(deploy_status).to be_failed
+ expect(project.pages_metadatum).not_to be_deployed
end
end
end
@@ -179,6 +192,7 @@ describe Projects::UpdatePagesService do
expect(deploy_status.description)
.to match(/artifacts for pages are too large/)
expect(deploy_status).to be_script_failure
+ expect(project.pages_metadatum).not_to be_deployed
end
end
@@ -196,6 +210,7 @@ describe Projects::UpdatePagesService do
subject.execute
expect(deploy_status.description).not_to be_present
+ expect(project.pages_metadatum).to be_deployed
end
end
diff --git a/spec/services/quick_actions/interpret_service_spec.rb b/spec/services/quick_actions/interpret_service_spec.rb
index b65ee16c189..788f83cc233 100644
--- a/spec/services/quick_actions/interpret_service_spec.rb
+++ b/spec/services/quick_actions/interpret_service_spec.rb
@@ -586,6 +586,22 @@ describe QuickActions::InterpretService do
expect(message).to eq('Made this issue confidential.')
end
+
+ context 'when issuable is already confidential' do
+ before do
+ issuable.update(confidential: true)
+ end
+
+ it 'does not return the success message' do
+ _, _, message = service.execute(content, issuable)
+
+ expect(message).to be_empty
+ end
+
+ it 'is not part of the available commands' do
+ expect(service.available_commands(issuable)).not_to include(a_hash_including(name: :confidential))
+ end
+ end
end
shared_examples 'shrug command' do
@@ -1529,12 +1545,20 @@ describe QuickActions::InterpretService do
end
it 'limits to commands passed ' do
- content = "/shrug\n/close"
+ content = "/shrug test\n/close"
text, commands = service.execute(content, issue, only: [:shrug])
expect(commands).to be_empty
- expect(text).to eq("#{described_class::SHRUG}\n/close")
+ expect(text).to eq("test #{described_class::SHRUG}\n/close")
+ end
+
+ it 'preserves leading whitespace ' do
+ content = " - list\n\n/close\n\ntest\n\n"
+
+ text, _ = service.execute(content, issue)
+
+ expect(text).to eq(" - list\n\ntest")
end
context '/create_merge_request command' do
diff --git a/spec/services/spam_service_spec.rb b/spec/services/spam_service_spec.rb
index b9e5e844c1f..76f77583612 100644
--- a/spec/services/spam_service_spec.rb
+++ b/spec/services/spam_service_spec.rb
@@ -44,30 +44,50 @@ describe SpamService do
end
context 'when indicated as spam by akismet' do
+ shared_examples 'akismet spam' do
+ it 'doesnt check as spam when request is missing' do
+ check_spam(issue, nil, false)
+
+ expect(issue).not_to be_spam
+ end
+
+ it 'creates a spam log' do
+ expect { check_spam(issue, request, false) }
+ .to log_spam(title: issue.title, description: issue.description, noteable_type: 'Issue')
+ end
+
+ it 'does not yield to the block' do
+ expect(check_spam(issue, request, false))
+ .to eql(SpamLog.last)
+ end
+ end
+
before do
allow(AkismetService).to receive(:new).and_return(double(spam?: true))
end
- it 'doesnt check as spam when request is missing' do
- check_spam(issue, nil, false)
+ context 'when allow_possible_spam feature flag is false' do
+ before do
+ stub_feature_flags(allow_possible_spam: false)
+ end
- expect(issue.spam).to be_falsey
- end
+ it_behaves_like 'akismet spam'
- it 'checks as spam' do
- check_spam(issue, request, false)
+ it 'checks as spam' do
+ check_spam(issue, request, false)
- expect(issue.spam).to be_truthy
+ expect(issue.spam).to be_truthy
+ end
end
- it 'creates a spam log' do
- expect { check_spam(issue, request, false) }
- .to change { SpamLog.count }.from(0).to(1)
- end
+ context 'when allow_possible_spam feature flag is true' do
+ it_behaves_like 'akismet spam'
+
+ it 'does not check as spam' do
+ check_spam(issue, request, false)
- it 'doesnt yield block' do
- expect(check_spam(issue, request, false))
- .to eql(SpamLog.last)
+ expect(issue.spam).to be_nil
+ end
end
end
diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb
index 910fe3b50b7..83101add724 100644
--- a/spec/services/system_note_service_spec.rb
+++ b/spec/services/system_note_service_spec.rb
@@ -3,7 +3,6 @@
require 'spec_helper'
describe SystemNoteService do
- include ProjectForksHelper
include Gitlab::Routing
include RepoHelpers
include AssetsHelpers
@@ -14,270 +13,65 @@ describe SystemNoteService do
let(:noteable) { create(:issue, project: project) }
let(:issue) { noteable }
- shared_examples_for 'a note with overridable created_at' do
- let(:noteable) { create(:issue, project: project, system_note_timestamp: Time.at(42)) }
-
- it 'the note has the correct time' do
- expect(subject.created_at).to eq Time.at(42)
- end
- end
-
- shared_examples_for 'a system note' do
- let(:expected_noteable) { noteable }
- let(:commit_count) { nil }
-
- it 'has the correct attributes', :aggregate_failures do
- expect(subject).to be_valid
- expect(subject).to be_system
-
- expect(subject.noteable).to eq expected_noteable
- expect(subject.project).to eq project
- expect(subject.author).to eq author
-
- expect(subject.system_note_metadata.action).to eq(action)
- expect(subject.system_note_metadata.commit_count).to eq(commit_count)
- end
- end
-
describe '.add_commits' do
- subject { described_class.add_commits(noteable, project, author, new_commits, old_commits, oldrev) }
-
- let(:noteable) { create(:merge_request, source_project: project, target_project: project) }
- let(:new_commits) { noteable.commits }
- let(:old_commits) { [] }
- let(:oldrev) { nil }
-
- it_behaves_like 'a system note' do
- let(:commit_count) { new_commits.size }
- let(:action) { 'commit' }
- end
-
- describe 'note body' do
- let(:note_lines) { subject.note.split("\n").reject(&:blank?) }
-
- describe 'comparison diff link line' do
- it 'adds the comparison text' do
- expect(note_lines[2]).to match "[Compare with previous version]"
- end
- end
-
- context 'without existing commits' do
- it 'adds a message header' do
- expect(note_lines[0]).to eq "added #{new_commits.size} commits"
- end
+ let(:new_commits) { double }
+ let(:old_commits) { double }
+ let(:oldrev) { double }
- it 'adds a message for each commit' do
- decoded_note_content = HTMLEntities.new.decode(subject.note)
-
- new_commits.each do |commit|
- expect(decoded_note_content).to include("<li>#{commit.short_id} - #{commit.title}</li>")
- end
- end
+ it 'calls CommitService' do
+ expect_next_instance_of(::SystemNotes::CommitService) do |service|
+ expect(service).to receive(:add_commits).with(new_commits, old_commits, oldrev)
end
- describe 'summary line for existing commits' do
- let(:summary_line) { note_lines[1] }
-
- context 'with one existing commit' do
- let(:old_commits) { [noteable.commits.last] }
-
- it 'includes the existing commit' do
- expect(summary_line).to start_with("<ul><li>#{old_commits.first.short_id} - 1 commit from branch <code>feature</code>")
- end
- end
-
- context 'with multiple existing commits' do
- let(:old_commits) { noteable.commits[3..-1] }
-
- context 'with oldrev' do
- let(:oldrev) { noteable.commits[2].id }
-
- it 'includes a commit range and count' do
- expect(summary_line)
- .to start_with("<ul><li>#{Commit.truncate_sha(oldrev)}...#{old_commits.last.short_id} - 26 commits from branch <code>feature</code>")
- end
- end
-
- context 'without oldrev' do
- it 'includes a commit range and count' do
- expect(summary_line)
- .to start_with("<ul><li>#{old_commits[0].short_id}..#{old_commits[-1].short_id} - 26 commits from branch <code>feature</code>")
- end
- end
-
- context 'on a fork' do
- before do
- expect(noteable).to receive(:for_fork?).and_return(true)
- end
-
- it 'includes the project namespace' do
- expect(summary_line).to include("<code>#{noteable.target_project_namespace}:feature</code>")
- end
- end
- end
- end
+ described_class.add_commits(noteable, project, author, new_commits, old_commits, oldrev)
end
end
describe '.tag_commit' do
- let(:noteable) do
- project.commit
- end
- let(:tag_name) { 'v1.2.3' }
-
- subject { described_class.tag_commit(noteable, project, author, tag_name) }
-
- it_behaves_like 'a system note' do
- let(:action) { 'tag' }
- end
+ let(:tag_name) { double }
- it 'sets the note text' do
- link = "/#{project.full_path}/-/tags/#{tag_name}"
+ it 'calls CommitService' do
+ expect_next_instance_of(::SystemNotes::CommitService) do |service|
+ expect(service).to receive(:tag_commit).with(tag_name)
+ end
- expect(subject.note).to eq "tagged commit #{noteable.sha} to [`#{tag_name}`](#{link})"
+ described_class.tag_commit(noteable, project, author, tag_name)
end
end
describe '.change_assignee' do
- subject { described_class.change_assignee(noteable, project, author, assignee) }
-
- let(:assignee) { create(:user) }
+ let(:assignee) { double }
- it_behaves_like 'a system note' do
- let(:action) { 'assignee' }
- end
-
- context 'when assignee added' do
- it_behaves_like 'a note with overridable created_at'
-
- it 'sets the note text' do
- expect(subject.note).to eq "assigned to @#{assignee.username}"
+ it 'calls IssuableService' do
+ expect_next_instance_of(::SystemNotes::IssuablesService) do |service|
+ expect(service).to receive(:change_assignee).with(assignee)
end
- end
- context 'when assignee removed' do
- let(:assignee) { nil }
-
- it_behaves_like 'a note with overridable created_at'
-
- it 'sets the note text' do
- expect(subject.note).to eq 'removed assignee'
- end
+ described_class.change_assignee(noteable, project, author, assignee)
end
end
describe '.change_issuable_assignees' do
- subject { described_class.change_issuable_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
+ let(:assignees) { [double, double] }
- def build_note(old_assignees, new_assignees)
- issue.assignees = new_assignees
- described_class.change_issuable_assignees(issue, project, author, old_assignees).note
- end
-
- it_behaves_like 'a note with overridable created_at'
-
- 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 "unassigned @#{assignee1.username}"
- 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
-
- it 'builds a correct phrase when the locale is different' do
- Gitlab::I18n.with_locale('pt-BR') do
- expect(build_note([assignee, assignee1, assignee2], [assignee3])).to eq \
- "assigned to @#{assignee3.username} and unassigned @#{assignee.username}, @#{assignee1.username}, and @#{assignee2.username}"
+ it 'calls IssuableService' do
+ expect_next_instance_of(::SystemNotes::IssuablesService) do |service|
+ expect(service).to receive(:change_issuable_assignees).with(assignees)
end
+
+ described_class.change_issuable_assignees(noteable, project, author, assignees)
end
end
describe '.change_milestone' do
- context 'for a project milestone' do
- subject { described_class.change_milestone(noteable, project, author, milestone) }
+ let(:milestone) { double }
- let(:milestone) { create(:milestone, project: project) }
-
- it_behaves_like 'a system note' do
- let(:action) { 'milestone' }
- end
-
- context 'when milestone added' do
- it 'sets the note text' do
- reference = milestone.to_reference(format: :iid)
-
- expect(subject.note).to eq "changed milestone to #{reference}"
- end
-
- it_behaves_like 'a note with overridable created_at'
- end
-
- context 'when milestone removed' do
- let(:milestone) { nil }
-
- it 'sets the note text' do
- expect(subject.note).to eq 'removed milestone'
- end
-
- it_behaves_like 'a note with overridable created_at'
- end
- end
-
- context 'for a group milestone' do
- subject { described_class.change_milestone(noteable, project, author, milestone) }
-
- let(:milestone) { create(:milestone, group: group) }
-
- it_behaves_like 'a system note' do
- let(:action) { 'milestone' }
+ it 'calls IssuableService' do
+ expect_next_instance_of(::SystemNotes::IssuablesService) do |service|
+ expect(service).to receive(:change_milestone).with(milestone)
end
- context 'when milestone added' do
- it 'sets the note text to use the milestone name' do
- expect(subject.note).to eq "changed milestone to #{milestone.to_reference(format: :name)}"
- end
-
- it_behaves_like 'a note with overridable created_at'
- end
-
- context 'when milestone removed' do
- let(:milestone) { nil }
-
- it 'sets the note text' do
- expect(subject.note).to eq 'removed milestone'
- end
-
- it_behaves_like 'a note with overridable created_at'
- end
+ described_class.change_milestone(noteable, project, author, milestone)
end
end
@@ -308,28 +102,15 @@ describe SystemNoteService do
end
describe '.change_status' do
- subject { described_class.change_status(noteable, project, author, status, source) }
-
- context 'with status reopened' do
- let(:status) { 'reopened' }
- let(:source) { nil }
+ let(:status) { double }
+ let(:source) { double }
- it_behaves_like 'a note with overridable created_at'
-
- it_behaves_like 'a system note' do
- let(:action) { 'opened' }
+ it 'calls IssuableService' do
+ expect_next_instance_of(::SystemNotes::IssuablesService) do |service|
+ expect(service).to receive(:change_status).with(status, source)
end
- end
-
- context 'with a source' do
- let(:status) { 'opened' }
- let(:source) { double('commit', gfm_reference: 'commit 123456') }
- it_behaves_like 'a note with overridable created_at'
-
- it 'sets the note text' do
- expect(subject.note).to eq "#{status} via commit 123456"
- end
+ described_class.change_status(noteable, project, author, status, source)
end
end
@@ -383,65 +164,34 @@ describe SystemNoteService do
end
describe '.change_title' do
- let(:noteable) { create(:issue, project: project, title: 'Lorem ipsum') }
+ let(:title) { double }
- subject { described_class.change_title(noteable, project, author, 'Old title') }
-
- context 'when noteable responds to `title`' do
- it_behaves_like 'a system note' do
- let(:action) { 'title' }
+ it 'calls IssuableService' do
+ expect_next_instance_of(::SystemNotes::IssuablesService) do |service|
+ expect(service).to receive(:change_title).with(title)
end
- it_behaves_like 'a note with overridable created_at'
-
- it 'sets the note text' do
- expect(subject.note)
- .to eq "changed title from **{-Old title-}** to **{+Lorem ipsum+}**"
- end
+ described_class.change_title(noteable, project, author, title)
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' }
+ it 'calls IssuableService' do
+ expect_next_instance_of(::SystemNotes::IssuablesService) do |service|
+ expect(service).to receive(:change_description)
end
- it_behaves_like 'a note with overridable created_at'
-
- it 'sets the note text' do
- expect(subject.note).to eq('changed the description')
- end
+ described_class.change_description(noteable, project, author)
end
end
describe '.change_issue_confidentiality' do
- subject { described_class.change_issue_confidentiality(noteable, project, author) }
-
- context 'issue has been made confidential' do
- before do
- noteable.update_attribute(:confidential, true)
- end
-
- it_behaves_like 'a system note' do
- let(:action) { 'confidential' }
+ it 'calls IssuableService' do
+ expect_next_instance_of(::SystemNotes::IssuablesService) do |service|
+ expect(service).to receive(:change_issue_confidentiality)
end
- it 'sets the note text' do
- expect(subject.note).to eq 'made the issue confidential'
- end
- end
-
- context 'issue has been made visible' do
- it_behaves_like 'a system note' do
- let(:action) { 'visible' }
- end
-
- it 'sets the note text' do
- expect(subject.note).to eq 'made the issue visible to everyone'
- end
+ described_class.change_issue_confidentiality(noteable, project, author)
end
end
@@ -521,295 +271,71 @@ describe SystemNoteService do
end
describe '.zoom_link_added' do
- subject { described_class.zoom_link_added(issue, project, author) }
-
- it_behaves_like 'a system note' do
- let(:action) { 'pinned_embed' }
- end
+ it 'calls ZoomService' do
+ expect_next_instance_of(::SystemNotes::ZoomService) do |service|
+ expect(service).to receive(:zoom_link_added)
+ end
- it 'sets the zoom link added note text' do
- expect(subject.note).to eq('added a Zoom call to this issue')
+ described_class.zoom_link_added(noteable, project, author)
end
end
describe '.zoom_link_removed' do
- subject { described_class.zoom_link_removed(issue, project, author) }
-
- it_behaves_like 'a system note' do
- let(:action) { 'pinned_embed' }
- end
+ it 'calls ZoomService' do
+ expect_next_instance_of(::SystemNotes::ZoomService) do |service|
+ expect(service).to receive(:zoom_link_removed)
+ end
- it 'sets the zoom link removed note text' do
- expect(subject.note).to eq('removed a Zoom call from this issue')
+ described_class.zoom_link_removed(noteable, project, author)
end
end
describe '.cross_reference' do
- subject { described_class.cross_reference(noteable, mentioner, author) }
-
- let(:mentioner) { create(:issue, project: project) }
-
- it_behaves_like 'a system note' do
- let(:action) { 'cross_reference' }
- end
-
- context 'when cross-reference disallowed' do
- before do
- expect(described_class).to receive(:cross_reference_disallowed?).and_return(true)
- end
-
- it 'returns nil' do
- expect(subject).to be_nil
- end
-
- it 'does not create a system note metadata record' do
- expect { subject }.not_to change { SystemNoteMetadata.count }
- end
- end
-
- context 'when cross-reference allowed' do
- before do
- expect(described_class).to receive(:cross_reference_disallowed?).and_return(false)
- end
+ let(:mentioner) { double }
- it_behaves_like 'a system note' do
- let(:action) { 'cross_reference' }
+ it 'calls IssuableService' do
+ expect_next_instance_of(::SystemNotes::IssuablesService) do |service|
+ expect(service).to receive(:cross_reference).with(mentioner)
end
- it_behaves_like 'a note with overridable created_at'
-
- describe 'note_body' do
- context 'cross-project' do
- let(:project2) { create(:project, :repository) }
- let(:mentioner) { create(:issue, project: project2) }
-
- context 'from Commit' do
- let(:mentioner) { project2.repository.commit }
-
- it 'references the mentioning commit' do
- expect(subject.note).to eq "mentioned in commit #{mentioner.to_reference(project)}"
- end
- end
-
- context 'from non-Commit' do
- it 'references the mentioning object' do
- expect(subject.note).to eq "mentioned in issue #{mentioner.to_reference(project)}"
- end
- end
- end
-
- context 'within the same project' do
- context 'from Commit' do
- let(:mentioner) { project.repository.commit }
-
- it 'references the mentioning commit' do
- expect(subject.note).to eq "mentioned in commit #{mentioner.to_reference}"
- end
- end
-
- context 'from non-Commit' do
- it 'references the mentioning object' do
- expect(subject.note).to eq "mentioned in issue #{mentioner.to_reference}"
- end
- end
- end
- end
+ described_class.cross_reference(double, mentioner, double)
end
end
describe '.cross_reference_disallowed?' do
- context 'when mentioner is not a MergeRequest' do
- it 'is falsey' do
- mentioner = noteable.dup
- expect(described_class.cross_reference_disallowed?(noteable, mentioner))
- .to be_falsey
- end
- end
+ let(:mentioner) { double }
- context 'when mentioner is a MergeRequest' do
- let(:mentioner) { create(:merge_request, :simple, source_project: project) }
- let(:noteable) { project.commit }
-
- it 'is truthy when noteable is in commits' do
- expect(mentioner).to receive(:commits).and_return([noteable])
- expect(described_class.cross_reference_disallowed?(noteable, mentioner))
- .to be_truthy
- end
-
- it 'is falsey when noteable is not in commits' do
- expect(mentioner).to receive(:commits).and_return([])
- expect(described_class.cross_reference_disallowed?(noteable, mentioner))
- .to be_falsey
+ it 'calls IssuableService' do
+ expect_next_instance_of(::SystemNotes::IssuablesService) do |service|
+ expect(service).to receive(:cross_reference_disallowed?).with(mentioner)
end
- end
- context 'when notable is an ExternalIssue' do
- let(:noteable) { ExternalIssue.new('EXT-1234', project) }
- it 'is truthy' do
- mentioner = noteable.dup
- expect(described_class.cross_reference_disallowed?(noteable, mentioner))
- .to be_truthy
- end
+ described_class.cross_reference_disallowed?(double, mentioner)
end
end
describe '.cross_reference_exists?' do
- let(:commit0) { project.commit }
- let(:commit1) { project.commit('HEAD~2') }
-
- context 'issue from commit' do
- before do
- # Mention issue (noteable) from commit0
- described_class.cross_reference(noteable, commit0, author)
- end
-
- it 'is truthy when already mentioned' do
- expect(described_class.cross_reference_exists?(noteable, commit0))
- .to be_truthy
- end
-
- it 'is falsey when not already mentioned' do
- expect(described_class.cross_reference_exists?(noteable, commit1))
- .to be_falsey
- end
-
- context 'legacy capitalized cross reference' do
- before do
- # Mention issue (noteable) from commit0
- system_note = described_class.cross_reference(noteable, commit0, author)
- system_note.update(note: system_note.note.capitalize)
- end
-
- it 'is truthy when already mentioned' do
- expect(described_class.cross_reference_exists?(noteable, commit0))
- .to be_truthy
- end
- end
- end
-
- context 'commit from commit' do
- before do
- # Mention commit1 from commit0
- described_class.cross_reference(commit0, commit1, author)
- end
-
- it 'is truthy when already mentioned' do
- expect(described_class.cross_reference_exists?(commit0, commit1))
- .to be_truthy
- end
+ let(:mentioner) { double }
- it 'is falsey when not already mentioned' do
- expect(described_class.cross_reference_exists?(commit1, commit0))
- .to be_falsey
+ it 'calls IssuableService' do
+ expect_next_instance_of(::SystemNotes::IssuablesService) do |service|
+ expect(service).to receive(:cross_reference_exists?).with(mentioner)
end
- context 'legacy capitalized cross reference' do
- before do
- # Mention commit1 from commit0
- system_note = described_class.cross_reference(commit0, commit1, author)
- system_note.update(note: system_note.note.capitalize)
- end
-
- it 'is truthy when already mentioned' do
- expect(described_class.cross_reference_exists?(commit0, commit1))
- .to be_truthy
- end
- end
- end
-
- context 'commit with cross-reference from fork' do
- let(:author2) { create(:project_member, :reporter, user: create(:user), project: project).user }
- let(:forked_project) { fork_project(project, author2, repository: true) }
- let(:commit2) { forked_project.commit }
-
- before do
- described_class.cross_reference(noteable, commit0, author2)
- end
-
- it 'is true when a fork mentions an external issue' do
- expect(described_class.cross_reference_exists?(noteable, commit2))
- .to be true
- end
-
- context 'legacy capitalized cross reference' do
- before do
- system_note = described_class.cross_reference(noteable, commit0, author2)
- system_note.update(note: system_note.note.capitalize)
- end
-
- it 'is true when a fork mentions an external issue' do
- expect(described_class.cross_reference_exists?(noteable, commit2))
- .to be true
- end
- end
+ described_class.cross_reference_exists?(double, mentioner)
end
end
describe '.noteable_moved' do
- let(:new_project) { create(:project) }
- let(:new_noteable) { create(:issue, project: new_project) }
-
- subject do
- described_class.noteable_moved(noteable, project, new_noteable, author, direction: direction)
- end
-
- shared_examples 'cross project mentionable' do
- include MarkupHelper
-
- it 'contains cross reference to new noteable' do
- expect(subject.note).to include cross_project_reference(new_project, new_noteable)
- end
-
- it 'mentions referenced noteable' do
- expect(subject.note).to include new_noteable.to_reference
- end
-
- it 'mentions referenced project' do
- expect(subject.note).to include new_project.full_path
- end
- end
-
- context 'moved to' do
- let(:direction) { :to }
-
- it_behaves_like 'cross project mentionable'
- it_behaves_like 'a system note' do
- let(:action) { 'moved' }
- end
+ let(:noteable_ref) { double }
+ let(:direction) { double }
- it 'notifies about noteable being moved to' do
- expect(subject.note).to match('moved to')
+ it 'calls IssuableService' do
+ expect_next_instance_of(::SystemNotes::IssuablesService) do |service|
+ expect(service).to receive(:noteable_moved).with(noteable_ref, direction)
end
- end
- context 'moved from' do
- let(:direction) { :from }
-
- it_behaves_like 'cross project mentionable'
- it_behaves_like 'a system note' do
- let(:action) { 'moved' }
- end
-
- it 'notifies about noteable being moved from' do
- expect(subject.note).to match('moved from')
- end
- end
-
- context 'invalid direction' do
- let(:direction) { :invalid }
-
- it 'raises error' do
- expect { subject }.to raise_error StandardError, /Invalid direction/
- end
- end
- end
-
- describe '.new_commit_summary' do
- it 'escapes HTML titles' do
- commit = double(title: '<pre>This is a test</pre>', short_id: '12345678')
- escaped = '&lt;pre&gt;This is a test&lt;/pre&gt;'
-
- expect(described_class.new_commit_summary([commit])).to all(match(/- #{escaped}/))
+ described_class.noteable_moved(double, double, noteable_ref, double, direction: direction)
end
end
@@ -1171,17 +697,14 @@ describe SystemNoteService do
end
describe '.change_task_status' do
- let(:noteable) { create(:issue, project: project) }
- let(:task) { double(:task, complete?: true, source: 'task') }
-
- subject { described_class.change_task_status(noteable, project, author, task) }
+ let(:new_task) { double }
- it_behaves_like 'a system note' do
- let(:action) { 'task' }
- end
+ it 'calls IssuableService' do
+ expect_next_instance_of(::SystemNotes::IssuablesService) do |service|
+ expect(service).to receive(:change_task_status).with(new_task)
+ end
- it "posts the 'marked the task as complete' system note" do
- expect(subject.note).to eq("marked the task **task** as completed")
+ described_class.change_task_status(noteable, project, author, new_task)
end
end
@@ -1259,90 +782,42 @@ describe SystemNoteService do
end
describe '.mark_duplicate_issue' do
- subject { described_class.mark_duplicate_issue(noteable, project, author, canonical_issue) }
+ let(:canonical_issue) { double }
- context 'within the same project' do
- let(:canonical_issue) { create(:issue, project: project) }
-
- it_behaves_like 'a system note' do
- let(:action) { 'duplicate' }
+ it 'calls IssuableService' do
+ expect_next_instance_of(::SystemNotes::IssuablesService) do |service|
+ expect(service).to receive(:mark_duplicate_issue).with(canonical_issue)
end
- it { expect(subject.note).to eq "marked this issue as a duplicate of #{canonical_issue.to_reference}" }
- end
-
- context 'across different projects' do
- let(:other_project) { create(:project) }
- let(:canonical_issue) { create(:issue, project: other_project) }
-
- it_behaves_like 'a system note' do
- let(:action) { 'duplicate' }
- end
-
- it { expect(subject.note).to eq "marked this issue as a duplicate of #{canonical_issue.to_reference(project)}" }
+ described_class.mark_duplicate_issue(noteable, project, author, canonical_issue)
end
end
describe '.mark_canonical_issue_of_duplicate' do
- subject { described_class.mark_canonical_issue_of_duplicate(noteable, project, author, duplicate_issue) }
+ let(:duplicate_issue) { double }
- context 'within the same project' do
- let(:duplicate_issue) { create(:issue, project: project) }
-
- it_behaves_like 'a system note' do
- let(:action) { 'duplicate' }
+ it 'calls IssuableService' do
+ expect_next_instance_of(::SystemNotes::IssuablesService) do |service|
+ expect(service).to receive(:mark_canonical_issue_of_duplicate).with(duplicate_issue)
end
- it { expect(subject.note).to eq "marked #{duplicate_issue.to_reference} as a duplicate of this issue" }
- end
-
- context 'across different projects' do
- let(:other_project) { create(:project) }
- let(:duplicate_issue) { create(:issue, project: other_project) }
-
- it_behaves_like 'a system note' do
- let(:action) { 'duplicate' }
- end
-
- it { expect(subject.note).to eq "marked #{duplicate_issue.to_reference(project)} as a duplicate of this issue" }
+ described_class.mark_canonical_issue_of_duplicate(noteable, project, author, duplicate_issue)
end
end
describe '.discussion_lock' do
- subject { described_class.discussion_lock(noteable, author) }
-
- context 'discussion unlocked' do
- it_behaves_like 'a system note' do
- let(:action) { 'unlocked' }
- end
-
- it 'creates the note text correctly' do
- [:issue, :merge_request].each do |type|
- issuable = create(type)
+ let(:issuable) { double }
- expect(described_class.discussion_lock(issuable, author).note)
- .to eq("unlocked this #{type.to_s.titleize.downcase}")
- end
- end
+ before do
+ allow(issuable).to receive(:project).and_return(double)
end
- context 'discussion locked' do
- before do
- noteable.update_attribute(:discussion_locked, true)
- end
-
- it_behaves_like 'a system note' do
- let(:action) { 'locked' }
+ it 'calls IssuableService' do
+ expect_next_instance_of(::SystemNotes::IssuablesService) do |service|
+ expect(service).to receive(:discussion_lock)
end
- it 'creates the note text correctly' do
- [:issue, :merge_request].each do |type|
- issuable = create(type, discussion_locked: true)
-
- expect(described_class.discussion_lock(issuable, author).note)
- .to eq("locked this #{type.to_s.titleize.downcase}")
- end
- end
+ described_class.discussion_lock(issuable, double)
end
end
end
diff --git a/spec/services/system_notes/base_service_spec.rb b/spec/services/system_notes/base_service_spec.rb
new file mode 100644
index 00000000000..96788b05829
--- /dev/null
+++ b/spec/services/system_notes/base_service_spec.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe SystemNotes::BaseService do
+ let(:noteable) { double }
+ let(:project) { double }
+ let(:author) { double }
+
+ let(:base_service) { described_class.new(noteable: noteable, project: project, author: author) }
+
+ describe '#noteable' do
+ subject { base_service.noteable }
+
+ it { is_expected.to eq(noteable) }
+
+ it 'returns nil if no arguments are given' do
+ instance = described_class.new
+ expect(instance.noteable).to be_nil
+ end
+ end
+
+ describe '#project' do
+ subject { base_service.project }
+
+ it { is_expected.to eq(project) }
+
+ it 'returns nil if no arguments are given' do
+ instance = described_class.new
+ expect(instance.project).to be_nil
+ end
+ end
+
+ describe '#author' do
+ subject { base_service.author }
+
+ it { is_expected.to eq(author) }
+
+ it 'returns nil if no arguments are given' do
+ instance = described_class.new
+ expect(instance.author).to be_nil
+ end
+ end
+end
diff --git a/spec/services/system_notes/commit_service_spec.rb b/spec/services/system_notes/commit_service_spec.rb
new file mode 100644
index 00000000000..4d4403be59a
--- /dev/null
+++ b/spec/services/system_notes/commit_service_spec.rb
@@ -0,0 +1,117 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe SystemNotes::CommitService do
+ set(:group) { create(:group) }
+ set(:project) { create(:project, :repository, group: group) }
+ set(:author) { create(:user) }
+
+ let(:commit_service) { described_class.new(noteable: noteable, project: project, author: author) }
+
+ describe '#add_commits' do
+ subject { commit_service.add_commits(new_commits, old_commits, oldrev) }
+
+ let(:noteable) { create(:merge_request, source_project: project, target_project: project) }
+ let(:new_commits) { noteable.commits }
+ let(:old_commits) { [] }
+ let(:oldrev) { nil }
+
+ it_behaves_like 'a system note' do
+ let(:commit_count) { new_commits.size }
+ let(:action) { 'commit' }
+ end
+
+ describe 'note body' do
+ let(:note_lines) { subject.note.split("\n").reject(&:blank?) }
+
+ describe 'comparison diff link line' do
+ it 'adds the comparison text' do
+ expect(note_lines[2]).to match "[Compare with previous version]"
+ end
+ end
+
+ context 'without existing commits' do
+ it 'adds a message header' do
+ expect(note_lines[0]).to eq "added #{new_commits.size} commits"
+ end
+
+ it 'adds a message for each commit' do
+ decoded_note_content = HTMLEntities.new.decode(subject.note)
+
+ new_commits.each do |commit|
+ expect(decoded_note_content).to include("<li>#{commit.short_id} - #{commit.title}</li>")
+ end
+ end
+ end
+
+ describe 'summary line for existing commits' do
+ let(:summary_line) { note_lines[1] }
+
+ context 'with one existing commit' do
+ let(:old_commits) { [noteable.commits.last] }
+
+ it 'includes the existing commit' do
+ expect(summary_line).to start_with("<ul><li>#{old_commits.first.short_id} - 1 commit from branch <code>feature</code>")
+ end
+ end
+
+ context 'with multiple existing commits' do
+ let(:old_commits) { noteable.commits[3..-1] }
+
+ context 'with oldrev' do
+ let(:oldrev) { noteable.commits[2].id }
+
+ it 'includes a commit range and count' do
+ expect(summary_line)
+ .to start_with("<ul><li>#{Commit.truncate_sha(oldrev)}...#{old_commits.last.short_id} - 26 commits from branch <code>feature</code>")
+ end
+ end
+
+ context 'without oldrev' do
+ it 'includes a commit range and count' do
+ expect(summary_line)
+ .to start_with("<ul><li>#{old_commits[0].short_id}..#{old_commits[-1].short_id} - 26 commits from branch <code>feature</code>")
+ end
+ end
+
+ context 'on a fork' do
+ before do
+ expect(noteable).to receive(:for_fork?).and_return(true)
+ end
+
+ it 'includes the project namespace' do
+ expect(summary_line).to include("<code>#{noteable.target_project_namespace}:feature</code>")
+ end
+ end
+ end
+ end
+ end
+ end
+
+ describe '#tag_commit' do
+ let(:noteable) { project.commit }
+ let(:tag_name) { 'v1.2.3' }
+
+ subject { commit_service.tag_commit(tag_name) }
+
+ it_behaves_like 'a system note' do
+ let(:action) { 'tag' }
+ end
+
+ it 'sets the note text' do
+ link = "/#{project.full_path}/-/tags/#{tag_name}"
+
+ expect(subject.note).to eq "tagged commit #{noteable.sha} to [`#{tag_name}`](#{link})"
+ end
+ end
+
+ describe '#new_commit_summary' do
+ it 'escapes HTML titles' do
+ commit = double(title: '<pre>This is a test</pre>', short_id: '12345678')
+ escaped = '&lt;pre&gt;This is a test&lt;/pre&gt;'
+
+ expect(described_class.new.new_commit_summary([commit])).to all(match(/- #{escaped}/))
+ end
+ end
+end
diff --git a/spec/services/system_notes/issuables_service_spec.rb b/spec/services/system_notes/issuables_service_spec.rb
new file mode 100644
index 00000000000..5023abad4cd
--- /dev/null
+++ b/spec/services/system_notes/issuables_service_spec.rb
@@ -0,0 +1,628 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe ::SystemNotes::IssuablesService do
+ include ProjectForksHelper
+
+ let_it_be(:group) { create(:group) }
+ let_it_be(:project) { create(:project, :repository, group: group) }
+ let_it_be(:author) { create(:user) }
+ let(:noteable) { create(:issue, project: project) }
+ let(:issue) { noteable }
+
+ let(:service) { described_class.new(noteable: noteable, project: project, author: author) }
+
+ describe '#change_assignee' do
+ subject { service.change_assignee(assignee) }
+
+ let(:assignee) { create(:user) }
+
+ it_behaves_like 'a system note' do
+ let(:action) { 'assignee' }
+ end
+
+ context 'when assignee added' do
+ it_behaves_like 'a note with overridable created_at'
+
+ it 'sets the note text' do
+ expect(subject.note).to eq "assigned to @#{assignee.username}"
+ end
+ end
+
+ context 'when assignee removed' do
+ let(:assignee) { nil }
+
+ it_behaves_like 'a note with overridable created_at'
+
+ it 'sets the note text' do
+ expect(subject.note).to eq 'removed assignee'
+ end
+ end
+ end
+
+ describe '#change_issuable_assignees' do
+ subject { service.change_issuable_assignees([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
+ service.change_issuable_assignees(old_assignees).note
+ end
+
+ it_behaves_like 'a note with overridable created_at'
+
+ 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 "unassigned @#{assignee1.username}"
+ 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
+
+ it 'builds a correct phrase when the locale is different' do
+ Gitlab::I18n.with_locale('pt-BR') do
+ expect(build_note([assignee, assignee1, assignee2], [assignee3])).to eq \
+ "assigned to @#{assignee3.username} and unassigned @#{assignee.username}, @#{assignee1.username}, and @#{assignee2.username}"
+ end
+ end
+ end
+
+ describe '#change_milestone' do
+ subject { service.change_milestone(milestone) }
+
+ context 'for a project milestone' do
+ let(:milestone) { create(:milestone, project: project) }
+
+ it_behaves_like 'a system note' do
+ let(:action) { 'milestone' }
+ end
+
+ context 'when milestone added' do
+ it 'sets the note text' do
+ reference = milestone.to_reference(format: :iid)
+
+ expect(subject.note).to eq "changed milestone to #{reference}"
+ end
+
+ it_behaves_like 'a note with overridable created_at'
+ end
+
+ context 'when milestone removed' do
+ let(:milestone) { nil }
+
+ it 'sets the note text' do
+ expect(subject.note).to eq 'removed milestone'
+ end
+
+ it_behaves_like 'a note with overridable created_at'
+ end
+ end
+
+ context 'for a group milestone' do
+ let(:milestone) { create(:milestone, group: group) }
+
+ it_behaves_like 'a system note' do
+ let(:action) { 'milestone' }
+ end
+
+ context 'when milestone added' do
+ it 'sets the note text to use the milestone name' do
+ expect(subject.note).to eq "changed milestone to #{milestone.to_reference(format: :name)}"
+ end
+
+ it_behaves_like 'a note with overridable created_at'
+ end
+
+ context 'when milestone removed' do
+ let(:milestone) { nil }
+
+ it 'sets the note text' do
+ expect(subject.note).to eq 'removed milestone'
+ end
+
+ it_behaves_like 'a note with overridable created_at'
+ end
+ end
+ end
+
+ describe '#change_status' do
+ subject { service.change_status(status, source) }
+
+ context 'with status reopened' do
+ let(:status) { 'reopened' }
+ let(:source) { nil }
+
+ it_behaves_like 'a note with overridable created_at'
+
+ it_behaves_like 'a system note' do
+ let(:action) { 'opened' }
+ end
+ end
+
+ context 'with a source' do
+ let(:status) { 'opened' }
+ let(:source) { double('commit', gfm_reference: 'commit 123456') }
+
+ it_behaves_like 'a note with overridable created_at'
+
+ it 'sets the note text' do
+ expect(subject.note).to eq "#{status} via commit 123456"
+ end
+ end
+ end
+
+ describe '#change_title' do
+ let(:noteable) { create(:issue, project: project, title: 'Lorem ipsum') }
+
+ subject { service.change_title('Old title') }
+
+ context 'when noteable responds to `title`' do
+ it_behaves_like 'a system note' do
+ let(:action) { 'title' }
+ end
+
+ it_behaves_like 'a note with overridable created_at'
+
+ it 'sets the note text' do
+ expect(subject.note)
+ .to eq "changed title from **{-Old title-}** to **{+Lorem ipsum+}**"
+ end
+ end
+ end
+
+ describe '#change_description' do
+ subject { service.change_description }
+
+ context 'when noteable responds to `description`' do
+ it_behaves_like 'a system note' do
+ let(:action) { 'description' }
+ end
+
+ it_behaves_like 'a note with overridable created_at'
+
+ it 'sets the note text' do
+ expect(subject.note).to eq('changed the description')
+ end
+
+ it 'associates the related description version' do
+ noteable.update!(description: 'New description')
+
+ description_version_id = subject.system_note_metadata.description_version_id
+
+ expect(description_version_id).not_to be_nil
+ expect(description_version_id).to eq(noteable.saved_description_version.id)
+ end
+ end
+ end
+
+ describe '#change_issue_confidentiality' do
+ subject { service.change_issue_confidentiality }
+
+ context 'issue has been made confidential' do
+ before do
+ noteable.update_attribute(:confidential, true)
+ end
+
+ it_behaves_like 'a system note' do
+ let(:action) { 'confidential' }
+ end
+
+ it 'sets the note text' do
+ expect(subject.note).to eq 'made the issue confidential'
+ end
+ end
+
+ context 'issue has been made visible' do
+ it_behaves_like 'a system note' do
+ let(:action) { 'visible' }
+ end
+
+ it 'sets the note text' do
+ expect(subject.note).to eq 'made the issue visible to everyone'
+ end
+ end
+ end
+
+ describe '#cross_reference' do
+ let(:service) { described_class.new(noteable: noteable, author: author) }
+
+ let(:mentioner) { create(:issue, project: project) }
+
+ subject { service.cross_reference(mentioner) }
+
+ it_behaves_like 'a system note' do
+ let(:action) { 'cross_reference' }
+ end
+
+ context 'when cross-reference disallowed' do
+ before do
+ expect_any_instance_of(described_class).to receive(:cross_reference_disallowed?).and_return(true)
+ end
+
+ it 'returns nil' do
+ expect(subject).to be_nil
+ end
+
+ it 'does not create a system note metadata record' do
+ expect { subject }.not_to change { SystemNoteMetadata.count }
+ end
+ end
+
+ context 'when cross-reference allowed' do
+ before do
+ expect_any_instance_of(described_class).to receive(:cross_reference_disallowed?).and_return(false)
+ end
+
+ it_behaves_like 'a system note' do
+ let(:action) { 'cross_reference' }
+ end
+
+ it_behaves_like 'a note with overridable created_at'
+
+ describe 'note_body' do
+ context 'cross-project' do
+ let(:project2) { create(:project, :repository) }
+ let(:mentioner) { create(:issue, project: project2) }
+
+ context 'from Commit' do
+ let(:mentioner) { project2.repository.commit }
+
+ it 'references the mentioning commit' do
+ expect(subject.note).to eq "mentioned in commit #{mentioner.to_reference(project)}"
+ end
+ end
+
+ context 'from non-Commit' do
+ it 'references the mentioning object' do
+ expect(subject.note).to eq "mentioned in issue #{mentioner.to_reference(project)}"
+ end
+ end
+ end
+
+ context 'within the same project' do
+ context 'from Commit' do
+ let(:mentioner) { project.repository.commit }
+
+ it 'references the mentioning commit' do
+ expect(subject.note).to eq "mentioned in commit #{mentioner.to_reference}"
+ end
+ end
+
+ context 'from non-Commit' do
+ it 'references the mentioning object' do
+ expect(subject.note).to eq "mentioned in issue #{mentioner.to_reference}"
+ end
+ end
+ end
+ end
+ end
+ end
+
+ describe '#cross_reference_exists?' do
+ let(:commit0) { project.commit }
+ let(:commit1) { project.commit('HEAD~2') }
+
+ context 'issue from commit' do
+ before do
+ # Mention issue (noteable) from commit0
+ service.cross_reference(commit0)
+ end
+
+ it 'is truthy when already mentioned' do
+ expect(service.cross_reference_exists?(commit0))
+ .to be_truthy
+ end
+
+ it 'is falsey when not already mentioned' do
+ expect(service.cross_reference_exists?(commit1))
+ .to be_falsey
+ end
+
+ context 'legacy capitalized cross reference' do
+ before do
+ # Mention issue (noteable) from commit0
+ system_note = service.cross_reference(commit0)
+ system_note.update(note: system_note.note.capitalize)
+ end
+
+ it 'is truthy when already mentioned' do
+ expect(service.cross_reference_exists?(commit0))
+ .to be_truthy
+ end
+ end
+ end
+
+ context 'commit from commit' do
+ let(:service) { described_class.new(noteable: commit0, author: author) }
+
+ before do
+ # Mention commit1 from commit0
+ service.cross_reference(commit1)
+ end
+
+ it 'is truthy when already mentioned' do
+ expect(service.cross_reference_exists?(commit1))
+ .to be_truthy
+ end
+
+ it 'is falsey when not already mentioned' do
+ service = described_class.new(noteable: commit1, author: author)
+
+ expect(service.cross_reference_exists?(commit0))
+ .to be_falsey
+ end
+
+ context 'legacy capitalized cross reference' do
+ before do
+ # Mention commit1 from commit0
+ system_note = service.cross_reference(commit1)
+ system_note.update(note: system_note.note.capitalize)
+ end
+
+ it 'is truthy when already mentioned' do
+ expect(service.cross_reference_exists?(commit1))
+ .to be_truthy
+ end
+ end
+ end
+
+ context 'commit with cross-reference from fork' do
+ let(:author2) { create(:project_member, :reporter, user: create(:user), project: project).user }
+ let(:forked_project) { fork_project(project, author2, repository: true) }
+ let(:commit2) { forked_project.commit }
+
+ let(:service) { described_class.new(noteable: noteable, author: author2) }
+
+ before do
+ service.cross_reference(commit0)
+ end
+
+ it 'is true when a fork mentions an external issue' do
+ expect(service.cross_reference_exists?(commit2))
+ .to be true
+ end
+
+ context 'legacy capitalized cross reference' do
+ before do
+ system_note = service.cross_reference(commit0)
+ system_note.update(note: system_note.note.capitalize)
+ end
+
+ it 'is true when a fork mentions an external issue' do
+ expect(service.cross_reference_exists?(commit2))
+ .to be true
+ end
+ end
+ end
+ end
+
+ describe '#change_task_status' do
+ let(:noteable) { create(:issue, project: project) }
+ let(:task) { double(:task, complete?: true, source: 'task') }
+
+ subject { service.change_task_status(task) }
+
+ it_behaves_like 'a system note' do
+ let(:action) { 'task' }
+ end
+
+ it "posts the 'marked the task as complete' system note" do
+ expect(subject.note).to eq("marked the task **task** as completed")
+ end
+ end
+
+ describe '#noteable_moved' do
+ let(:new_project) { create(:project) }
+ let(:new_noteable) { create(:issue, project: new_project) }
+
+ subject do
+ # service = described_class.new(noteable: noteable, project: project, author: author)
+ service.noteable_moved(new_noteable, direction)
+ end
+
+ shared_examples 'cross project mentionable' do
+ include MarkupHelper
+
+ it 'contains cross reference to new noteable' do
+ expect(subject.note).to include cross_project_reference(new_project, new_noteable)
+ end
+
+ it 'mentions referenced noteable' do
+ expect(subject.note).to include new_noteable.to_reference
+ end
+
+ it 'mentions referenced project' do
+ expect(subject.note).to include new_project.full_path
+ end
+ end
+
+ context 'moved to' do
+ let(:direction) { :to }
+
+ it_behaves_like 'cross project mentionable'
+ it_behaves_like 'a system note' do
+ let(:action) { 'moved' }
+ end
+
+ it 'notifies about noteable being moved to' do
+ expect(subject.note).to match('moved to')
+ end
+ end
+
+ context 'moved from' do
+ let(:direction) { :from }
+
+ it_behaves_like 'cross project mentionable'
+ it_behaves_like 'a system note' do
+ let(:action) { 'moved' }
+ end
+
+ it 'notifies about noteable being moved from' do
+ expect(subject.note).to match('moved from')
+ end
+ end
+
+ context 'invalid direction' do
+ let(:direction) { :invalid }
+
+ it 'raises error' do
+ expect { subject }.to raise_error StandardError, /Invalid direction/
+ end
+ end
+ end
+
+ describe '#mark_duplicate_issue' do
+ subject { service.mark_duplicate_issue(canonical_issue) }
+
+ context 'within the same project' do
+ let(:canonical_issue) { create(:issue, project: project) }
+
+ it_behaves_like 'a system note' do
+ let(:action) { 'duplicate' }
+ end
+
+ it { expect(subject.note).to eq "marked this issue as a duplicate of #{canonical_issue.to_reference}" }
+ end
+
+ context 'across different projects' do
+ let(:other_project) { create(:project) }
+ let(:canonical_issue) { create(:issue, project: other_project) }
+
+ it_behaves_like 'a system note' do
+ let(:action) { 'duplicate' }
+ end
+
+ it { expect(subject.note).to eq "marked this issue as a duplicate of #{canonical_issue.to_reference(project)}" }
+ end
+ end
+
+ describe '#mark_canonical_issue_of_duplicate' do
+ subject { service.mark_canonical_issue_of_duplicate(duplicate_issue) }
+
+ context 'within the same project' do
+ let(:duplicate_issue) { create(:issue, project: project) }
+
+ it_behaves_like 'a system note' do
+ let(:action) { 'duplicate' }
+ end
+
+ it { expect(subject.note).to eq "marked #{duplicate_issue.to_reference} as a duplicate of this issue" }
+ end
+
+ context 'across different projects' do
+ let(:other_project) { create(:project) }
+ let(:duplicate_issue) { create(:issue, project: other_project) }
+
+ it_behaves_like 'a system note' do
+ let(:action) { 'duplicate' }
+ end
+
+ it { expect(subject.note).to eq "marked #{duplicate_issue.to_reference(project)} as a duplicate of this issue" }
+ end
+ end
+
+ describe '#discussion_lock' do
+ subject { service.discussion_lock }
+
+ context 'discussion unlocked' do
+ it_behaves_like 'a system note' do
+ let(:action) { 'unlocked' }
+ end
+
+ it 'creates the note text correctly' do
+ [:issue, :merge_request].each do |type|
+ issuable = create(type)
+
+ service = described_class.new(noteable: issuable, author: author)
+ expect(service.discussion_lock.note)
+ .to eq("unlocked this #{type.to_s.titleize.downcase}")
+ end
+ end
+ end
+
+ context 'discussion locked' do
+ before do
+ noteable.update_attribute(:discussion_locked, true)
+ end
+
+ it_behaves_like 'a system note' do
+ let(:action) { 'locked' }
+ end
+
+ it 'creates the note text correctly' do
+ [:issue, :merge_request].each do |type|
+ issuable = create(type, discussion_locked: true)
+
+ service = described_class.new(noteable: issuable, author: author)
+ expect(service.discussion_lock.note)
+ .to eq("locked this #{type.to_s.titleize.downcase}")
+ end
+ end
+ end
+ end
+
+ describe '#cross_reference_disallowed?' do
+ context 'when mentioner is not a MergeRequest' do
+ it 'is falsey' do
+ mentioner = noteable.dup
+ expect(service.cross_reference_disallowed?(mentioner))
+ .to be_falsey
+ end
+ end
+
+ context 'when mentioner is a MergeRequest' do
+ let(:mentioner) { create(:merge_request, :simple, source_project: project) }
+ let(:noteable) { project.commit }
+
+ it 'is truthy when noteable is in commits' do
+ expect(mentioner).to receive(:commits).and_return([noteable])
+ expect(service.cross_reference_disallowed?(mentioner))
+ .to be_truthy
+ end
+
+ it 'is falsey when noteable is not in commits' do
+ expect(mentioner).to receive(:commits).and_return([])
+ expect(service.cross_reference_disallowed?(mentioner))
+ .to be_falsey
+ end
+ end
+
+ context 'when notable is an ExternalIssue' do
+ let(:noteable) { ExternalIssue.new('EXT-1234', project) }
+ it 'is truthy' do
+ mentioner = noteable.dup
+ expect(service.cross_reference_disallowed?(mentioner))
+ .to be_truthy
+ end
+ end
+ end
+end
diff --git a/spec/services/system_notes/zoom_service_spec.rb b/spec/services/system_notes/zoom_service_spec.rb
new file mode 100644
index 00000000000..435cdb5748e
--- /dev/null
+++ b/spec/services/system_notes/zoom_service_spec.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe ::SystemNotes::ZoomService do
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:author) { create(:user) }
+
+ let(:noteable) { create(:issue, project: project) }
+
+ let(:service) { described_class.new(noteable: noteable, project: project, author: author) }
+
+ describe '#zoom_link_added' do
+ subject { service.zoom_link_added }
+
+ it_behaves_like 'a system note' do
+ let(:action) { 'pinned_embed' }
+ end
+
+ it 'sets the zoom link added note text' do
+ expect(subject.note).to eq('added a Zoom call to this issue')
+ end
+ end
+
+ describe '#zoom_link_removed' do
+ subject { service.zoom_link_removed }
+
+ it_behaves_like 'a system note' do
+ let(:action) { 'pinned_embed' }
+ end
+
+ it 'sets the zoom link removed note text' do
+ expect(subject.note).to eq('removed a Zoom call from this issue')
+ end
+ end
+end
diff --git a/spec/services/todos/destroy/private_features_service_spec.rb b/spec/services/todos/destroy/private_features_service_spec.rb
index 7831e3a47e0..dfe9f42e8b1 100644
--- a/spec/services/todos/destroy/private_features_service_spec.rb
+++ b/spec/services/todos/destroy/private_features_service_spec.rb
@@ -27,7 +27,7 @@ describe Todos::Destroy::PrivateFeaturesService do
context 'when user_id is provided' do
subject { described_class.new(project.id, user.id).execute }
- context 'when all feaures have same visibility as the project' do
+ context 'when all features have same visibility as the project' do
it 'removes only user issue todos' do
expect { subject }.not_to change { Todo.count }
end
@@ -92,7 +92,7 @@ describe Todos::Destroy::PrivateFeaturesService do
context 'when user_id is not provided' do
subject { described_class.new(project.id).execute }
- context 'when all feaures have same visibility as the project' do
+ context 'when all features have same visibility as the project' do
it 'does not remove any todos' do
expect { subject }.not_to change { Todo.count }
end
diff --git a/spec/services/users/destroy_service_spec.rb b/spec/services/users/destroy_service_spec.rb
index 4a5f4509a7b..23a0c71175e 100644
--- a/spec/services/users/destroy_service_spec.rb
+++ b/spec/services/users/destroy_service_spec.rb
@@ -183,7 +183,7 @@ describe Users::DestroyService do
let!(:project) { create(:project, :empty_repo, :legacy_storage, namespace: user.namespace) }
it 'removes repository' do
- expect(gitlab_shell.exists?(project.repository_storage, "#{project.disk_path}.git")).to be_falsey
+ expect(gitlab_shell.repository_exists?(project.repository_storage, "#{project.disk_path}.git")).to be_falsey
end
end
@@ -191,7 +191,7 @@ describe Users::DestroyService do
let!(:project) { create(:project, :empty_repo, namespace: user.namespace) }
it 'removes repository' do
- expect(gitlab_shell.exists?(project.repository_storage, "#{project.disk_path}.git")).to be_falsey
+ expect(gitlab_shell.repository_exists?(project.repository_storage, "#{project.disk_path}.git")).to be_falsey
end
end
end
diff --git a/spec/simplecov_env.rb b/spec/simplecov_env.rb
index 82236bb4201..c5b8a6db605 100644
--- a/spec/simplecov_env.rb
+++ b/spec/simplecov_env.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'simplecov'
require 'active_support/core_ext/numeric/time'
require_relative '../lib/gitlab/utils'
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index 18bfe5a2be7..7a5e570558e 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require './spec/simplecov_env'
SimpleCovEnv.start!
@@ -10,6 +12,7 @@ require 'rspec/rails'
require 'shoulda/matchers'
require 'rspec/retry'
require 'rspec-parameterized'
+require 'test_prof/recipes/rspec/let_it_be'
rspec_profiling_is_configured =
ENV['RSPEC_PROFILING_POSTGRES_URL'].present? ||
@@ -85,9 +88,10 @@ RSpec.configure do |config|
config.include FixtureHelpers
config.include GitlabRoutingHelper
config.include StubFeatureFlags
+ config.include StubExperiments
config.include StubGitlabCalls
config.include StubGitlabData
- config.include ExpectNextInstanceOf
+ config.include NextInstanceOf
config.include TestEnv
config.include Devise::Test::ControllerHelpers, type: :controller
config.include Devise::Test::IntegrationHelpers, type: :feature
@@ -151,6 +155,17 @@ RSpec.configure do |config|
.with(:force_autodevops_on_by_default, anything)
.and_return(false)
+ # The following can be removed once Vue Issuable Sidebar
+ # is feature-complete and can be made default in place
+ # of older sidebar.
+ # See https://gitlab.com/groups/gitlab-org/-/epics/1863
+ allow(Feature).to receive(:enabled?)
+ .with(:vue_issuable_sidebar, anything)
+ .and_return(false)
+ allow(Feature).to receive(:enabled?)
+ .with(:vue_issuable_epic_sidebar, anything)
+ .and_return(false)
+
# Stub these calls due to being expensive operations
# It can be reenabled for specific tests via:
#
@@ -160,6 +175,25 @@ RSpec.configure do |config|
allow(Gitlab::Git::KeepAround).to receive(:execute)
Gitlab::ThreadMemoryCache.cache_backend.clear
+
+ # Temporary patch to force admin mode to be active by default in tests when
+ # using the feature flag :user_mode_in_session, since this will require
+ # modifying a significant number of specs to test both states for admin
+ # mode enabled / disabled.
+ #
+ # See https://gitlab.com/gitlab-org/gitlab/issues/31511
+ # See gitlab/spec/support/helpers/admin_mode_helpers.rb
+ #
+ # If it is required to have the real behaviour that an admin is signed in
+ # with normal user mode and needs to switch to admin mode, it is possible to
+ # mark such tests with the `do_not_mock_admin_mode` metadata tag, e.g:
+ #
+ # context 'some test with normal user mode', :do_not_mock_admin_mode do ... end
+ unless example.metadata[:do_not_mock_admin_mode]
+ allow_any_instance_of(Gitlab::Auth::CurrentUserMode).to receive(:admin_mode?) do |current_user_mode|
+ current_user_mode.send(:user)&.admin?
+ end
+ end
end
config.around(:example, :quarantine) do |example|
@@ -330,6 +364,10 @@ FactoryBot::SyntaxRunner.class_eval do
include RSpec::Mocks::ExampleMethods
end
+# Use FactoryBot 4.x behavior:
+# https://github.com/thoughtbot/factory_bot/blob/master/GETTING_STARTED.md#associations
+FactoryBot.use_parent_strategy = false
+
ActiveRecord::Migration.maintain_test_schema!
Shoulda::Matchers.configure do |config|
@@ -341,3 +379,6 @@ end
# Prevent Rugged from picking up local developer gitconfig.
Rugged::Settings['search_path_global'] = Rails.root.join('tmp/tests').to_s
+
+# Disable timestamp checks for invisible_captcha
+InvisibleCaptcha.timestamp_enabled = false
diff --git a/spec/support/api/boards_shared_examples.rb b/spec/support/api/boards_shared_examples.rb
index b7aff32460d..d41490f33e4 100644
--- a/spec/support/api/boards_shared_examples.rb
+++ b/spec/support/api/boards_shared_examples.rb
@@ -171,7 +171,7 @@ shared_examples_for 'group and project boards' do |route_definition, ee = false|
if board_parent.try(:namespace)
board_parent.update(namespace: owner.namespace)
else
- board.parent.add_owner(owner)
+ board.resource_parent.add_owner(owner)
end
end
diff --git a/spec/support/api/milestones_shared_examples.rb b/spec/support/api/milestones_shared_examples.rb
index d6439f77408..ce8c2140e99 100644
--- a/spec/support/api/milestones_shared_examples.rb
+++ b/spec/support/api/milestones_shared_examples.rb
@@ -205,7 +205,7 @@ shared_examples_for 'group and project milestones' do |route_definition|
describe "DELETE #{route_definition}/:milestone_id" do
it "rejects a member with reporter access from deleting a milestone" do
reporter = create(:user)
- milestone.parent.add_reporter(reporter)
+ milestone.resource_parent.add_reporter(reporter)
delete api(resource_route, reporter)
diff --git a/spec/support/controllers/githubish_import_controller_shared_examples.rb b/spec/support/controllers/githubish_import_controller_shared_examples.rb
index 718d9857b18..f23812e7149 100644
--- a/spec/support/controllers/githubish_import_controller_shared_examples.rb
+++ b/spec/support/controllers/githubish_import_controller_shared_examples.rb
@@ -139,6 +139,38 @@ shared_examples 'a GitHub-ish import controller: GET status' do
expect { get :status, format: :json }
.not_to exceed_all_query_limit(control_count)
end
+
+ context 'when filtering' do
+ let(:repo_2) { OpenStruct.new(login: 'emacs', full_name: 'asd/emacs', name: 'emacs', owner: { login: 'owner' }) }
+ let(:project) { create(:project, import_type: provider, namespace: user.namespace, import_status: :finished, import_source: 'example/repo') }
+ let(:group) { create(:group) }
+
+ before do
+ group.add_owner(user)
+ stub_client(repos: [repo, repo_2, org_repo], orgs: [org], org_repos: [org_repo])
+ end
+
+ it 'filters list of repositories by name' do
+ get :status, params: { filter: 'emacs' }, format: :json
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response.dig("imported_projects").count).to eq(0)
+ expect(json_response.dig("provider_repos").count).to eq(1)
+ expect(json_response.dig("provider_repos", 0, "id")).to eq(repo_2.id)
+ expect(json_response.dig("namespaces", 0, "id")).to eq(group.id)
+ end
+
+ context 'when user input contains html' do
+ let(:expected_filter) { 'test' }
+ let(:filter) { "<html>#{expected_filter}</html>" }
+
+ it 'sanitizes user input' do
+ get :status, params: { filter: filter }, format: :json
+
+ expect(assigns(:filter)).to eq(expected_filter)
+ end
+ end
+ end
end
shared_examples 'a GitHub-ish import controller: POST create' do
diff --git a/spec/support/features/rss_shared_examples.rb b/spec/support/features/rss_shared_examples.rb
index c97eeba87db..bbe793a81bc 100644
--- a/spec/support/features/rss_shared_examples.rb
+++ b/spec/support/features/rss_shared_examples.rb
@@ -8,7 +8,9 @@ end
shared_examples "it has an RSS button with current_user's feed token" do
it "shows the RSS button with current_user's feed token" do
- expect(page).to have_css("a:has(.fa-rss)[href*='feed_token=#{user.feed_token}']")
+ expect(page)
+ .to have_css("a:has(.fa-rss)[href*='feed_token=#{user.feed_token}']")
+ .or have_css("a.js-rss-button[href*='feed_token=#{user.feed_token}']")
end
end
@@ -20,6 +22,8 @@ end
shared_examples "it has an RSS button without a feed token" do
it "shows the RSS button without a feed token" do
- expect(page).to have_css("a:has(.fa-rss):not([href*='feed_token'])")
+ expect(page)
+ .to have_css("a:has(.fa-rss):not([href*='feed_token'])")
+ .or have_css("a.js-rss-button:not([href*='feed_token'])")
end
end
diff --git a/spec/support/google_api/cloud_platform_helpers.rb b/spec/support/google_api/cloud_platform_helpers.rb
index a1328ef0d13..38ffca8c5ae 100644
--- a/spec/support/google_api/cloud_platform_helpers.rb
+++ b/spec/support/google_api/cloud_platform_helpers.rb
@@ -65,7 +65,7 @@ module GoogleApi
end
def cloud_platform_create_cluster_url(project_id, zone)
- "https://container.googleapis.com/v1/projects/#{project_id}/zones/#{zone}/clusters"
+ "https://container.googleapis.com/v1beta1/projects/#{project_id}/zones/#{zone}/clusters"
end
def cloud_platform_get_zone_operation_url(project_id, zone, operation_id)
diff --git a/spec/support/helpers/admin_mode_helpers.rb b/spec/support/helpers/admin_mode_helpers.rb
new file mode 100644
index 00000000000..de8ffe40536
--- /dev/null
+++ b/spec/support/helpers/admin_mode_helpers.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+# Helper for enabling admin mode in tests
+
+module AdminModeHelper
+ # Users are logged in by default in user mode and have to switch to admin
+ # mode for accessing any administrative functionality. This helper lets a user
+ # be in admin mode without requiring a second authentication step (provided
+ # the user is an admin)
+ def enable_admin_mode!(user)
+ fake_user_mode = instance_double(Gitlab::Auth::CurrentUserMode)
+
+ allow(Gitlab::Auth::CurrentUserMode).to receive(:new).with(user).and_return(fake_user_mode)
+ allow(fake_user_mode).to receive(:admin_mode?).and_return(user&.admin?)
+ end
+end
diff --git a/spec/support/helpers/cycle_analytics_helpers.rb b/spec/support/helpers/cycle_analytics_helpers.rb
index 575b2e779c5..a604359942f 100644
--- a/spec/support/helpers/cycle_analytics_helpers.rb
+++ b/spec/support/helpers/cycle_analytics_helpers.rb
@@ -25,11 +25,15 @@ module CycleAnalyticsHelpers
return if skip_push_handler
- Git::BranchPushService.new(project,
- user,
- oldrev: oldrev,
- newrev: commit_shas.last,
- ref: 'refs/heads/master').execute
+ Git::BranchPushService.new(
+ project,
+ user,
+ change: {
+ oldrev: oldrev,
+ newrev: commit_shas.last,
+ ref: 'refs/heads/master'
+ }
+ ).execute
end
def create_cycle(user, project, issue, mr, milestone, pipeline)
@@ -109,6 +113,7 @@ module CycleAnalyticsHelpers
def new_dummy_job(user, project, environment)
create(:ci_build,
+ :with_deployment,
project: project,
user: user,
environment: environment,
diff --git a/spec/support/helpers/expect_next_instance_of.rb b/spec/support/helpers/expect_next_instance_of.rb
deleted file mode 100644
index 749d2cb2a56..00000000000
--- a/spec/support/helpers/expect_next_instance_of.rb
+++ /dev/null
@@ -1,15 +0,0 @@
-# frozen_string_literal: true
-
-module ExpectNextInstanceOf
- def expect_next_instance_of(klass, *new_args)
- receive_new = receive(:new)
- receive_new.with(*new_args) if new_args.any?
-
- expect(klass).to receive_new
- .and_wrap_original do |method, *original_args|
- method.call(*original_args).tap do |instance|
- yield(instance)
- end
- end
- end
-end
diff --git a/spec/support/helpers/gpg_helpers.rb b/spec/support/helpers/gpg_helpers.rb
index 03ffadc8f7f..f4df1cf601c 100644
--- a/spec/support/helpers/gpg_helpers.rb
+++ b/spec/support/helpers/gpg_helpers.rb
@@ -373,6 +373,8 @@ module GpgHelpers
KEY
end
+ # passphrase for secret key is:
+ # 4a45718624c9939a043471d83d1eda7c
def secret_key
<<~SECRET
-----BEGIN PGP PRIVATE KEY BLOCK-----
diff --git a/spec/support/helpers/group_api_helpers.rb b/spec/support/helpers/group_api_helpers.rb
new file mode 100644
index 00000000000..56c4cc121a7
--- /dev/null
+++ b/spec/support/helpers/group_api_helpers.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module GroupAPIHelpers
+ extend self
+
+ def attributes_for_group_api(params = {})
+ # project_creation_level and subgroup_creation_level are Integers in the model
+ # but are strings in the API
+ attributes_for(:group, params).except(:project_creation_level, :subgroup_creation_level)
+ end
+end
diff --git a/spec/support/helpers/javascript_fixtures_helpers.rb b/spec/support/helpers/javascript_fixtures_helpers.rb
index 7ec65318ec5..fd5ad9451f7 100644
--- a/spec/support/helpers/javascript_fixtures_helpers.rb
+++ b/spec/support/helpers/javascript_fixtures_helpers.rb
@@ -69,7 +69,7 @@ module JavaScriptFixturesHelpers
link_tags = doc.css('link')
link_tags.remove
- scripts = doc.css("script:not([type='text/template']):not([type='text/x-template'])")
+ scripts = doc.css("script:not([type='text/template']):not([type='text/x-template']):not([type='application/json'])")
scripts.remove
fixture = doc.to_html
diff --git a/spec/support/helpers/kubernetes_helpers.rb b/spec/support/helpers/kubernetes_helpers.rb
index 538a5b8ef3c..e74dbca4f93 100644
--- a/spec/support/helpers/kubernetes_helpers.rb
+++ b/spec/support/helpers/kubernetes_helpers.rb
@@ -11,6 +11,10 @@ module KubernetesHelpers
kube_response(kube_pods_body)
end
+ def kube_pod_response
+ kube_response(kube_pod)
+ end
+
def kube_logs_response
kube_response(kube_logs_body)
end
@@ -63,11 +67,30 @@ module KubernetesHelpers
WebMock.stub_request(:get, pods_url).to_return(response || kube_pods_response)
end
- def stub_kubeclient_logs(pod_name, namespace, status: nil)
+ def stub_kubeclient_pod_details(pod, namespace, status: nil)
stub_kubeclient_discover(service.api_url)
- logs_url = service.api_url + "/api/v1/namespaces/#{namespace}/pods/#{pod_name}/log?tailLines=#{Clusters::Platforms::Kubernetes::LOGS_LIMIT}"
+
+ pod_url = service.api_url + "/api/v1/namespaces/#{namespace}/pods/#{pod}"
response = { status: status } if status
+ WebMock.stub_request(:get, pod_url).to_return(response || kube_pod_response)
+ end
+
+ def stub_kubeclient_logs(pod_name, namespace, container: nil, status: nil, message: nil)
+ stub_kubeclient_discover(service.api_url)
+
+ if container
+ container_query_param = "container=#{container}&"
+ end
+
+ logs_url = service.api_url + "/api/v1/namespaces/#{namespace}/pods/#{pod_name}" \
+ "/log?#{container_query_param}tailLines=#{Clusters::Platforms::Kubernetes::LOGS_LIMIT}"
+
+ if status
+ response = { status: status }
+ response[:body] = { message: message }.to_json if message
+ end
+
WebMock.stub_request(:get, logs_url).to_return(response || kube_logs_response)
end
@@ -296,10 +319,10 @@ module KubernetesHelpers
}
end
- def kube_knative_services_body(**options)
+ def kube_knative_services_body(legacy_knative: false, **options)
{
"kind" => "List",
- "items" => [kube_service(options)]
+ "items" => [legacy_knative ? knative_05_service(options) : kube_service(options)]
}
end
@@ -387,6 +410,29 @@ module KubernetesHelpers
"generation" => 2
},
"status" => {
+ "url" => "http://#{name}.#{namespace}.#{domain}",
+ "address" => {
+ "url" => "#{name}.#{namespace}.svc.cluster.local"
+ },
+ "latestCreatedRevisionName" => "#{name}-00002",
+ "latestReadyRevisionName" => "#{name}-00002",
+ "observedGeneration" => 2
+ }
+ }
+ end
+
+ def knative_05_service(name: "kubetest", namespace: "default", domain: "example.com")
+ {
+ "metadata" => {
+ "creationTimestamp" => "2018-11-21T06:16:33Z",
+ "name" => name,
+ "namespace" => namespace,
+ "selfLink" => "/apis/serving.knative.dev/v1alpha1/namespaces/#{namespace}/services/#{name}"
+ },
+ "spec" => {
+ "generation" => 2
+ },
+ "status" => {
"domain" => "#{name}.#{namespace}.#{domain}",
"domainInternal" => "#{name}.#{namespace}.svc.cluster.local",
"latestCreatedRevisionName" => "#{name}-00002",
@@ -414,8 +460,10 @@ module KubernetesHelpers
}
},
"status" => {
- "domain" => "#{name}.#{namespace}.#{domain}",
- "domainInternal" => "#{name}.#{namespace}.svc.cluster.local",
+ "url" => "http://#{name}.#{namespace}.#{domain}",
+ "address" => {
+ "url" => "#{name}.#{namespace}.svc.cluster.local"
+ },
"latestCreatedRevisionName" => "#{name}-00002",
"latestReadyRevisionName" => "#{name}-00002",
"observedGeneration" => 2
diff --git a/spec/support/helpers/lfs_http_helpers.rb b/spec/support/helpers/lfs_http_helpers.rb
new file mode 100644
index 00000000000..0537b122040
--- /dev/null
+++ b/spec/support/helpers/lfs_http_helpers.rb
@@ -0,0 +1,62 @@
+# frozen_string_literal: true
+require_relative 'workhorse_helpers'
+
+module LfsHttpHelpers
+ include WorkhorseHelpers
+
+ def authorize_ci_project
+ ActionController::HttpAuthentication::Basic.encode_credentials('gitlab-ci-token', build.token)
+ end
+
+ def authorize_user
+ ActionController::HttpAuthentication::Basic.encode_credentials(user.username, user.password)
+ end
+
+ def authorize_deploy_key
+ Gitlab::LfsToken.new(key).basic_encoding
+ end
+
+ def authorize_user_key
+ Gitlab::LfsToken.new(user).basic_encoding
+ end
+
+ def authorize_deploy_token
+ ActionController::HttpAuthentication::Basic.encode_credentials(deploy_token.username, deploy_token.token)
+ end
+
+ def post_lfs_json(url, body = nil, headers = nil)
+ params = body.try(:to_json)
+ headers = (headers || {}).merge('Content-Type' => LfsRequest::CONTENT_TYPE)
+
+ post(url, params: params, headers: headers)
+ end
+
+ def batch_url(project)
+ "#{project.http_url_to_repo}/info/lfs/objects/batch"
+ end
+
+ def objects_url(project, oid = nil, size = nil)
+ File.join(["#{project.http_url_to_repo}/gitlab-lfs/objects", oid, size].compact.map(&:to_s))
+ end
+
+ def authorize_url(project, oid, size)
+ File.join(objects_url(project, oid, size), 'authorize')
+ end
+
+ def download_body(objects)
+ request_body('download', objects)
+ end
+
+ def upload_body(objects)
+ request_body('upload', objects)
+ end
+
+ def request_body(operation, objects)
+ objects = [objects] unless objects.is_a?(Array)
+
+ {
+ 'operation' => operation,
+ 'objects' => objects
+ }
+ end
+end
diff --git a/spec/support/helpers/login_helpers.rb b/spec/support/helpers/login_helpers.rb
index 95fcfde2d32..7d5896e4eeb 100644
--- a/spec/support/helpers/login_helpers.rb
+++ b/spec/support/helpers/login_helpers.rb
@@ -48,6 +48,14 @@ module LoginHelpers
@current_user = user
end
+ def gitlab_enable_admin_mode_sign_in(user)
+ visit new_admin_session_path
+
+ fill_in 'password', with: user.password
+
+ click_button 'Enter admin mode'
+ end
+
def gitlab_sign_in_via(provider, user, uid, saml_response = nil)
mock_auth_hash_with_saml_xml(provider, uid, user.email, saml_response)
visit new_user_session_path
diff --git a/spec/support/helpers/migrations_helpers.rb b/spec/support/helpers/migrations_helpers.rb
index 176788d0506..68f71494771 100644
--- a/spec/support/helpers/migrations_helpers.rb
+++ b/spec/support/helpers/migrations_helpers.rb
@@ -132,6 +132,41 @@ module MigrationsHelpers
migration.name == described_class.name
end
end
+
+ class ReversibleMigrationTest
+ attr_reader :before_up, :after_up
+
+ def initialize
+ @before_up = -> {}
+ @after_up = -> {}
+ end
+
+ def before(expectations)
+ @before_up = expectations
+
+ self
+ end
+
+ def after(expectations)
+ @after_up = expectations
+
+ self
+ end
+ end
+
+ def reversible_migration(&block)
+ tests = yield(ReversibleMigrationTest.new)
+
+ tests.before_up.call
+
+ migrate!
+
+ tests.after_up.call
+
+ schema_migrate_down!
+
+ tests.before_up.call
+ end
end
MigrationsHelpers.prepend_if_ee('EE::MigrationsHelpers')
diff --git a/spec/support/helpers/next_instance_of.rb b/spec/support/helpers/next_instance_of.rb
new file mode 100644
index 00000000000..83c788c3d38
--- /dev/null
+++ b/spec/support/helpers/next_instance_of.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+module NextInstanceOf
+ def expect_next_instance_of(klass, *new_args)
+ stub_new(expect(klass), *new_args) do |expectation|
+ yield(expectation)
+ end
+ end
+
+ def allow_next_instance_of(klass, *new_args)
+ stub_new(allow(klass), *new_args) do |allowance|
+ yield(allowance)
+ end
+ end
+
+ private
+
+ def stub_new(target, *new_args)
+ receive_new = receive(:new)
+ receive_new.with(*new_args) if new_args.any?
+
+ target.to receive_new.and_wrap_original do |method, *original_args|
+ method.call(*original_args).tap do |instance|
+ yield(instance)
+ end
+ end
+ end
+end
diff --git a/spec/support/helpers/rack_attack_spec_helpers.rb b/spec/support/helpers/rack_attack_spec_helpers.rb
new file mode 100644
index 00000000000..234271ba1c0
--- /dev/null
+++ b/spec/support/helpers/rack_attack_spec_helpers.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+module RackAttackSpecHelpers
+ def post_args_with_token_headers(url, token_headers)
+ [url, params: nil, headers: token_headers]
+ end
+
+ def api_get_args_with_token_headers(partial_url, token_headers)
+ ["/api/#{API::API.version}#{partial_url}", params: nil, headers: token_headers]
+ end
+
+ def rss_url(user)
+ "/dashboard/projects.atom?feed_token=#{user.feed_token}"
+ end
+
+ def private_token_headers(user)
+ { 'HTTP_PRIVATE_TOKEN' => user.private_token }
+ end
+
+ def personal_access_token_headers(personal_access_token)
+ { 'HTTP_PRIVATE_TOKEN' => personal_access_token.token }
+ end
+
+ def oauth_token_headers(oauth_access_token)
+ { 'AUTHORIZATION' => "Bearer #{oauth_access_token.token}" }
+ end
+
+ def expect_rejection(&block)
+ yield
+
+ expect(response).to have_http_status(429)
+ end
+end
diff --git a/spec/support/helpers/repo_helpers.rb b/spec/support/helpers/repo_helpers.rb
index b5defba332a..255a15b1ab0 100644
--- a/spec/support/helpers/repo_helpers.rb
+++ b/spec/support/helpers/repo_helpers.rb
@@ -92,7 +92,7 @@ eos
)
end
- def sample_compare
+ def sample_compare(extra_changes = [])
changes = [
{
line_code: 'a5cc2925ca8258af241be7e5b0381edf30266302_20_20',
@@ -102,7 +102,7 @@ eos
line_code: '7445606fbf8f3683cd42bdc54b05d7a0bc2dfc44_4_6',
file_path: '.gitmodules'
}
- ]
+ ] + extra_changes
commits = %w(
5937ac0a7beb003549fc5fd26fc247adbce4a52e
diff --git a/spec/support/helpers/select2_helper.rb b/spec/support/helpers/select2_helper.rb
index 9c42c2b0d8b..38bf34bdd61 100644
--- a/spec/support/helpers/select2_helper.rb
+++ b/spec/support/helpers/select2_helper.rb
@@ -24,7 +24,7 @@ module Select2Helper
selector = options.fetch(:from)
- first(selector, visible: false)
+ ensure_select2_loaded(selector)
if options[:multiple]
execute_script("$('#{selector}').select2('val', ['#{value}']).trigger('change');")
@@ -34,14 +34,24 @@ module Select2Helper
end
def open_select2(selector)
+ ensure_select2_loaded(selector)
+
execute_script("$('#{selector}').select2('open');")
end
def close_select2(selector)
+ ensure_select2_loaded(selector)
+
execute_script("$('#{selector}').select2('close');")
end
def scroll_select2_to_bottom(selector)
evaluate_script "$('#{selector}').scrollTop($('#{selector}')[0].scrollHeight); $('#{selector}');"
end
+
+ private
+
+ def ensure_select2_loaded(selector)
+ first(selector, visible: :all).sibling('.select2-container')
+ end
end
diff --git a/spec/support/helpers/stub_experiments.rb b/spec/support/helpers/stub_experiments.rb
new file mode 100644
index 00000000000..ed868e22c6e
--- /dev/null
+++ b/spec/support/helpers/stub_experiments.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module StubExperiments
+ # Stub Experiment with `key: true/false`
+ #
+ # @param [Hash] experiment where key is feature name and value is boolean whether enabled or not.
+ #
+ # Examples
+ # - `stub_experiment(signup_flow: false)` ... Disable `signup_flow` experiment globally.
+ def stub_experiment(experiments)
+ experiments.each do |experiment_key, enabled|
+ allow(Gitlab::Experimentation).to receive(:enabled?).with(experiment_key, any_args) { enabled }
+ end
+ end
+end
diff --git a/spec/support/helpers/test_env.rb b/spec/support/helpers/test_env.rb
index 3274651ef19..a409dd2ef26 100644
--- a/spec/support/helpers/test_env.rb
+++ b/spec/support/helpers/test_env.rb
@@ -36,6 +36,7 @@ module TestEnv
'expand-collapse-lines' => '238e82d',
'pages-deploy' => '7897d5b',
'pages-deploy-target' => '7975be0',
+ 'audio' => 'c3c21fd',
'video' => '8879059',
'add-balsamiq-file' => 'b89b56d',
'crlf-diff' => '5938907',
@@ -99,7 +100,6 @@ module TestEnv
clean_test_path
- # Set up GitLab shell for test instance
setup_gitlab_shell
setup_gitaly
@@ -144,10 +144,7 @@ module TestEnv
end
def setup_gitlab_shell
- component_timed_setup('GitLab Shell',
- install_dir: Gitlab.config.gitlab_shell.path,
- version: Gitlab::Shell.version_required,
- task: 'gitlab:shell:install')
+ FileUtils.mkdir_p(Gitlab.config.gitlab_shell.path)
end
def setup_gitaly
diff --git a/spec/support/helpers/wait_for_requests.rb b/spec/support/helpers/wait_for_requests.rb
index 30dff1063b5..d5483d0b0a7 100644
--- a/spec/support/helpers/wait_for_requests.rb
+++ b/spec/support/helpers/wait_for_requests.rb
@@ -49,11 +49,11 @@ module WaitForRequests
return true unless javascript_test?
finished_all_ajax_requests? &&
- finished_all_vue_resource_requests?
+ finished_all_axios_requests?
end
- def finished_all_vue_resource_requests?
- Capybara.page.evaluate_script('window.activeVueResources || 0').zero?
+ def finished_all_axios_requests?
+ Capybara.page.evaluate_script('window.pendingRequests || 0').zero?
end
def finished_all_ajax_requests?
diff --git a/spec/support/helpers/workhorse_helpers.rb b/spec/support/helpers/workhorse_helpers.rb
index 40007a14b85..e0fba191deb 100644
--- a/spec/support/helpers/workhorse_helpers.rb
+++ b/spec/support/helpers/workhorse_helpers.rb
@@ -22,16 +22,40 @@ module WorkhorseHelpers
# workhorse_post_with_file will transform file_key inside params as if it was disk accelerated by workhorse
def workhorse_post_with_file(url, file_key:, params:)
+ workhorse_request_with_file(:post, url,
+ file_key: file_key,
+ params: params,
+ env: { 'CONTENT_TYPE' => 'multipart/form-data' },
+ send_rewritten_field: true
+ )
+ end
+
+ # workhorse_finalize will transform file_key inside params as if it was the finalize call of an inline object storage upload.
+ # note that based on the content of the params it can simulate a disc acceleration or an object storage upload
+ def workhorse_finalize(url, method: :post, file_key:, params:, headers: {})
+ workhorse_request_with_file(method, url,
+ file_key: file_key,
+ params: params,
+ extra_headers: headers,
+ send_rewritten_field: false
+ )
+ end
+
+ def workhorse_request_with_file(method, url, file_key:, params:, env: {}, extra_headers: {}, send_rewritten_field:)
workhorse_params = params.dup
file = workhorse_params.delete(file_key)
- workhorse_params.merge!(workhorse_disk_accelerated_file_params(file_key, file))
+ workhorse_params = workhorse_disk_accelerated_file_params(file_key, file).merge(workhorse_params)
+
+ headers = if send_rewritten_field
+ workhorse_rewritten_fields_header(file_key => file.path)
+ else
+ {}
+ end
+
+ headers.merge!(extra_headers)
- post(url,
- params: workhorse_params,
- headers: workhorse_rewritten_fields_header(file_key => file.path),
- env: { 'CONTENT_TYPE' => 'multipart/form-data' }
- )
+ process(method, url, params: workhorse_params, headers: headers, env: env)
end
private
@@ -45,9 +69,24 @@ module WorkhorseHelpers
end
def workhorse_disk_accelerated_file_params(key, file)
+ return {} unless file
+
{
"#{key}.name" => file.original_filename,
- "#{key}.path" => file.path
- }
+ "#{key}.size" => file.size
+ }.tap do |params|
+ params["#{key}.path"] = file.path if file.path
+ params["#{key}.remote_id"] = file.remote_id if file.respond_to?(:remote_id) && file.remote_id
+ end
+ end
+
+ def fog_to_uploaded_file(file)
+ filename = File.basename(file.key)
+
+ UploadedFile.new(nil,
+ filename: filename,
+ remote_id: filename,
+ size: file.content_length
+ )
end
end
diff --git a/spec/support/matchers/graphql_matchers.rb b/spec/support/matchers/graphql_matchers.rb
index 4d48b4b5389..d735c10f698 100644
--- a/spec/support/matchers/graphql_matchers.rb
+++ b/spec/support/matchers/graphql_matchers.rb
@@ -28,9 +28,15 @@ RSpec::Matchers.define :have_graphql_fields do |*expected|
end
end
-RSpec::Matchers.define :have_graphql_field do |field_name|
+RSpec::Matchers.define :have_graphql_field do |field_name, args = {}|
match do |kls|
- expect(kls.fields.keys).to include(GraphqlHelpers.fieldnamerize(field_name))
+ field = kls.fields[GraphqlHelpers.fieldnamerize(field_name)]
+
+ expect(field).to be_present
+
+ args.each do |argument, value|
+ expect(field.send(argument)).to eq(value)
+ end
end
end
diff --git a/spec/support/matchers/log_spam.rb b/spec/support/matchers/log_spam.rb
new file mode 100644
index 00000000000..541cacf558c
--- /dev/null
+++ b/spec/support/matchers/log_spam.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+# This matcher checkes if one spam log with provided attributes was created
+#
+# Example:
+#
+# expect { create_issue }.to log_spam
+RSpec::Matchers.define :log_spam do |expected|
+ def spam_logs
+ SpamLog.all
+ end
+
+ match do |block|
+ block.call
+
+ expect(spam_logs).to contain_exactly(
+ have_attributes(expected)
+ )
+ end
+
+ description do
+ count = spam_logs.count
+
+ if count == 1
+ keys = expected.keys.map(&:to_s)
+ actual = spam_logs.first.attributes.slice(*keys)
+ "create a spam log with #{expected} attributes. #{actual} created instead."
+ else
+ "create exactly 1 spam log with #{expected} attributes. #{count} spam logs created instead."
+ end
+ end
+
+ supports_block_expectations
+end
diff --git a/spec/support/matchers/markdown_matchers.rb b/spec/support/matchers/markdown_matchers.rb
index 3245f8418b1..35b2993443f 100644
--- a/spec/support/matchers/markdown_matchers.rb
+++ b/spec/support/matchers/markdown_matchers.rb
@@ -193,6 +193,17 @@ module MarkdownMatchers
end
end
+ # AudioLinkFilter
+ matcher :parse_audio_links do
+ set_default_markdown_messages
+
+ match do |actual|
+ audio = actual.at_css('audio')
+
+ expect(audio['src']).to end_with('/assets/audio/gitlab-demo.wav')
+ end
+ end
+
# ColorFilter
matcher :parse_colors do
set_default_markdown_messages
diff --git a/spec/support/matchers/policy_matchers.rb b/spec/support/matchers/policy_matchers.rb
new file mode 100644
index 00000000000..020c5ce2baf
--- /dev/null
+++ b/spec/support/matchers/policy_matchers.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+RSpec::Matchers.define :allow_action do |action|
+ match do |policy|
+ expect(policy).to be_allowed(action)
+ end
+
+ failure_message do |policy|
+ policy.debug(action, debug_output = +'')
+ "expected #{policy} to allow #{action}\n\n#{debug_output}"
+ end
+
+ failure_message_when_negated do |policy|
+ policy.debug(action, debug_output = +'')
+ "expected #{policy} not to allow #{action}\n\n#{debug_output}"
+ end
+end
diff --git a/spec/support/omniauth_strategy.rb b/spec/support/omniauth_strategy.rb
index eefa04bd9dd..23907b8e450 100644
--- a/spec/support/omniauth_strategy.rb
+++ b/spec/support/omniauth_strategy.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module StrategyHelpers
include Rack::Test::Methods
include ActionDispatch::Assertions::ResponseAssertions
diff --git a/spec/support/redis/redis_shared_examples.rb b/spec/support/redis/redis_shared_examples.rb
index 7e47cdae866..97a23f02b3e 100644
--- a/spec/support/redis/redis_shared_examples.rb
+++ b/spec/support/redis/redis_shared_examples.rb
@@ -90,6 +90,7 @@ RSpec.shared_examples "redis_shared_examples" do
describe '._raw_config' do
subject { described_class._raw_config }
+
let(:config_file_name) { '/var/empty/doesnotexist' }
it 'is frozen' do
diff --git a/spec/support/shared_contexts/policies/group_policy_shared_context.rb b/spec/support/shared_contexts/policies/group_policy_shared_context.rb
index b89723b1e1a..c503197a773 100644
--- a/spec/support/shared_contexts/policies/group_policy_shared_context.rb
+++ b/spec/support/shared_contexts/policies/group_policy_shared_context.rb
@@ -1,13 +1,13 @@
# frozen_string_literal: true
RSpec.shared_context 'GroupPolicy context' do
- let(:guest) { create(:user) }
- let(:reporter) { create(:user) }
- let(:developer) { create(:user) }
- let(:maintainer) { create(:user) }
- let(:owner) { create(:user) }
- let(:admin) { create(:admin) }
- let(:group) { create(:group, :private, :owner_subgroup_creation_only) }
+ let_it_be(:guest) { create(:user) }
+ let_it_be(:reporter) { create(:user) }
+ let_it_be(:developer) { create(:user) }
+ let_it_be(:maintainer) { create(:user) }
+ let_it_be(:owner) { create(:user) }
+ let_it_be(:admin) { create(:admin) }
+ let_it_be(:group, refind: true) { create(:group, :private, :owner_subgroup_creation_only) }
let(:guest_permissions) do
%i[
@@ -16,6 +16,7 @@ RSpec.shared_context 'GroupPolicy context' do
read_group_merge_requests
]
end
+ let(:read_group_permissions) { %i[read_label read_list read_milestone] }
let(:reporter_permissions) { %i[admin_label read_container_image] }
let(:developer_permissions) { [:admin_milestone] }
let(:maintainer_permissions) do
@@ -36,7 +37,7 @@ RSpec.shared_context 'GroupPolicy context' do
].compact
end
- before do
+ before_all do
group.add_guest(guest)
group.add_reporter(reporter)
group.add_developer(developer)
diff --git a/spec/support/shared_contexts/policies/project_policy_shared_context.rb b/spec/support/shared_contexts/policies/project_policy_shared_context.rb
index 1aa40dcde3d..65398c13d90 100644
--- a/spec/support/shared_contexts/policies/project_policy_shared_context.rb
+++ b/spec/support/shared_contexts/policies/project_policy_shared_context.rb
@@ -38,14 +38,14 @@ RSpec.shared_context 'ProjectPolicy context' do
update_commit_status create_build update_build create_pipeline
update_pipeline create_merge_request_from create_wiki push_code
resolve_note create_container_image update_container_image
- create_environment create_deployment create_release update_release
+ create_environment create_deployment update_deployment create_release update_release
]
end
let(:base_maintainer_permissions) do
%i[
push_to_delete_protected_branch update_project_snippet update_environment
- update_deployment admin_project_snippet admin_project_member admin_note admin_wiki admin_project
+ admin_project_snippet admin_project_member admin_note admin_wiki admin_project
admin_commit_status admin_build admin_container_image
admin_pipeline admin_environment admin_deployment destroy_release add_cluster
daily_statistics
diff --git a/spec/support/shared_contexts/rack_attack_shared_context.rb b/spec/support/shared_contexts/rack_attack_shared_context.rb
new file mode 100644
index 00000000000..c925f565226
--- /dev/null
+++ b/spec/support/shared_contexts/rack_attack_shared_context.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+shared_context 'rack attack cache store' do
+ around do |example|
+ # Instead of test environment's :null_store so the throttles can increment
+ Rack::Attack.cache.store = ActiveSupport::Cache::MemoryStore.new
+
+ # Make time-dependent tests deterministic
+ Timecop.freeze { example.run }
+
+ Rack::Attack.cache.store = Rails.cache
+ end
+end
diff --git a/spec/support/shared_contexts/services_shared_context.rb b/spec/support/shared_contexts/services_shared_context.rb
index 4d176ab5fca..113bcc2af9c 100644
--- a/spec/support/shared_contexts/services_shared_context.rb
+++ b/spec/support/shared_contexts/services_shared_context.rb
@@ -28,12 +28,17 @@ Service.available_services_names.each do |service|
end
end
+ let(:licensed_features) do
+ {
+ 'github' => :github_project_service_integration,
+ 'jenkins' => :jenkins_integration,
+ 'jenkins_deprecated' => :jenkins_integration,
+ 'alerts' => :incident_management
+ }
+ end
+
before do
- if service == 'github' && respond_to?(:stub_licensed_features)
- stub_licensed_features(github_project_service_integration: true)
- project.clear_memoization(:disabled_services)
- project.clear_memoization(:licensed_feature_available)
- end
+ enable_license_for_service(service)
end
def initialize_service(service)
@@ -42,5 +47,18 @@ Service.available_services_names.each do |service|
service_item.save!
service_item
end
+
+ private
+
+ def enable_license_for_service(service)
+ return unless respond_to?(:stub_licensed_features)
+
+ licensed_feature = licensed_features[service]
+ return unless licensed_feature
+
+ stub_licensed_features(licensed_feature => true)
+ project.clear_memoization(:disabled_services)
+ project.clear_memoization(:licensed_feature_available)
+ end
end
end
diff --git a/spec/support/shared_contexts/session_shared_context.rb b/spec/support/shared_contexts/session_shared_context.rb
new file mode 100644
index 00000000000..86c145a8360
--- /dev/null
+++ b/spec/support/shared_contexts/session_shared_context.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+# the session is empty by default; you can overwrite it by defining your own
+# let(:session) variable
+# we do not use a parameter such as |session| because it does not play nice
+# with let variables
+shared_context 'custom session' do
+ let!(:session) { {} }
+
+ around do |example|
+ Gitlab::Session.with_session(session) do
+ example.run
+ end
+ end
+end
diff --git a/spec/support/shared_examples/chat_slash_commands_shared_examples.rb b/spec/support/shared_examples/chat_slash_commands_shared_examples.rb
index a99068ab678..370f2072705 100644
--- a/spec/support/shared_examples/chat_slash_commands_shared_examples.rb
+++ b/spec/support/shared_examples/chat_slash_commands_shared_examples.rb
@@ -94,16 +94,32 @@ RSpec.shared_examples 'chat slash commands service' do
subject.trigger(params)
end
+ shared_examples_for 'blocks command execution' do
+ it do
+ expect_any_instance_of(Gitlab::SlashCommands::Command).not_to receive(:execute)
+
+ result = subject.trigger(params)
+ expect(result[:text]).to match(error_message)
+ end
+ end
+
context 'when user is blocked' do
before do
chat_name.user.block
end
- it 'blocks command execution' do
- expect_any_instance_of(Gitlab::SlashCommands::Command).not_to receive(:execute)
+ it_behaves_like 'blocks command execution' do
+ let(:error_message) { 'you do not have access to the GitLab project' }
+ end
+ end
- result = subject.trigger(params)
- expect(result).to include(text: /^You are not allowed/)
+ context 'when user is deactivated' do
+ before do
+ chat_name.user.deactivate
+ end
+
+ it_behaves_like 'blocks command execution' do
+ let(:error_message) { 'your account has been deactivated by your administrator' }
end
end
end
diff --git a/spec/support/shared_examples/ci/pipeline_email_examples.rb b/spec/support/shared_examples/ci/pipeline_email_examples.rb
new file mode 100644
index 00000000000..f72d8af3c65
--- /dev/null
+++ b/spec/support/shared_examples/ci/pipeline_email_examples.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+shared_examples_for 'correct pipeline information for pipelines for merge requests' do
+ context 'when pipeline for merge request' do
+ let(:pipeline) { merge_request.all_pipelines.first }
+
+ let(:merge_request) do
+ create(:merge_request, :with_detached_merge_request_pipeline,
+ source_project: project,
+ target_project: project)
+ end
+
+ it 'renders a source ref of the pipeline' do
+ render
+
+ expect(rendered).to have_content pipeline.source_ref
+ expect(rendered).not_to have_content pipeline.ref
+ end
+ end
+end
diff --git a/spec/support/shared_examples/ci_trace_shared_examples.rb b/spec/support/shared_examples/ci_trace_shared_examples.rb
index e2b4b50d41d..441d3f4ccb9 100644
--- a/spec/support/shared_examples/ci_trace_shared_examples.rb
+++ b/spec/support/shared_examples/ci_trace_shared_examples.rb
@@ -423,7 +423,7 @@ shared_examples_for 'trace with disabled live trace feature' do
expect(build.job_artifacts_trace.file.filename).to eq('job.log')
expect(File.exist?(src_path)).to be_falsy
expect(src_checksum)
- .to eq(Digest::SHA256.file(build.job_artifacts_trace.file.path).hexdigest)
+ .to eq(described_class.hexdigest(build.job_artifacts_trace.file.path))
expect(build.job_artifacts_trace.file_sha256).to eq(src_checksum)
end
end
@@ -449,7 +449,7 @@ shared_examples_for 'trace with disabled live trace feature' do
expect(build.job_artifacts_trace.file.filename).to eq('job.log')
expect(build.old_trace).to be_nil
expect(src_checksum)
- .to eq(Digest::SHA256.file(build.job_artifacts_trace.file.path).hexdigest)
+ .to eq(described_class.hexdigest(build.job_artifacts_trace.file.path))
expect(build.job_artifacts_trace.file_sha256).to eq(src_checksum)
end
end
@@ -787,7 +787,7 @@ shared_examples_for 'trace with enabled live trace feature' do
expect(build.job_artifacts_trace.file.filename).to eq('job.log')
expect(Ci::BuildTraceChunk.where(build: build)).not_to be_exist
expect(src_checksum)
- .to eq(Digest::SHA256.file(build.job_artifacts_trace.file.path).hexdigest)
+ .to eq(described_class.hexdigest(build.job_artifacts_trace.file.path))
expect(build.job_artifacts_trace.file_sha256).to eq(src_checksum)
end
end
diff --git a/spec/support/shared_examples/common_system_notes_examples.rb b/spec/support/shared_examples/common_system_notes_examples.rb
index 75f93a32d78..ca79603a022 100644
--- a/spec/support/shared_examples/common_system_notes_examples.rb
+++ b/spec/support/shared_examples/common_system_notes_examples.rb
@@ -27,3 +27,30 @@ shared_examples 'WIP notes creation' do |wip_action|
expect(Note.second.note).to match('changed title')
end
end
+
+shared_examples_for 'a note with overridable created_at' do
+ let(:noteable) { create(:issue, project: project, system_note_timestamp: Time.at(42)) }
+
+ it 'the note has the correct time' do
+ expect(subject.created_at).to eq Time.at(42)
+ end
+end
+
+shared_examples_for 'a system note' do |params|
+ let(:expected_noteable) { noteable }
+ let(:commit_count) { nil }
+
+ it 'has the correct attributes', :aggregate_failures do
+ exclude_project = !params.nil? && params[:exclude_project]
+
+ expect(subject).to be_valid
+ expect(subject).to be_system
+
+ expect(subject.noteable).to eq expected_noteable
+ expect(subject.project).to eq project unless exclude_project
+ expect(subject.author).to eq author
+
+ expect(subject.system_note_metadata.action).to eq(action)
+ expect(subject.system_note_metadata.commit_count).to eq(commit_count)
+ end
+end
diff --git a/spec/support/shared_examples/controllers/repository_lfs_file_load_examples.rb b/spec/support/shared_examples/controllers/repository_lfs_file_load_examples.rb
index d3cadf2ba7c..5dea17069f9 100644
--- a/spec/support/shared_examples/controllers/repository_lfs_file_load_examples.rb
+++ b/spec/support/shared_examples/controllers/repository_lfs_file_load_examples.rb
@@ -25,13 +25,17 @@ shared_examples 'a controller that can serve LFS files' do |options = {}|
context 'when lfs is enabled' do
before do
allow_any_instance_of(Project).to receive(:lfs_enabled?).and_return(true)
+ allow_any_instance_of(LfsObjectUploader).to receive(:exists?).and_return(true)
+ allow(controller).to receive(:send_file) { controller.head :ok }
end
- context 'when project has access' do
+ def link_project(project)
+ project.lfs_objects << lfs_object
+ end
+
+ context 'when the project is linked to the LfsObject' do
before do
- project.lfs_objects << lfs_object
- allow_any_instance_of(LfsObjectUploader).to receive(:exists?).and_return(true)
- allow(controller).to receive(:send_file) { controller.head :ok }
+ link_project(project)
end
it 'serves the file' do
@@ -76,13 +80,68 @@ shared_examples 'a controller that can serve LFS files' do |options = {}|
end
end
- context 'when project does not have access' do
+ context 'when project is not linked to the LfsObject' do
it 'does not serve the file' do
subject
expect(response).to have_gitlab_http_status(404)
end
end
+
+ context 'when the project is part of a fork network' do
+ shared_examples 'a controller that correctly serves lfs files within a fork network' do
+ it do
+ expect(fork_network_member).not_to eq(fork_network.root_project)
+ end
+
+ it 'does not serve the file if no members are linked to the LfsObject' do
+ subject
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+
+ it 'serves the file when the fork network root is linked to the LfsObject' do
+ link_project(fork_network.root_project)
+
+ subject
+
+ expect(response).to have_gitlab_http_status(200)
+ end
+
+ it 'serves the file when the fork network member is linked to the LfsObject' do
+ link_project(fork_network_member)
+
+ subject
+
+ expect(response).to have_gitlab_http_status(200)
+ end
+ end
+
+ context 'when the project is the root of the fork network' do
+ let!(:fork_network) { create(:fork_network, root_project: project) }
+ let!(:fork_network_member) { create(:fork_network_member, fork_network: fork_network).project }
+
+ before do
+ project.reload
+ end
+
+ it_behaves_like 'a controller that correctly serves lfs files within a fork network'
+ end
+
+ context 'when the project is a downstream member of the fork network' do
+ let!(:fork_network) { create(:fork_network) }
+ let!(:fork_network_member) do
+ create(:fork_network_member, project: project, fork_network: fork_network)
+ project
+ end
+
+ before do
+ project.reload
+ end
+
+ it_behaves_like 'a controller that correctly serves lfs files within a fork network'
+ end
+ end
end
context 'when lfs is not enabled' do
diff --git a/spec/support/shared_examples/controllers/uploads_actions_shared_examples.rb b/spec/support/shared_examples/controllers/uploads_actions_shared_examples.rb
index 4bc22861d58..c24418b2f90 100644
--- a/spec/support/shared_examples/controllers/uploads_actions_shared_examples.rb
+++ b/spec/support/shared_examples/controllers/uploads_actions_shared_examples.rb
@@ -104,7 +104,7 @@ shared_examples 'handle uploads' do
context "when neither the uploader nor the model exists" do
before do
- allow_any_instance_of(Upload).to receive(:build_uploader).and_return(nil)
+ allow_any_instance_of(Upload).to receive(:retrieve_uploader).and_return(nil)
allow(controller).to receive(:find_model).and_return(nil)
end
@@ -338,7 +338,7 @@ shared_examples 'handle uploads authorize' do
it_behaves_like 'a valid response' do
it 'responds with status 200, location of uploads remote store and object details' do
- expect(json_response['TempPath']).to eq(uploader_class.workhorse_local_upload_path)
+ expect(json_response).not_to have_key('TempPath')
expect(json_response['RemoteObject']).to have_key('ID')
expect(json_response['RemoteObject']).to have_key('GetURL')
expect(json_response['RemoteObject']).to have_key('StoreURL')
diff --git a/spec/support/shared_examples/cycle_analytics_event_shared_examples.rb b/spec/support/shared_examples/cycle_analytics_event_shared_examples.rb
new file mode 100644
index 00000000000..dce1dbe1cd1
--- /dev/null
+++ b/spec/support/shared_examples/cycle_analytics_event_shared_examples.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+shared_examples_for 'cycle analytics event' do
+ let(:instance) { described_class.new({}) }
+
+ it { expect(described_class.name).to be_a_kind_of(String) }
+ it { expect(described_class.identifier).to be_a_kind_of(Symbol) }
+ it { expect(instance.object_type.ancestors).to include(ApplicationRecord) }
+ it { expect(instance).to respond_to(:timestamp_projection) }
+
+ describe '#apply_query_customization' do
+ it 'expects an ActiveRecord::Relation object as argument and returns a modified version of it' do
+ input_query = instance.object_type.all
+
+ output_query = instance.apply_query_customization(input_query)
+ expect(output_query).to be_a_kind_of(ActiveRecord::Relation)
+ end
+ end
+end
diff --git a/spec/support/shared_examples/cycle_analytics_stage_examples.rb b/spec/support/shared_examples/cycle_analytics_stage_shared_examples.rb
index 151f5325e84..afa035d039a 100644
--- a/spec/support/shared_examples/cycle_analytics_stage_examples.rb
+++ b/spec/support/shared_examples/cycle_analytics_stage_shared_examples.rb
@@ -46,13 +46,20 @@ shared_examples_for 'cycle analytics stage' do
expect(stage).not_to be_valid
expect(stage.errors.details[:end_event]).to eq([{ error: :not_allowed_for_the_given_start_event }])
end
+
+ context 'disallows default stage names when creating custom stage' do
+ let(:invalid_params) { valid_params.merge(name: Gitlab::Analytics::CycleAnalytics::DefaultStages.names.first, custom: true) }
+ let(:stage) { described_class.new(invalid_params) }
+
+ it { expect(stage).not_to be_valid }
+ end
end
- describe '#subject_model' do
+ describe '#subject_class' do
it 'infers the model from the start event' do
stage = described_class.new(valid_params)
- expect(stage.subject_model).to eq(MergeRequest)
+ expect(stage.subject_class).to eq(MergeRequest)
end
end
@@ -71,4 +78,30 @@ shared_examples_for 'cycle analytics stage' do
expect(stage.end_event).to be_a_kind_of(Gitlab::Analytics::CycleAnalytics::StageEvents::MergeRequestMerged)
end
end
+
+ describe '#matches_with_stage_params?' do
+ let(:params) { Gitlab::Analytics::CycleAnalytics::DefaultStages.params_for_test_stage }
+
+ it 'matches with default stage params' do
+ stage = described_class.new(params)
+
+ expect(stage).to be_default_stage
+ expect(stage).to be_matches_with_stage_params(params)
+ end
+
+ it "mismatches when the stage is custom" do
+ stage = described_class.new(params.merge(custom: true))
+
+ expect(stage).not_to be_default_stage
+ expect(stage).not_to be_matches_with_stage_params(params)
+ end
+ end
+
+ describe '#parent_id' do
+ it "delegates to 'parent_name'_id attribute" do
+ stage = described_class.new(parent: parent)
+
+ expect(stage.parent_id).to eq(parent.id)
+ end
+ end
end
diff --git a/spec/support/shared_examples/diff_file_collections.rb b/spec/support/shared_examples/diff_file_collections.rb
index 367ddf06c28..4c64abd2a97 100644
--- a/spec/support/shared_examples/diff_file_collections.rb
+++ b/spec/support/shared_examples/diff_file_collections.rb
@@ -1,6 +1,8 @@
# frozen_string_literal: true
shared_examples 'diff statistics' do |test_include_stats_flag: true|
+ subject { described_class.new(diffable, collection_default_args) }
+
def stub_stats_find_by_path(path, stats_mock)
expect_next_instance_of(Gitlab::Git::DiffStatsCollection) do |collection|
allow(collection).to receive(:find_by_path).and_call_original
@@ -10,8 +12,6 @@ shared_examples 'diff statistics' do |test_include_stats_flag: true|
context 'when should request diff stats' do
it 'Repository#diff_stats is called' do
- subject = described_class.new(diffable, collection_default_args)
-
expect(diffable.project.repository)
.to receive(:diff_stats)
.with(diffable.diff_refs.base_sha, diffable.diff_refs.head_sha)
@@ -21,8 +21,6 @@ shared_examples 'diff statistics' do |test_include_stats_flag: true|
end
it 'Gitlab::Diff::File is initialized with diff stats' do
- subject = described_class.new(diffable, collection_default_args)
-
stats_mock = double(Gitaly::DiffStats, path: '.gitignore', additions: 758, deletions: 120)
stub_stats_find_by_path(stub_path, stats_mock)
@@ -37,8 +35,6 @@ shared_examples 'diff statistics' do |test_include_stats_flag: true|
it 'Repository#diff_stats is not called' do
collection_default_args[:diff_options][:include_stats] = false
- subject = described_class.new(diffable, collection_default_args)
-
expect(diffable.project.repository).not_to receive(:diff_stats)
subject.diff_files
diff --git a/spec/support/shared_examples/evidence_updated_exposed_fields.rb b/spec/support/shared_examples/evidence_updated_exposed_fields.rb
new file mode 100644
index 00000000000..2a02fdd7666
--- /dev/null
+++ b/spec/support/shared_examples/evidence_updated_exposed_fields.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+shared_examples 'updated exposed field' do
+ it 'creates another Evidence object' do
+ model.send("#{updated_field}=", updated_value)
+
+ expect(model.evidence_summary_keys).to include(updated_field)
+ expect { model.save! }.to change(Evidence, :count).by(1)
+ expect(updated_json_field).to eq(updated_value)
+ end
+end
+
+shared_examples 'updated non-exposed field' do
+ it 'does not create any Evidence object' do
+ model.send("#{updated_field}=", updated_value)
+
+ expect(model.evidence_summary_keys).not_to include(updated_field)
+ expect { model.save! }.not_to change(Evidence, :count)
+ end
+end
+
+shared_examples 'updated field on non-linked entity' do
+ it 'does not create any Evidence object' do
+ model.send("#{updated_field}=", updated_value)
+
+ expect(model.evidence_summary_keys).to be_empty
+ expect { model.save! }.not_to change(Evidence, :count)
+ end
+end
diff --git a/spec/support/shared_examples/lfs_http_shared_examples.rb b/spec/support/shared_examples/lfs_http_shared_examples.rb
new file mode 100644
index 00000000000..bcd30fe9654
--- /dev/null
+++ b/spec/support/shared_examples/lfs_http_shared_examples.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+shared_examples 'LFS http 200 response' do
+ it_behaves_like 'LFS http expected response code and message' do
+ let(:response_code) { 200 }
+ end
+end
+
+shared_examples 'LFS http 401 response' do
+ it_behaves_like 'LFS http expected response code and message' do
+ let(:response_code) { 401 }
+ end
+end
+
+shared_examples 'LFS http 403 response' do
+ it_behaves_like 'LFS http expected response code and message' do
+ let(:response_code) { 403 }
+ let(:message) { 'Access forbidden. Check your access level.' }
+ end
+end
+
+shared_examples 'LFS http 501 response' do
+ it_behaves_like 'LFS http expected response code and message' do
+ let(:response_code) { 501 }
+ let(:message) { 'Git LFS is not enabled on this GitLab server, contact your admin.' }
+ end
+end
+
+shared_examples 'LFS http 404 response' do
+ it_behaves_like 'LFS http expected response code and message' do
+ let(:response_code) { 404 }
+ end
+end
+
+shared_examples 'LFS http expected response code and message' do
+ let(:response_code) { }
+ let(:message) { }
+
+ it 'responds with the expected response code and message' do
+ expect(response).to have_gitlab_http_status(response_code)
+ expect(json_response['message']).to eq(message) if message
+ end
+end
diff --git a/spec/support/shared_examples/lib/gitlab/ci/build/rules/rule/clause/clause_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/ci/build/rules/rule/clause/clause_shared_examples.rb
new file mode 100644
index 00000000000..1934fd584f3
--- /dev/null
+++ b/spec/support/shared_examples/lib/gitlab/ci/build/rules/rule/clause/clause_shared_examples.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'a glob matching rule' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:case_name, :globs, :files, :satisfied) do
+ 'exact top-level match' | ['Dockerfile'] | { 'Dockerfile' => '', 'Gemfile' => '' } | true
+ 'exact top-level no match' | ['Dockerfile'] | { 'Gemfile' => '' } | false
+ 'pattern top-level match' | ['Docker*'] | { 'Dockerfile' => '', 'Gemfile' => '' } | true
+ 'pattern top-level no match' | ['Docker*'] | { 'Gemfile' => '' } | false
+ 'exact nested match' | ['project/build.properties'] | { 'project/build.properties' => '' } | true
+ 'exact nested no match' | ['project/build.properties'] | { 'project/README.md' => '' } | false
+ 'pattern nested match' | ['src/**/*.go'] | { 'src/gitlab.com/goproject/goproject.go' => '' } | true
+ 'pattern nested no match' | ['src/**/*.go'] | { 'src/gitlab.com/goproject/README.md' => '' } | false
+ 'ext top-level match' | ['*.go'] | { 'main.go' => '', 'cmd/goproject/main.go' => '' } | true
+ 'ext nested no match' | ['*.go'] | { 'cmd/goproject/main.go' => '' } | false
+ 'ext slash no match' | ['/*.go'] | { 'main.go' => '', 'cmd/goproject/main.go' => '' } | false
+ end
+
+ with_them do
+ it { is_expected.to eq(satisfied) }
+ end
+end
diff --git a/spec/support/shared_examples/lib/gitlab/import_export/project_tree_restorer_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/import_export/project_tree_restorer_shared_examples.rb
new file mode 100644
index 00000000000..f26a8554055
--- /dev/null
+++ b/spec/support/shared_examples/lib/gitlab/import_export/project_tree_restorer_shared_examples.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+# Shared examples for ProjectTreeRestorer (shared to allow the testing
+# of EE-specific features)
+RSpec.shared_examples 'restores project correctly' do |**results|
+ it 'restores the project' do
+ expect(shared.errors).to be_empty
+ expect(restored_project_json).to be_truthy
+ end
+
+ it 'has labels' do
+ labels_size = results.fetch(:labels, 0)
+
+ expect(project.labels.size).to eq(labels_size)
+ end
+
+ it 'has label priorities' do
+ label_with_priorities = results[:label_with_priorities]
+
+ if label_with_priorities
+ expect(project.labels.find_by(title: label_with_priorities).priorities).not_to be_empty
+ end
+ end
+
+ it 'has milestones' do
+ expect(project.milestones.size).to eq(results.fetch(:milestones, 0))
+ end
+
+ it 'has issues' do
+ expect(project.issues.size).to eq(results.fetch(:issues, 0))
+ end
+
+ it 'does not set params that are excluded from import_export settings' do
+ expect(project.import_type).to be_nil
+ expect(project.creator_id).not_to eq 123
+ end
+end
diff --git a/spec/support/shared_examples/models/cluster_application_status_shared_examples.rb b/spec/support/shared_examples/models/cluster_application_status_shared_examples.rb
index 5341aacb445..a6653f89377 100644
--- a/spec/support/shared_examples/models/cluster_application_status_shared_examples.rb
+++ b/spec/support/shared_examples/models/cluster_application_status_shared_examples.rb
@@ -11,6 +11,20 @@ shared_examples 'cluster application status specs' do |application_name|
end
end
+ describe '#status_states' do
+ let(:cluster) { create(:cluster, :provided_by_gcp) }
+
+ subject { described_class.new(cluster: cluster) }
+
+ it 'returns a hash of state values' do
+ expect(subject.status_states).to include(:installed)
+ end
+
+ it 'returns an integer for installed state value' do
+ expect(subject.status_states[:installed]).to eq(3)
+ end
+ end
+
describe '.available' do
subject { described_class.available }
@@ -61,7 +75,7 @@ shared_examples 'cluster application status specs' do |application_name|
subject.reload
- expect(subject.version).to eq(subject.class.const_get(:VERSION))
+ expect(subject.version).to eq(subject.class.const_get(:VERSION, false))
end
context 'application is updating' do
@@ -90,13 +104,14 @@ shared_examples 'cluster application status specs' do |application_name|
subject.reload
- expect(subject.version).to eq(subject.class.const_get(:VERSION))
+ expect(subject.version).to eq(subject.class.const_get(:VERSION, false))
end
end
end
describe '#make_errored' do
subject { create(application_name, :installing) }
+
let(:reason) { 'some errors' }
it 'is errored' do
diff --git a/spec/support/shared_examples/models/cluster_application_version_shared_examples.rb b/spec/support/shared_examples/models/cluster_application_version_shared_examples.rb
index 181b102e685..ba02da41b53 100644
--- a/spec/support/shared_examples/models/cluster_application_version_shared_examples.rb
+++ b/spec/support/shared_examples/models/cluster_application_version_shared_examples.rb
@@ -12,7 +12,7 @@ shared_examples 'cluster application version specs' do |application_name|
context 'version is the same as VERSION' do
let(:application) { build(application_name) }
- let(:version) { application.class.const_get(:VERSION) }
+ let(:version) { application.class.const_get(:VERSION, false) }
it { is_expected.to be_falsey }
end
diff --git a/spec/support/shared_examples/models/clusters/providers/provider_status.rb b/spec/support/shared_examples/models/clusters/providers/provider_status.rb
new file mode 100644
index 00000000000..63cb9a56f5b
--- /dev/null
+++ b/spec/support/shared_examples/models/clusters/providers/provider_status.rb
@@ -0,0 +1,73 @@
+# frozen_string_literal: true
+
+shared_examples 'provider status' do |factory|
+ describe 'state_machine' do
+ context 'when any => [:created]' do
+ let(:provider) { build(factory, :creating) }
+
+ it 'nullifies API credentials' do
+ expect(provider).to receive(:nullify_credentials).and_call_original
+ provider.make_created
+
+ expect(provider).to be_created
+ end
+ end
+
+ context 'when any => [:creating]' do
+ let(:provider) { build(factory) }
+ let(:operation_id) { 'operation-xxx' }
+
+ it 'calls #assign_operation_id on the provider' do
+ expect(provider).to receive(:assign_operation_id).with(operation_id).and_call_original
+
+ provider.make_creating(operation_id)
+ end
+ end
+
+ context 'when any => [:errored]' do
+ let(:provider) { build(factory, :creating) }
+ let(:status_reason) { 'err msg' }
+
+ it 'calls #nullify_credentials on the provider' do
+ expect(provider).to receive(:nullify_credentials).and_call_original
+
+ provider.make_errored(status_reason)
+ end
+
+ it 'sets a status reason' do
+ provider.make_errored(status_reason)
+
+ expect(provider.status_reason).to eq('err msg')
+ end
+
+ context 'when status_reason is nil' do
+ let(:provider) { build(factory, :errored) }
+
+ it 'does not set status_reason' do
+ provider.make_errored(nil)
+
+ expect(provider.status_reason).not_to be_nil
+ end
+ end
+ end
+ end
+
+ describe '#on_creation?' do
+ using RSpec::Parameterized::TableSyntax
+
+ subject { provider.on_creation? }
+
+ where(:status, :result) do
+ :scheduled | true
+ :creating | true
+ :created | false
+ :errored | false
+ end
+
+ with_them do
+ let(:provider) { build(factory, status) }
+
+ it { is_expected.to eq result }
+ end
+ end
+end
diff --git a/spec/support/shared_examples/models/concern/issuable_shared_examples.rb b/spec/support/shared_examples/models/concern/issuable_shared_examples.rb
index 9604555c57d..4ebb5e35e0e 100644
--- a/spec/support/shared_examples/models/concern/issuable_shared_examples.rb
+++ b/spec/support/shared_examples/models/concern/issuable_shared_examples.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
shared_examples_for 'matches_cross_reference_regex? fails fast' do
it 'fails fast for long strings' do
# took well under 1 second in CI https://dev.gitlab.org/gitlab/gitlabhq/merge_requests/3267#note_172823
@@ -6,3 +8,59 @@ shared_examples_for 'matches_cross_reference_regex? fails fast' do
end.not_to raise_error
end
end
+
+shared_examples_for 'validates description length with custom validation' do
+ let(:issuable) { build(:issue, description: 'x' * (::Issuable::DESCRIPTION_LENGTH_MAX + 1)) }
+ let(:context) { :update }
+
+ subject { issuable.validate(context) }
+
+ context 'when Issuable is a new record' do
+ it 'validates the maximum description length' do
+ subject
+ expect(issuable.errors[:description]).to eq(["is too long (maximum is #{::Issuable::DESCRIPTION_LENGTH_MAX} characters)"])
+ end
+
+ context 'on create' do
+ let(:context) { :create }
+
+ it 'does not validate the maximum description length' do
+ allow(issuable).to receive(:description_max_length_for_new_records_is_valid).and_call_original
+
+ subject
+
+ expect(issuable).not_to have_received(:description_max_length_for_new_records_is_valid)
+ end
+ end
+ end
+
+ context 'when Issuable is an existing record' do
+ before do
+ allow(issuable).to receive(:expire_etag_cache) # to skip the expire_etag_cache callback
+
+ issuable.save!(validate: false)
+ end
+
+ it 'does not validate the maximum description length' do
+ subject
+ expect(issuable.errors).not_to have_key(:description)
+ end
+ end
+end
+
+shared_examples_for 'truncates the description to its allowed maximum length on import' do
+ before do
+ allow(issuable).to receive(:importing?).and_return(true)
+ end
+
+ let(:issuable) { build(:issue, description: 'x' * (::Issuable::DESCRIPTION_LENGTH_MAX + 1)) }
+
+ subject { issuable.validate(:create) }
+
+ it 'truncates the description to its allowed maximum length' do
+ subject
+
+ expect(issuable.description).to eq('x' * ::Issuable::DESCRIPTION_LENGTH_MAX)
+ expect(issuable.errors[:description]).to be_empty
+ end
+end
diff --git a/spec/support/shared_examples/models/with_uploads_shared_examples.rb b/spec/support/shared_examples/models/with_uploads_shared_examples.rb
index eb1ade03017..822836c771e 100644
--- a/spec/support/shared_examples/models/with_uploads_shared_examples.rb
+++ b/spec/support/shared_examples/models/with_uploads_shared_examples.rb
@@ -41,7 +41,8 @@ shared_examples_for 'model with uploads' do |supports_fileuploads|
end
it 'deletes remote files' do
- expect_any_instance_of(Uploads::Fog).to receive(:delete_keys).with(uploads.map(&:path))
+ expected_array = array_including(*uploads.map(&:path))
+ expect_any_instance_of(Uploads::Fog).to receive(:delete_keys).with(expected_array)
model_object.destroy
end
diff --git a/spec/support/shared_examples/quick_actions/issue/zoom_quick_actions_shared_examples.rb b/spec/support/shared_examples/quick_actions/issue/zoom_quick_actions_shared_examples.rb
index cb5460bde23..b4a8e3fca4d 100644
--- a/spec/support/shared_examples/quick_actions/issue/zoom_quick_actions_shared_examples.rb
+++ b/spec/support/shared_examples/quick_actions/issue/zoom_quick_actions_shared_examples.rb
@@ -45,14 +45,6 @@ shared_examples 'zoom quick actions' do
expect(page).to have_content('Failed to add a Zoom meeting')
expect(page).not_to have_content(zoom_link)
end
-
- context 'when feature flag disabled' do
- before do
- stub_feature_flags(issue_zoom_integration: false)
- end
-
- include_examples 'skip silently'
- end
end
context 'with Zoom link not at the end of the issue description' do
@@ -92,14 +84,6 @@ shared_examples 'zoom quick actions' do
expect(page).to have_content('Zoom meeting removed')
expect(issue.reload.description).to eq("Text with #{zoom_link}")
end
-
- context 'when feature flag disabled' do
- before do
- stub_feature_flags(issue_zoom_integration: false)
- end
-
- include_examples 'skip silently'
- end
end
context 'with a Zoom link not at the end of the description' do
diff --git a/spec/support/shared_examples/relative_positioning_shared_examples.rb b/spec/support/shared_examples/relative_positioning_shared_examples.rb
index b7382cea93c..99e62ebf422 100644
--- a/spec/support/shared_examples/relative_positioning_shared_examples.rb
+++ b/spec/support/shared_examples/relative_positioning_shared_examples.rb
@@ -84,6 +84,22 @@ RSpec.shared_examples 'a class that supports relative positioning' do
expect(item1.relative_position).to be < item2.relative_position
end
+
+ context 'when there is no space' do
+ let(:item3) { create(factory, default_params) }
+
+ before do
+ item1.update(relative_position: 1000)
+ item2.update(relative_position: 1001)
+ item3.update(relative_position: 1002)
+ end
+
+ it 'moves items correctly' do
+ item3.move_before(item2)
+
+ expect(item3.relative_position).to be_between(item1.reload.relative_position, item2.reload.relative_position).exclusive
+ end
+ end
end
describe '#move_after' do
@@ -94,6 +110,22 @@ RSpec.shared_examples 'a class that supports relative positioning' do
expect(item1.relative_position).to be > item2.relative_position
end
+
+ context 'when there is no space' do
+ let(:item3) { create(factory, default_params) }
+
+ before do
+ item1.update(relative_position: 1000)
+ item2.update(relative_position: 1001)
+ item3.update(relative_position: 1002)
+ end
+
+ it 'moves items correctly' do
+ item1.move_after(item2)
+
+ expect(item1.relative_position).to be_between(item2.reload.relative_position, item3.reload.relative_position).exclusive
+ end
+ end
end
describe '#move_to_end' do
@@ -196,7 +228,7 @@ RSpec.shared_examples 'a class that supports relative positioning' do
new_item.move_between(item1, item2)
- expect(new_item.relative_position).to be_between(item1.relative_position, item2.relative_position)
+ expect(new_item.relative_position).to be_between(item1.relative_position, item2.relative_position).exclusive
end
it 'uses rebalancing if there is no place' do
@@ -208,7 +240,7 @@ RSpec.shared_examples 'a class that supports relative positioning' do
new_item.move_between(item2, item3)
new_item.save!
- expect(new_item.relative_position).to be_between(item2.relative_position, item3.relative_position)
+ expect(new_item.relative_position).to be_between(item2.relative_position, item3.relative_position).exclusive
expect(item1.reload.relative_position).not_to eq(100)
end
diff --git a/spec/support/shared_examples/repo_type_shared_examples.rb b/spec/support/shared_examples/repo_type_shared_examples.rb
new file mode 100644
index 00000000000..dc9e3a73346
--- /dev/null
+++ b/spec/support/shared_examples/repo_type_shared_examples.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+shared_examples 'a repo type' do
+ describe "#identifier_for_subject" do
+ subject { described_class.identifier_for_subject(project) }
+
+ it { is_expected.to eq(expected_identifier) }
+ end
+
+ describe "#fetch_id" do
+ it "finds an id match in the identifier" do
+ expect(described_class.fetch_id(expected_identifier)).to eq(expected_id)
+ end
+
+ it 'does not break on other identifiers' do
+ expect(described_class.fetch_id("wiki-noid")).to eq(nil)
+ end
+ end
+
+ describe "#path_suffix" do
+ subject { described_class.path_suffix }
+
+ it { is_expected.to eq(expected_suffix) }
+ end
+
+ describe "#repository_for" do
+ it "finds the repository for the repo type" do
+ expect(described_class.repository_for(project)).to eq(expected_repository)
+ end
+ end
+end
diff --git a/spec/support/shared_examples/requests/rack_attack_shared_examples.rb b/spec/support/shared_examples/requests/rack_attack_shared_examples.rb
index afc6f59b773..a2e38cfc60b 100644
--- a/spec/support/shared_examples/requests/rack_attack_shared_examples.rb
+++ b/spec/support/shared_examples/requests/rack_attack_shared_examples.rb
@@ -8,6 +8,14 @@
# * period_in_seconds
# * period
shared_examples_for 'rate-limited token-authenticated requests' do
+ let(:throttle_types) do
+ {
+ "throttle_protected_paths" => "throttle_authenticated_protected_paths_api",
+ "throttle_authenticated_api" => "throttle_authenticated_api",
+ "throttle_authenticated_web" => "throttle_authenticated_web"
+ }
+ end
+
before do
# Set low limits
settings_to_set[:"#{throttle_setting_prefix}_requests_per_period"] = requests_per_period
@@ -84,7 +92,8 @@ shared_examples_for 'rate-limited token-authenticated requests' do
request_method: 'GET',
path: get_args.first,
user_id: user.id,
- username: user.username
+ username: user.username,
+ throttle_type: throttle_types[throttle_setting_prefix]
}
expect(Gitlab::AuthLogger).to receive(:error).with(arguments).once
@@ -116,6 +125,13 @@ end
# * period_in_seconds
# * period
shared_examples_for 'rate-limited web authenticated requests' do
+ let(:throttle_types) do
+ {
+ "throttle_protected_paths" => "throttle_authenticated_protected_paths_web",
+ "throttle_authenticated_web" => "throttle_authenticated_web"
+ }
+ end
+
before do
login_as(user)
@@ -196,7 +212,8 @@ shared_examples_for 'rate-limited web authenticated requests' do
request_method: 'GET',
path: '/dashboard/snippets',
user_id: user.id,
- username: user.username
+ username: user.username,
+ throttle_type: throttle_types[throttle_setting_prefix]
}
expect(Gitlab::AuthLogger).to receive(:error).with(arguments).once
diff --git a/spec/support/shared_examples/services/boards/boards_create_service.rb b/spec/support/shared_examples/services/boards/boards_create_service.rb
index 19818a6091b..7fd69354c2d 100644
--- a/spec/support/shared_examples/services/boards/boards_create_service.rb
+++ b/spec/support/shared_examples/services/boards/boards_create_service.rb
@@ -17,7 +17,7 @@ shared_examples 'boards create service' do
context 'when parent has a board' do
before do
- create(:board, parent: parent)
+ create(:board, resource_parent: parent)
end
it 'does not create a new board' do
diff --git a/spec/support/shared_examples/services/boards/boards_list_service.rb b/spec/support/shared_examples/services/boards/boards_list_service.rb
index 566e5050f8e..25dc2e04942 100644
--- a/spec/support/shared_examples/services/boards/boards_list_service.rb
+++ b/spec/support/shared_examples/services/boards/boards_list_service.rb
@@ -15,7 +15,7 @@ shared_examples 'boards list service' do
context 'when parent has a board' do
before do
- create(:board, parent: parent)
+ create(:board, resource_parent: parent)
end
it 'does not create a new board' do
@@ -24,7 +24,7 @@ shared_examples 'boards list service' do
end
it 'returns parent boards' do
- board = create(:board, parent: parent)
+ board = create(:board, resource_parent: parent)
expect(service.execute).to eq [board]
end
diff --git a/spec/support/shared_examples/slack_mattermost_notifications_shared_examples.rb b/spec/support/shared_examples/slack_mattermost_notifications_shared_examples.rb
index 455f7b117b2..f15128d3e13 100644
--- a/spec/support/shared_examples/slack_mattermost_notifications_shared_examples.rb
+++ b/spec/support/shared_examples/slack_mattermost_notifications_shared_examples.rb
@@ -1,7 +1,5 @@
# frozen_string_literal: true
-Dir[Rails.root.join("app/models/project_services/chat_message/*.rb")].each { |f| require f }
-
RSpec.shared_examples 'slack or mattermost notifications' do |service_name|
let(:chat_service) { described_class.new }
let(:webhook_url) { 'https://example.gitlab.com/' }
@@ -371,6 +369,48 @@ RSpec.shared_examples 'slack or mattermost notifications' do |service_name|
end
end
+ context 'on a protected branch with protected branches defined using wildcards' do
+ before do
+ create(:protected_branch, project: project, name: '*-stable')
+ end
+
+ let(:data) do
+ Gitlab::DataBuilder::Push.build(
+ project: project,
+ user: user,
+ ref: '1-stable'
+ )
+ end
+
+ context 'pushing tags' do
+ let(:data) do
+ Gitlab::DataBuilder::Push.build(
+ project: project,
+ user: user,
+ ref: "#{Gitlab::Git::TAG_REF_PREFIX}test"
+ )
+ end
+
+ it_behaves_like "triggered #{service_name} service", event_type: "push"
+ end
+
+ context 'notification enabled only for default branch' do
+ it_behaves_like "untriggered #{service_name} service", event_type: "push", branches_to_be_notified: "default"
+ end
+
+ context 'notification enabled only for protected branches' do
+ it_behaves_like "triggered #{service_name} service", event_type: "push", branches_to_be_notified: "protected"
+ end
+
+ context 'notification enabled only for default and protected branches' do
+ it_behaves_like "triggered #{service_name} service", event_type: "push", branches_to_be_notified: "default_and_protected"
+ end
+
+ context 'notification enabled for all branches' do
+ it_behaves_like "triggered #{service_name} service", event_type: "push", branches_to_be_notified: "all"
+ end
+ end
+
context 'on a neither protected nor default branch' do
let(:data) do
Gitlab::DataBuilder::Push.build(
@@ -572,6 +612,36 @@ RSpec.shared_examples 'slack or mattermost notifications' do |service_name|
end
end
+ context 'on a protected branch with protected branches defined usin wildcards' do
+ before do
+ create(:protected_branch, project: project, name: '*-stable')
+ end
+
+ let(:pipeline) do
+ create(:ci_pipeline,
+ project: project, status: :failed,
+ sha: project.commit.sha, ref: '1-stable')
+ end
+
+ let(:data) { Gitlab::DataBuilder::Pipeline.build(pipeline) }
+
+ context 'notification enabled only for default branch' do
+ it_behaves_like "untriggered #{service_name} service", event_type: "pipeline", branches_to_be_notified: "default"
+ end
+
+ context 'notification enabled only for protected branches' do
+ it_behaves_like "triggered #{service_name} service", event_type: "pipeline", branches_to_be_notified: "protected"
+ end
+
+ context 'notification enabled only for default and protected branches' do
+ it_behaves_like "triggered #{service_name} service", event_type: "pipeline", branches_to_be_notified: "default_and_protected"
+ end
+
+ context 'notification enabled for all branches' do
+ it_behaves_like "triggered #{service_name} service", event_type: "pipeline", branches_to_be_notified: "all"
+ end
+ end
+
context 'on a neither protected nor default branch' do
let(:pipeline) do
create(:ci_pipeline,
diff --git a/spec/support/shared_examples/snippet_visibility_shared_examples.rb b/spec/support/shared_examples/snippet_visibility_shared_examples.rb
index b5321c6db34..e2089ee623a 100644
--- a/spec/support/shared_examples/snippet_visibility_shared_examples.rb
+++ b/spec/support/shared_examples/snippet_visibility_shared_examples.rb
@@ -11,13 +11,21 @@ RSpec.shared_examples 'snippet visibility' do
set(:author) { create(:user) }
set(:member) { create(:user) }
set(:external) { create(:user, :external) }
+ set(:non_member) { create(:user) }
+
+ set(:project) do
+ create(:project).tap do |project|
+ project.add_developer(author)
+ project.add_developer(member)
+ end
+ end
context "For project snippets" do
let!(:users) do
{
unauthenticated: nil,
external: external,
- non_member: create(:user),
+ non_member: non_member,
member: member,
author: author
}
@@ -211,14 +219,18 @@ RSpec.shared_examples 'snippet visibility' do
end
with_them do
- let!(:project) { create(:project, visibility_level: Gitlab::VisibilityLevel.level_value(project_type.to_s)) }
+ let!(:project_visibility) { project.update_column(:visibility_level, Gitlab::VisibilityLevel.level_value(project_type.to_s)) }
let!(:project_feature) { project.project_feature.update_column(:snippets_access_level, feature_visibility) }
let!(:user) { users[user_type] }
let!(:snippet) { create(:project_snippet, visibility_level: snippet_type, project: project, author: author) }
- let!(:members) do
- project.add_developer(author)
- project.add_developer(member)
- project.add_developer(external) if project.private?
+ let!(:external_member) do
+ member = project.project_member(external)
+
+ if project.private?
+ project.add_developer(external) unless member
+ else
+ member.delete if member
+ end
end
context "For #{params[:project_type]} project and #{params[:user_type]} users" do
@@ -256,7 +268,7 @@ RSpec.shared_examples 'snippet visibility' do
{
unauthenticated: nil,
external: external,
- non_member: create(:user),
+ non_member: non_member,
author: author
}
end
diff --git a/spec/support/shared_examples/trackable_shared_examples.rb b/spec/support/shared_examples/trackable_shared_examples.rb
new file mode 100644
index 00000000000..6ad75a14d6b
--- /dev/null
+++ b/spec/support/shared_examples/trackable_shared_examples.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+shared_examples 'a Trackable Controller' do
+ describe '#track_event' do
+ before do
+ sign_in user
+ end
+
+ context 'with no params' do
+ controller(described_class) do
+ def index
+ track_event
+ head :ok
+ end
+ end
+
+ it 'tracks the action name' do
+ expect(Gitlab::Tracking).to receive(:event).with('AnonymousController', 'index', {})
+ get :index
+ end
+ end
+
+ context 'with params' do
+ controller(described_class) do
+ def index
+ track_event('some_event', category: 'SomeCategory', label: 'errorlabel')
+ head :ok
+ end
+ end
+
+ it 'tracks with the specified param' do
+ expect(Gitlab::Tracking).to receive(:event).with('SomeCategory', 'some_event', label: 'errorlabel')
+ get :index
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/updating_mentions_shared_examples.rb b/spec/support/shared_examples/updating_mentions_shared_examples.rb
index ef385f94cc2..9a8f8012762 100644
--- a/spec/support/shared_examples/updating_mentions_shared_examples.rb
+++ b/spec/support/shared_examples/updating_mentions_shared_examples.rb
@@ -1,11 +1,16 @@
# frozen_string_literal: true
RSpec.shared_examples 'updating mentions' do |service_class|
- let(:mentioned_user) { create(:user) }
- let(:service_class) { service_class }
+ let(:service_class) { service_class }
+ let(:mentioned_user) { create(:user) }
+ let(:group_member1) { create(:user) }
+ let(:group_member2) { create(:user) }
+ let(:external_group) { create(:group, :private) }
before do
project.add_developer(mentioned_user)
+ group.add_developer(group_member1)
+ group.add_developer(group_member2)
end
def update_mentionable(opts)
@@ -16,23 +21,74 @@ RSpec.shared_examples 'updating mentions' do |service_class|
mentionable.reload
end
- context 'in title' do
- before do
- update_mentionable(title: mentioned_user.to_reference)
+ context 'when mentioning a different user' do
+ context 'in title' do
+ before do
+ update_mentionable(title: "For #{mentioned_user.to_reference}")
+ end
+
+ it 'emails only the newly-mentioned user' do
+ should_only_email(mentioned_user)
+ end
+ end
+
+ context 'in description' do
+ before do
+ update_mentionable(description: "For #{mentioned_user.to_reference}")
+ end
+
+ it 'emails only the newly-mentioned user' do
+ should_only_email(mentioned_user)
+ end
+ end
+ end
+
+ context 'when mentioning a user and a group with access to' do
+ shared_examples 'updating attribute with allowed mentions' do |attribute|
+ before do
+ update_mentionable(
+ { attribute => "For #{group.to_reference}, cc: #{mentioned_user.to_reference}" }
+ )
+ end
+
+ it 'emails group members' do
+ should_email(mentioned_user)
+ should_email(group_member1)
+ should_email(group_member2)
+ end
+ end
+
+ context 'when group is public' do
+ it_behaves_like 'updating attribute with allowed mentions', :title
+ it_behaves_like 'updating attribute with allowed mentions', :description
end
- it 'emails only the newly-mentioned user' do
- should_only_email(mentioned_user)
+ context 'when the group is private' do
+ before do
+ group.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
+ end
+
+ it_behaves_like 'updating attribute with allowed mentions', :title
+ it_behaves_like 'updating attribute with allowed mentions', :description
end
end
- context 'in description' do
- before do
- update_mentionable(description: mentioned_user.to_reference)
+ context 'when mentioning a user and a group without access to' do
+ shared_examples 'updating attribute with not allowed mentions' do |attribute|
+ before do
+ update_mentionable(
+ { attribute => "For #{external_group.to_reference}, cc: #{mentioned_user.to_reference}" }
+ )
+ end
+
+ it 'emails mentioned user' do
+ should_only_email(mentioned_user)
+ end
end
- it 'emails only the newly-mentioned user' do
- should_only_email(mentioned_user)
+ context 'when the group is private' do
+ it_behaves_like 'updating attribute with not allowed mentions', :title
+ it_behaves_like 'updating attribute with not allowed mentions', :description
end
end
end
diff --git a/spec/support/shared_examples/versioned_description_shared_examples.rb b/spec/support/shared_examples/versioned_description_shared_examples.rb
new file mode 100644
index 00000000000..59124af19ec
--- /dev/null
+++ b/spec/support/shared_examples/versioned_description_shared_examples.rb
@@ -0,0 +1,65 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'versioned description' do
+ describe 'associations' do
+ it { is_expected.to have_many(:description_versions) }
+ end
+
+ describe 'save_description_version' do
+ let(:factory_name) { described_class.name.underscore.to_sym }
+ let!(:model) { create(factory_name, description: 'Original description') }
+
+ context 'when feature is enabled' do
+ before do
+ stub_feature_flags(save_description_versions: true)
+ end
+
+ context 'when description was changed' do
+ before do
+ model.update!(description: 'New description')
+ end
+
+ it 'saves the old and new description for the first update' do
+ expect(model.description_versions.first.description).to eq('Original description')
+ expect(model.description_versions.last.description).to eq('New description')
+ end
+
+ it 'only saves the new description for subsequent updates' do
+ expect { model.update!(description: 'Another description') }.to change { model.description_versions.count }.by(1)
+
+ expect(model.description_versions.last.description).to eq('Another description')
+ end
+
+ it 'sets the new description version to `saved_description_version`' do
+ expect(model.saved_description_version).to eq(model.description_versions.last)
+ end
+
+ it 'clears `saved_description_version` after another save that does not change description' do
+ model.save!
+
+ expect(model.saved_description_version).to be_nil
+ end
+ end
+
+ context 'when description was not changed' do
+ it 'does not save any description version' do
+ expect { model.save! }.not_to change { model.description_versions.count }
+
+ expect(model.saved_description_version).to be_nil
+ end
+ end
+ end
+
+ context 'when feature is disabled' do
+ before do
+ stub_feature_flags(save_description_versions: false)
+ end
+
+ it 'does not save any description version' do
+ expect { model.update!(description: 'New description') }.not_to change { model.description_versions.count }
+
+ expect(model.saved_description_version).to be_nil
+ end
+ end
+ end
+end
diff --git a/spec/tasks/cache/clear/redis_spec.rb b/spec/tasks/cache/clear/redis_spec.rb
index 97c8c943f3a..4f597988763 100644
--- a/spec/tasks/cache/clear/redis_spec.rb
+++ b/spec/tasks/cache/clear/redis_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'rake_helper'
describe 'clearing redis cache' do
diff --git a/spec/tasks/config_lint_spec.rb b/spec/tasks/config_lint_spec.rb
index 83d54259dfa..c6c11d76388 100644
--- a/spec/tasks/config_lint_spec.rb
+++ b/spec/tasks/config_lint_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'rake_helper'
Rake.application.rake_require 'tasks/config_lint'
diff --git a/spec/tasks/gitlab/artifacts/check_rake_spec.rb b/spec/tasks/gitlab/artifacts/check_rake_spec.rb
index d495b08aca0..04015f0e21a 100644
--- a/spec/tasks/gitlab/artifacts/check_rake_spec.rb
+++ b/spec/tasks/gitlab/artifacts/check_rake_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'rake_helper'
describe 'gitlab:artifacts rake tasks' do
diff --git a/spec/tasks/gitlab/artifacts/migrate_rake_spec.rb b/spec/tasks/gitlab/artifacts/migrate_rake_spec.rb
index be69c10d7c8..55bfb7acd9d 100644
--- a/spec/tasks/gitlab/artifacts/migrate_rake_spec.rb
+++ b/spec/tasks/gitlab/artifacts/migrate_rake_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'rake_helper'
describe 'gitlab:artifacts namespace rake task' do
@@ -11,10 +13,11 @@ describe 'gitlab:artifacts namespace rake task' do
stub_artifacts_object_storage(enabled: object_storage_enabled)
end
- subject { run_rake_task('gitlab:artifacts:migrate') }
+ describe 'gitlab:artifacts:migrate' do
+ subject { run_rake_task('gitlab:artifacts:migrate') }
- context 'job artifacts' do
let!(:artifact) { create(:ci_job_artifact, :archive, file_store: store) }
+ let!(:job_trace) { create(:ci_job_artifact, :trace, file_store: store) }
context 'when local storage is used' do
let(:store) { ObjectStorage::Store::LOCAL }
@@ -27,6 +30,7 @@ describe 'gitlab:artifacts namespace rake task' do
subject
expect(artifact.reload.file_store).to eq(ObjectStorage::Store::REMOTE)
+ expect(job_trace.reload.file_store).to eq(ObjectStorage::Store::REMOTE)
end
end
@@ -37,6 +41,7 @@ describe 'gitlab:artifacts namespace rake task' do
subject
expect(artifact.reload.file_store).to eq(ObjectStorage::Store::REMOTE)
+ expect(job_trace.reload.file_store).to eq(ObjectStorage::Store::REMOTE)
end
end
@@ -45,6 +50,7 @@ describe 'gitlab:artifacts namespace rake task' do
subject
expect(artifact.reload.file_store).to eq(ObjectStorage::Store::LOCAL)
+ expect(job_trace.reload.file_store).to eq(ObjectStorage::Store::LOCAL)
end
end
end
@@ -57,6 +63,40 @@ describe 'gitlab:artifacts namespace rake task' do
subject
expect(artifact.reload.file_store).to eq(ObjectStorage::Store::REMOTE)
+ expect(job_trace.reload.file_store).to eq(ObjectStorage::Store::REMOTE)
+ end
+ end
+ end
+
+ describe 'gitlab:artifacts:migrate_to_local' do
+ let(:object_storage_enabled) { true }
+
+ subject { run_rake_task('gitlab:artifacts:migrate_to_local') }
+
+ let!(:artifact) { create(:ci_job_artifact, :archive, file_store: store) }
+ let!(:job_trace) { create(:ci_job_artifact, :trace, file_store: store) }
+
+ context 'when remote storage is used' do
+ let(:store) { ObjectStorage::Store::REMOTE }
+
+ context 'and job has remote file store defined' do
+ it "migrates file to local storage" do
+ subject
+
+ expect(artifact.reload.file_store).to eq(ObjectStorage::Store::LOCAL)
+ expect(job_trace.reload.file_store).to eq(ObjectStorage::Store::LOCAL)
+ end
+ end
+ end
+
+ context 'when local storage is used' do
+ let(:store) { ObjectStorage::Store::LOCAL }
+
+ it 'file stays on local storage' do
+ subject
+
+ expect(artifact.reload.file_store).to eq(ObjectStorage::Store::LOCAL)
+ expect(job_trace.reload.file_store).to eq(ObjectStorage::Store::LOCAL)
end
end
end
diff --git a/spec/tasks/gitlab/backup_rake_spec.rb b/spec/tasks/gitlab/backup_rake_spec.rb
index bdbd39475b9..e58919c8688 100644
--- a/spec/tasks/gitlab/backup_rake_spec.rb
+++ b/spec/tasks/gitlab/backup_rake_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
require 'rake'
@@ -115,6 +117,45 @@ describe 'gitlab:app namespace rake task' do
expect(raw_repo.empty?).to be(true)
end
end
+
+ context 'when the backup is restored' do
+ let!(:included_project) { create(:project, :repository) }
+
+ before do
+ expect { run_rake_task('gitlab:backup:create') }.to output.to_stdout
+
+ backup_tar = Dir.glob(File.join(Gitlab.config.backup.path, '*_gitlab_backup.tar')).last
+ allow(Dir).to receive(:glob).and_return([backup_tar])
+ allow(File).to receive(:exist?).and_return(true)
+ allow(Kernel).to receive(:system).and_return(true)
+ allow(FileUtils).to receive(:cp_r).and_return(true)
+ allow(FileUtils).to receive(:mv).and_return(true)
+ allow(YAML).to receive(:load_file)
+ .and_return({ gitlab_version: Gitlab::VERSION })
+
+ expect(Rake::Task['gitlab:db:drop_tables']).to receive(:invoke)
+ expect(Rake::Task['gitlab:backup:db:restore']).to receive(:invoke)
+ expect(Rake::Task['gitlab:backup:repo:restore']).to receive(:invoke)
+ expect(Rake::Task['gitlab:backup:builds:restore']).to receive(:invoke)
+ expect(Rake::Task['gitlab:backup:uploads:restore']).to receive(:invoke)
+ expect(Rake::Task['gitlab:backup:artifacts:restore']).to receive(:invoke)
+ expect(Rake::Task['gitlab:backup:pages:restore']).to receive(:invoke)
+ expect(Rake::Task['gitlab:backup:lfs:restore']).to receive(:invoke)
+ expect(Rake::Task['gitlab:backup:registry:restore']).to receive(:invoke)
+ expect(Rake::Task['gitlab:shell:setup']).to receive(:invoke)
+
+ # We only need a backup of the repositories for this test
+ stub_env('SKIP', 'db,uploads,builds,artifacts,lfs,registry')
+ end
+
+ it 'restores the data' do
+ expect { run_rake_task('gitlab:backup:restore') }.to output.to_stdout
+
+ raw_repo = included_project.repository.raw
+
+ expect(raw_repo.empty?).to be(false)
+ end
+ end
end # backup_restore task
describe 'backup' do
diff --git a/spec/tasks/gitlab/check_rake_spec.rb b/spec/tasks/gitlab/check_rake_spec.rb
index 0fcb9b269f3..b3c8ca03aec 100644
--- a/spec/tasks/gitlab/check_rake_spec.rb
+++ b/spec/tasks/gitlab/check_rake_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'rake_helper'
describe 'check.rake' do
@@ -17,6 +19,7 @@ describe 'check.rake' do
describe 'gitlab:check rake task' do
subject { run_rake_task('gitlab:check') }
+
let(:name) { 'GitLab subtasks' }
it_behaves_like 'system check rake task'
@@ -24,6 +27,7 @@ describe 'check.rake' do
describe 'gitlab:gitlab_shell:check rake task' do
subject { run_rake_task('gitlab:gitlab_shell:check') }
+
let(:name) { 'GitLab Shell' }
it_behaves_like 'system check rake task'
@@ -31,6 +35,7 @@ describe 'check.rake' do
describe 'gitlab:gitaly:check rake task' do
subject { run_rake_task('gitlab:gitaly:check') }
+
let(:name) { 'Gitaly' }
it_behaves_like 'system check rake task'
@@ -38,6 +43,7 @@ describe 'check.rake' do
describe 'gitlab:sidekiq:check rake task' do
subject { run_rake_task('gitlab:sidekiq:check') }
+
let(:name) { 'Sidekiq' }
it_behaves_like 'system check rake task'
@@ -45,6 +51,7 @@ describe 'check.rake' do
describe 'gitlab:incoming_email:check rake task' do
subject { run_rake_task('gitlab:incoming_email:check') }
+
let(:name) { 'Incoming Email' }
it_behaves_like 'system check rake task'
@@ -54,6 +61,7 @@ describe 'check.rake' do
include LdapHelpers
subject { run_rake_task('gitlab:ldap:check') }
+
let(:name) { 'LDAP' }
it_behaves_like 'system check rake task'
diff --git a/spec/tasks/gitlab/cleanup_rake_spec.rb b/spec/tasks/gitlab/cleanup_rake_spec.rb
index 4aee6d005a8..3c3e5eea838 100644
--- a/spec/tasks/gitlab/cleanup_rake_spec.rb
+++ b/spec/tasks/gitlab/cleanup_rake_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'rake_helper'
describe 'gitlab:cleanup rake tasks' do
@@ -5,74 +7,6 @@ describe 'gitlab:cleanup rake tasks' do
Rake.application.rake_require 'tasks/gitlab/cleanup'
end
- describe 'cleanup namespaces and repos' do
- let(:gitlab_shell) { Gitlab::Shell.new }
- let(:storage) { storages.keys.first }
- let(:storages) do
- {
- 'default' => Gitlab::GitalyClient::StorageSettings.new(@default_storage_hash.merge('path' => 'tmp/tests/default_storage'))
- }
- end
-
- before(:all) do
- @default_storage_hash = Gitlab.config.repositories.storages.default.to_h
- end
-
- before do
- allow(Gitlab.config.repositories).to receive(:storages).and_return(storages)
- end
-
- after do
- Gitlab::GitalyClient::StorageService.new(storage).delete_all_repositories
- end
-
- describe 'cleanup:repos' do
- before do
- gitlab_shell.add_namespace(storage, 'broken/project.git')
- gitlab_shell.add_namespace(storage, '@hashed/12/34/5678.git')
- end
-
- it 'moves it to an orphaned path' do
- now = Time.now
-
- Timecop.freeze(now) do
- run_rake_task('gitlab:cleanup:repos')
- repo_list = Gitlab::GitalyClient::StorageService.new(storage).list_directories(depth: 0)
-
- expect(repo_list.last).to include("broken+orphaned+#{now.to_i}")
- end
- end
-
- it 'ignores @hashed repos' do
- run_rake_task('gitlab:cleanup:repos')
-
- expect(gitlab_shell.exists?(storage, '@hashed/12/34/5678.git')).to be(true)
- end
- end
-
- describe 'cleanup:dirs' do
- it 'removes missing namespaces' do
- gitlab_shell.add_namespace(storage, "namespace_1/project.git")
- gitlab_shell.add_namespace(storage, "namespace_2/project.git")
- allow(Namespace).to receive(:pluck).and_return(['namespace_1'])
-
- stub_env('REMOVE', 'true')
- run_rake_task('gitlab:cleanup:dirs')
-
- expect(gitlab_shell.exists?(storage, 'namespace_1')).to be(true)
- expect(gitlab_shell.exists?(storage, 'namespace_2')).to be(false)
- end
-
- it 'ignores @hashed directory' do
- gitlab_shell.add_namespace(storage, '@hashed/12/34/5678.git')
-
- run_rake_task('gitlab:cleanup:dirs')
-
- expect(gitlab_shell.exists?(storage, '@hashed/12/34/5678.git')).to be(true)
- end
- end
- end
-
# A single integration test that is redundant with one part of the
# Gitlab::Cleanup::ProjectUploads spec.
#
diff --git a/spec/tasks/gitlab/db_rake_spec.rb b/spec/tasks/gitlab/db_rake_spec.rb
index 5818892d56a..49b9a572423 100644
--- a/spec/tasks/gitlab/db_rake_spec.rb
+++ b/spec/tasks/gitlab/db_rake_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
require 'rake'
diff --git a/spec/tasks/gitlab/git_rake_spec.rb b/spec/tasks/gitlab/git_rake_spec.rb
index 57b006e1a39..b8156e55ec7 100644
--- a/spec/tasks/gitlab/git_rake_spec.rb
+++ b/spec/tasks/gitlab/git_rake_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'rake_helper'
describe 'gitlab:git rake tasks' do
diff --git a/spec/tasks/gitlab/gitaly_rake_spec.rb b/spec/tasks/gitlab/gitaly_rake_spec.rb
index 2f3fc7839c1..0cc92680582 100644
--- a/spec/tasks/gitlab/gitaly_rake_spec.rb
+++ b/spec/tasks/gitlab/gitaly_rake_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'rake_helper'
describe 'gitlab:gitaly namespace rake task' do
diff --git a/spec/tasks/gitlab/info_rake_spec.rb b/spec/tasks/gitlab/info_rake_spec.rb
index ca74378a12a..8d6b3985380 100644
--- a/spec/tasks/gitlab/info_rake_spec.rb
+++ b/spec/tasks/gitlab/info_rake_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'rake_helper'
describe 'gitlab:env:info' do
diff --git a/spec/tasks/gitlab/ldap_rake_spec.rb b/spec/tasks/gitlab/ldap_rake_spec.rb
index 279234f2887..bbc3f625088 100644
--- a/spec/tasks/gitlab/ldap_rake_spec.rb
+++ b/spec/tasks/gitlab/ldap_rake_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'rake_helper'
describe 'gitlab:ldap:rename_provider rake task' do
diff --git a/spec/tasks/gitlab/lfs/check_rake_spec.rb b/spec/tasks/gitlab/lfs/check_rake_spec.rb
index 2610edf8bac..3d698efdcb6 100644
--- a/spec/tasks/gitlab/lfs/check_rake_spec.rb
+++ b/spec/tasks/gitlab/lfs/check_rake_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'rake_helper'
describe 'gitlab:lfs rake tasks' do
diff --git a/spec/tasks/gitlab/lfs/migrate_rake_spec.rb b/spec/tasks/gitlab/lfs/migrate_rake_spec.rb
index 66d1a192a96..fc7be0eebcd 100644
--- a/spec/tasks/gitlab/lfs/migrate_rake_spec.rb
+++ b/spec/tasks/gitlab/lfs/migrate_rake_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'rake_helper'
describe 'gitlab:lfs namespace rake task' do
@@ -5,32 +7,49 @@ describe 'gitlab:lfs namespace rake task' do
Rake.application.rake_require 'tasks/gitlab/lfs/migrate'
end
- describe 'migrate' do
+ context 'migration tasks' do
let(:local) { ObjectStorage::Store::LOCAL }
let(:remote) { ObjectStorage::Store::REMOTE }
- let!(:lfs_object) { create(:lfs_object, :with_file, file_store: local) }
- def lfs_migrate
- run_rake_task('gitlab:lfs:migrate')
+ before do
+ stub_lfs_object_storage(background_upload: false, direct_upload: false)
end
- context 'object storage disabled' do
- before do
- stub_lfs_object_storage(enabled: false)
+ describe 'migrate' do
+ subject { run_rake_task('gitlab:lfs:migrate') }
+
+ let!(:lfs_object) { create(:lfs_object, :with_file) }
+
+ context 'object storage disabled' do
+ before do
+ stub_lfs_object_storage(enabled: false)
+ end
+
+ it "doesn't migrate files" do
+ expect { subject }.not_to change { lfs_object.reload.file_store }
+ end
end
- it "doesn't migrate files" do
- expect { lfs_migrate }.not_to change { lfs_object.reload.file_store }
+ context 'object storage enabled' do
+ it 'migrates local file to object storage' do
+ expect { subject }.to change { lfs_object.reload.file_store }.from(local).to(remote)
+ end
end
end
- context 'object storage enabled' do
+ describe 'migrate_to_local' do
+ subject { run_rake_task('gitlab:lfs:migrate_to_local') }
+
+ let(:lfs_object) { create(:lfs_object, :with_file, :object_storage) }
+
before do
- stub_lfs_object_storage
+ stub_lfs_object_storage(background_upload: false, direct_upload: true)
end
- it 'migrates local file to object storage' do
- expect { lfs_migrate }.to change { lfs_object.reload.file_store }.from(local).to(remote)
+ context 'object storage enabled' do
+ it 'migrates remote files to local storage' do
+ expect { subject }.to change { lfs_object.reload.file_store }.from(remote).to(local)
+ end
end
end
end
diff --git a/spec/tasks/gitlab/shell_rake_spec.rb b/spec/tasks/gitlab/shell_rake_spec.rb
index c3e912b02c5..abad16be580 100644
--- a/spec/tasks/gitlab/shell_rake_spec.rb
+++ b/spec/tasks/gitlab/shell_rake_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'rake_helper'
describe 'gitlab:shell rake tasks' do
@@ -12,8 +14,10 @@ describe 'gitlab:shell rake tasks' do
storages = Gitlab::GitalyClient::StorageSettings.allow_disk_access do
Gitlab.config.repositories.storages.values.map(&:legacy_disk_path)
end
- expect(Kernel).to receive(:system).with('bin/install', *storages).and_call_original
- expect(Kernel).to receive(:system).with('bin/compile').and_call_original
+
+ expect_any_instance_of(Gitlab::TaskHelpers).to receive(:checkout_or_clone_version)
+ allow(Kernel).to receive(:system).with('bin/install', *storages).and_return(true)
+ allow(Kernel).to receive(:system).with('bin/compile').and_return(true)
run_rake_task('gitlab:shell:install')
end
diff --git a/spec/tasks/gitlab/storage_rake_spec.rb b/spec/tasks/gitlab/storage_rake_spec.rb
index 0e47408fc72..ae11e091000 100644
--- a/spec/tasks/gitlab/storage_rake_spec.rb
+++ b/spec/tasks/gitlab/storage_rake_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'rake_helper'
describe 'rake gitlab:storage:*', :sidekiq do
diff --git a/spec/tasks/gitlab/task_helpers_spec.rb b/spec/tasks/gitlab/task_helpers_spec.rb
index e9322ec4931..4b4f7d7c956 100644
--- a/spec/tasks/gitlab/task_helpers_spec.rb
+++ b/spec/tasks/gitlab/task_helpers_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
class TestHelpersTest
diff --git a/spec/tasks/gitlab/traces_rake_spec.rb b/spec/tasks/gitlab/traces_rake_spec.rb
deleted file mode 100644
index aaf0d7242dd..00000000000
--- a/spec/tasks/gitlab/traces_rake_spec.rb
+++ /dev/null
@@ -1,113 +0,0 @@
-require 'rake_helper'
-
-describe 'gitlab:traces rake tasks' do
- before do
- Rake.application.rake_require 'tasks/gitlab/traces'
- end
-
- describe 'gitlab:traces:archive' do
- shared_examples 'passes the job id to worker' do
- it do
- expect(ArchiveTraceWorker).to receive(:bulk_perform_async).with([[job.id]])
-
- run_rake_task('gitlab:traces:archive')
- end
- end
-
- shared_examples 'does not pass the job id to worker' do
- it do
- expect(ArchiveTraceWorker).not_to receive(:bulk_perform_async)
-
- run_rake_task('gitlab:traces:archive')
- end
- end
-
- context 'when trace file stored in default path' do
- let!(:job) { create(:ci_build, :success, :trace_live) }
-
- it_behaves_like 'passes the job id to worker'
- end
-
- context 'when trace is stored in database' do
- let!(:job) { create(:ci_build, :success) }
-
- before do
- job.update_column(:trace, 'trace in db')
- end
-
- it_behaves_like 'passes the job id to worker'
- end
-
- context 'when job has trace artifact' do
- let!(:job) { create(:ci_build, :success) }
-
- before do
- create(:ci_job_artifact, :trace, job: job)
- end
-
- it_behaves_like 'does not pass the job id to worker'
- end
-
- context 'when job is not finished yet' do
- let!(:build) { create(:ci_build, :running, :trace_live) }
-
- it_behaves_like 'does not pass the job id to worker'
- end
- end
-
- describe 'gitlab:traces:migrate' do
- let(:object_storage_enabled) { false }
-
- before do
- stub_artifacts_object_storage(enabled: object_storage_enabled)
- end
-
- subject { run_rake_task('gitlab:traces:migrate') }
-
- let!(:job_trace) { create(:ci_job_artifact, :trace, file_store: store) }
-
- context 'when local storage is used' do
- let(:store) { ObjectStorage::Store::LOCAL }
-
- context 'and job does not have file store defined' do
- let(:object_storage_enabled) { true }
- let(:store) { nil }
-
- it "migrates file to remote storage" do
- subject
-
- expect(job_trace.reload.file_store).to eq(ObjectStorage::Store::REMOTE)
- end
- end
-
- context 'and remote storage is defined' do
- let(:object_storage_enabled) { true }
-
- it "migrates file to remote storage" do
- subject
-
- expect(job_trace.reload.file_store).to eq(ObjectStorage::Store::REMOTE)
- end
- end
-
- context 'and remote storage is not defined' do
- it "fails to migrate to remote storage" do
- subject
-
- expect(job_trace.reload.file_store).to eq(ObjectStorage::Store::LOCAL)
- end
- end
- end
-
- context 'when remote storage is used' do
- let(:object_storage_enabled) { true }
- let(:store) { ObjectStorage::Store::REMOTE }
-
- it "file stays on remote storage" do
- subject
-
- expect(job_trace.reload.file_store).to eq(ObjectStorage::Store::REMOTE)
- end
- end
- end
-end
diff --git a/spec/tasks/gitlab/uploads/check_rake_spec.rb b/spec/tasks/gitlab/uploads/check_rake_spec.rb
index 5d597c66133..91f0cedb246 100644
--- a/spec/tasks/gitlab/uploads/check_rake_spec.rb
+++ b/spec/tasks/gitlab/uploads/check_rake_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'rake_helper'
describe 'gitlab:uploads rake tasks' do
diff --git a/spec/tasks/gitlab/uploads/migrate_rake_spec.rb b/spec/tasks/gitlab/uploads/migrate_rake_spec.rb
index 9588e8be5dc..2f773bdfeec 100644
--- a/spec/tasks/gitlab/uploads/migrate_rake_spec.rb
+++ b/spec/tasks/gitlab/uploads/migrate_rake_spec.rb
@@ -1,31 +1,44 @@
+# frozen_string_literal: true
+
require 'rake_helper'
-describe 'gitlab:uploads:migrate rake tasks' do
+describe 'gitlab:uploads:migrate and migrate_to_local rake tasks' do
let(:model_class) { nil }
let(:uploader_class) { nil }
let(:mounted_as) { nil }
let(:batch_size) { 3 }
before do
- stub_env('BATCH', batch_size.to_s)
+ stub_env('MIGRATION_BATCH_SIZE', batch_size.to_s)
stub_uploads_object_storage(uploader_class)
Rake.application.rake_require 'tasks/gitlab/uploads/migrate'
allow(ObjectStorage::MigrateUploadsWorker).to receive(:perform_async)
end
- def run
+ def run(task)
args = [uploader_class.to_s, model_class.to_s, mounted_as].compact
- run_rake_task("gitlab:uploads:migrate", *args)
+ run_rake_task(task, *args)
end
shared_examples 'enqueue jobs in batch' do |batch:|
- it do
+ it 'migrates local storage to remote object storage' do
expect(ObjectStorage::MigrateUploadsWorker)
.to receive(:perform_async).exactly(batch).times
- .and_return("A fake job.")
+ .and_return("A fake job.")
+
+ run('gitlab:uploads:migrate')
+ end
+
+ it 'migrates remote object storage to local storage' do
+ expect(Upload).to receive(:where).exactly(batch + 1).times { Upload.all }
+ expect(ObjectStorage::MigrateUploadsWorker)
+ .to receive(:perform_async)
+ .with(anything, model_class.name, mounted_as, ObjectStorage::Store::LOCAL)
+ .exactly(batch).times
+ .and_return("A fake job.")
- run
+ run('gitlab:uploads:migrate_to_local')
end
end
diff --git a/spec/tasks/gitlab/web_hook_rake_spec.rb b/spec/tasks/gitlab/web_hook_rake_spec.rb
index 7bdf33ff6b0..be31507000d 100644
--- a/spec/tasks/gitlab/web_hook_rake_spec.rb
+++ b/spec/tasks/gitlab/web_hook_rake_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'rake_helper'
describe 'gitlab:web_hook namespace rake tasks' do
diff --git a/spec/tasks/gitlab/workhorse_rake_spec.rb b/spec/tasks/gitlab/workhorse_rake_spec.rb
index 42516d36c67..b7877a84185 100644
--- a/spec/tasks/gitlab/workhorse_rake_spec.rb
+++ b/spec/tasks/gitlab/workhorse_rake_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'rake_helper'
describe 'gitlab:workhorse namespace rake task' do
diff --git a/spec/tasks/tokens_spec.rb b/spec/tasks/tokens_spec.rb
index 4188e7caccb..9c69155056a 100644
--- a/spec/tasks/tokens_spec.rb
+++ b/spec/tasks/tokens_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'rake_helper'
describe 'tokens rake tasks' do
diff --git a/spec/uploaders/attachment_uploader_spec.rb b/spec/uploaders/attachment_uploader_spec.rb
index a9415854d25..d5a92b9b317 100644
--- a/spec/uploaders/attachment_uploader_spec.rb
+++ b/spec/uploaders/attachment_uploader_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe AttachmentUploader do
diff --git a/spec/uploaders/avatar_uploader_spec.rb b/spec/uploaders/avatar_uploader_spec.rb
index 6aaec7a4fef..c0844360589 100644
--- a/spec/uploaders/avatar_uploader_spec.rb
+++ b/spec/uploaders/avatar_uploader_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe AvatarUploader do
diff --git a/spec/uploaders/external_diff_uploader_spec.rb b/spec/uploaders/external_diff_uploader_spec.rb
index 1c959770dc4..ee11085d54e 100644
--- a/spec/uploaders/external_diff_uploader_spec.rb
+++ b/spec/uploaders/external_diff_uploader_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe ExternalDiffUploader do
diff --git a/spec/uploaders/file_mover_spec.rb b/spec/uploaders/file_mover_spec.rb
index 5ee0a10f38d..c4a315b3fd6 100644
--- a/spec/uploaders/file_mover_spec.rb
+++ b/spec/uploaders/file_mover_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe FileMover do
diff --git a/spec/uploaders/file_uploader_spec.rb b/spec/uploaders/file_uploader_spec.rb
index 04206de3dc6..beb7aa3cf2c 100644
--- a/spec/uploaders/file_uploader_spec.rb
+++ b/spec/uploaders/file_uploader_spec.rb
@@ -1,9 +1,11 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe FileUploader do
let(:group) { create(:group, name: 'awesome') }
let(:project) { create(:project, :legacy_storage, namespace: group, name: 'project') }
- let(:uploader) { described_class.new(project) }
+ let(:uploader) { described_class.new(project, :avatar) }
let(:upload) { double(model: project, path: 'secret/foo.jpg') }
subject { uploader }
@@ -184,6 +186,14 @@ describe FileUploader do
end
end
+ describe '#replace_file_without_saving!' do
+ let(:replacement) { Tempfile.create('replacement.jpg') }
+
+ it 'replaces an existing file without changing its metadata' do
+ expect { subject.replace_file_without_saving! CarrierWave::SanitizedFile.new(replacement) }.not_to change { subject.upload }
+ end
+ end
+
context 'when remote file is used' do
let(:temp_file) { Tempfile.new("test") }
diff --git a/spec/uploaders/gitlab_uploader_spec.rb b/spec/uploaders/gitlab_uploader_spec.rb
index 3bee4875348..4329171f0be 100644
--- a/spec/uploaders/gitlab_uploader_spec.rb
+++ b/spec/uploaders/gitlab_uploader_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
require 'carrierwave/storage/fog'
@@ -69,6 +71,16 @@ describe GitlabUploader do
end
end
+ describe '#replace_file_without_saving!' do
+ it 'allows file to be replaced without triggering any callbacks' do
+ new_file = CarrierWave::SanitizedFile.new(Tempfile.new)
+
+ expect(subject).not_to receive(:with_callbacks)
+
+ subject.replace_file_without_saving!(new_file)
+ end
+ end
+
describe '#open' do
context 'when trace is stored in File storage' do
context 'when file exists' do
diff --git a/spec/uploaders/import_export_uploader_spec.rb b/spec/uploaders/import_export_uploader_spec.rb
index 2dea48e3a88..7e8937ff5a6 100644
--- a/spec/uploaders/import_export_uploader_spec.rb
+++ b/spec/uploaders/import_export_uploader_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe ImportExportUploader do
diff --git a/spec/uploaders/job_artifact_uploader_spec.rb b/spec/uploaders/job_artifact_uploader_spec.rb
index 061432f082a..60b5a6697b1 100644
--- a/spec/uploaders/job_artifact_uploader_spec.rb
+++ b/spec/uploaders/job_artifact_uploader_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe JobArtifactUploader do
diff --git a/spec/uploaders/lfs_object_uploader_spec.rb b/spec/uploaders/lfs_object_uploader_spec.rb
index 9f28510c3e4..1041e13d34f 100644
--- a/spec/uploaders/lfs_object_uploader_spec.rb
+++ b/spec/uploaders/lfs_object_uploader_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe LfsObjectUploader do
diff --git a/spec/uploaders/namespace_file_uploader_spec.rb b/spec/uploaders/namespace_file_uploader_spec.rb
index 77401814194..aa98b3e2828 100644
--- a/spec/uploaders/namespace_file_uploader_spec.rb
+++ b/spec/uploaders/namespace_file_uploader_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe NamespaceFileUploader do
diff --git a/spec/uploaders/object_storage_spec.rb b/spec/uploaders/object_storage_spec.rb
index 0cf486c7087..37b107ee36e 100644
--- a/spec/uploaders/object_storage_spec.rb
+++ b/spec/uploaders/object_storage_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
require 'carrierwave/storage/fog'
diff --git a/spec/uploaders/personal_file_uploader_spec.rb b/spec/uploaders/personal_file_uploader_spec.rb
index d9f0e2f3cb7..ec652af222d 100644
--- a/spec/uploaders/personal_file_uploader_spec.rb
+++ b/spec/uploaders/personal_file_uploader_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe PersonalFileUploader do
diff --git a/spec/uploaders/records_uploads_spec.rb b/spec/uploaders/records_uploads_spec.rb
index 046ad3406d9..71eff23c77c 100644
--- a/spec/uploaders/records_uploads_spec.rb
+++ b/spec/uploaders/records_uploads_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe RecordsUploads do
diff --git a/spec/uploaders/uploader_helper_spec.rb b/spec/uploaders/uploader_helper_spec.rb
index 7c9ec8ff59c..753f32a9570 100644
--- a/spec/uploaders/uploader_helper_spec.rb
+++ b/spec/uploaders/uploader_helper_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe UploaderHelper do
diff --git a/spec/uploaders/workers/object_storage/background_move_worker_spec.rb b/spec/uploaders/workers/object_storage/background_move_worker_spec.rb
index cc8970d2ba0..cf4872d6904 100644
--- a/spec/uploaders/workers/object_storage/background_move_worker_spec.rb
+++ b/spec/uploaders/workers/object_storage/background_move_worker_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe ObjectStorage::BackgroundMoveWorker do
diff --git a/spec/uploaders/workers/object_storage/migrate_uploads_worker_spec.rb b/spec/uploaders/workers/object_storage/migrate_uploads_worker_spec.rb
index da490cb02af..c7a4680e18d 100644
--- a/spec/uploaders/workers/object_storage/migrate_uploads_worker_spec.rb
+++ b/spec/uploaders/workers/object_storage/migrate_uploads_worker_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe ObjectStorage::MigrateUploadsWorker, :sidekiq do
@@ -11,8 +13,8 @@ describe ObjectStorage::MigrateUploadsWorker, :sidekiq do
let(:uploads) { Upload.all }
let(:to_store) { ObjectStorage::Store::REMOTE }
- def perform(uploads)
- described_class.new.perform(uploads.ids, model_class.to_s, mounted_as, to_store)
+ def perform(uploads, store = nil)
+ described_class.new.perform(uploads.ids, model_class.to_s, mounted_as, store || to_store)
rescue ObjectStorage::MigrateUploadsWorker::Report::MigrationFailures
# swallow
end
@@ -42,33 +44,23 @@ describe ObjectStorage::MigrateUploadsWorker, :sidekiq do
end
describe '.sanity_check!' do
- shared_examples 'raises a SanityCheckError' do
+ shared_examples 'raises a SanityCheckError' do |expected_message|
let(:mount_point) { nil }
it do
expect { described_class.sanity_check!(uploads, model_class, mount_point) }
- .to raise_error(described_class::SanityCheckError)
+ .to raise_error(described_class::SanityCheckError).with_message(expected_message)
end
end
- before do
- stub_const("WrongModel", Class.new)
- end
-
context 'uploader types mismatch' do
let!(:outlier) { create(:upload, uploader: 'GitlabUploader') }
- include_examples 'raises a SanityCheckError'
- end
-
- context 'model types mismatch' do
- let!(:outlier) { create(:upload, model_type: 'WrongModel') }
-
- include_examples 'raises a SanityCheckError'
+ include_examples 'raises a SanityCheckError', /Multiple uploaders found/
end
context 'mount point not found' do
- include_examples 'raises a SanityCheckError' do
+ include_examples 'raises a SanityCheckError', /Mount point [a-z:]+ not found in/ do
let(:mount_point) { :potato }
end
end
@@ -97,12 +89,28 @@ describe ObjectStorage::MigrateUploadsWorker, :sidekiq do
it_behaves_like 'outputs correctly', success: 10
- it 'migrates files' do
+ it 'migrates files to remote storage' do
perform(uploads)
expect(Upload.where(store: ObjectStorage::Store::LOCAL).count).to eq(0)
end
+ context 'reversed' do
+ let(:to_store) { ObjectStorage::Store::LOCAL }
+
+ before do
+ perform(uploads, ObjectStorage::Store::REMOTE)
+ end
+
+ it 'migrates files to local storage' do
+ expect(Upload.where(store: ObjectStorage::Store::REMOTE).count).to eq(10)
+
+ perform(uploads)
+
+ expect(Upload.where(store: ObjectStorage::Store::LOCAL).count).to eq(10)
+ end
+ end
+
context 'migration is unsuccessful' do
before do
allow_any_instance_of(ObjectStorage::Concern)
diff --git a/spec/validators/branch_filter_validator_spec.rb b/spec/validators/branch_filter_validator_spec.rb
index 3be54827431..957d1dd99bb 100644
--- a/spec/validators/branch_filter_validator_spec.rb
+++ b/spec/validators/branch_filter_validator_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe BranchFilterValidator do
@@ -6,25 +8,25 @@ describe BranchFilterValidator do
describe '#validates_each' do
it 'allows valid branch names' do
- validator.validate_each(hook, :push_events_branch_filter, "good_branch_name")
- validator.validate_each(hook, :push_events_branch_filter, "another/good_branch_name")
+ validator.validate_each(hook, :push_events_branch_filter, +"good_branch_name")
+ validator.validate_each(hook, :push_events_branch_filter, +"another/good_branch_name")
expect(hook.errors.empty?).to be true
end
it 'disallows bad branch names' do
- validator.validate_each(hook, :push_events_branch_filter, "bad branch~name")
+ validator.validate_each(hook, :push_events_branch_filter, +"bad branch~name")
expect(hook.errors[:push_events_branch_filter].empty?).to be false
end
it 'allows wildcards' do
- validator.validate_each(hook, :push_events_branch_filter, "features/*")
- validator.validate_each(hook, :push_events_branch_filter, "features/*/bla")
- validator.validate_each(hook, :push_events_branch_filter, "*-stable")
+ validator.validate_each(hook, :push_events_branch_filter, +"features/*")
+ validator.validate_each(hook, :push_events_branch_filter, +"features/*/bla")
+ validator.validate_each(hook, :push_events_branch_filter, +"*-stable")
expect(hook.errors.empty?).to be true
end
it 'gets rid of whitespace' do
- filter = ' master '
+ filter = +' master '
validator.validate_each(hook, :push_events_branch_filter, filter)
expect(filter).to eq 'master'
diff --git a/spec/validators/js_regex_validator_spec.rb b/spec/validators/js_regex_validator_spec.rb
index 4d3bafaf267..dcc5e7ed4e8 100644
--- a/spec/validators/js_regex_validator_spec.rb
+++ b/spec/validators/js_regex_validator_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe JsRegexValidator do
diff --git a/spec/validators/named_ecdsa_key_validator_spec.rb b/spec/validators/named_ecdsa_key_validator_spec.rb
index 7e3ceb1cbad..044c5b84a56 100644
--- a/spec/validators/named_ecdsa_key_validator_spec.rb
+++ b/spec/validators/named_ecdsa_key_validator_spec.rb
@@ -43,7 +43,7 @@ describe NamedEcdsaKeyValidator do
context 'with ECDSA certificate with explicit curve params' do
let(:value) { attributes_for(:pages_domain, :explicit_ecdsa)[:key] }
- it 'adds errors', :quarantine do
+ it 'adds errors' do
expect(value).to be_present
subject
diff --git a/spec/validators/namespace_path_validator_spec.rb b/spec/validators/namespace_path_validator_spec.rb
index 61e2845f35f..bc9834dbd2a 100644
--- a/spec/validators/namespace_path_validator_spec.rb
+++ b/spec/validators/namespace_path_validator_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe NamespacePathValidator do
@@ -5,7 +7,7 @@ describe NamespacePathValidator do
describe '.valid_path?' do
it 'handles invalid utf8' do
- expect(described_class.valid_path?("a\0weird\255path")).to be_falsey
+ expect(described_class.valid_path?(+"a\0weird\255path")).to be_falsey
end
end
diff --git a/spec/validators/project_path_validator_spec.rb b/spec/validators/project_path_validator_spec.rb
index 8bb5e72dc22..d6046f7214b 100644
--- a/spec/validators/project_path_validator_spec.rb
+++ b/spec/validators/project_path_validator_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe ProjectPathValidator do
@@ -5,7 +7,7 @@ describe ProjectPathValidator do
describe '.valid_path?' do
it 'handles invalid utf8' do
- expect(described_class.valid_path?("a\0weird\255path")).to be_falsey
+ expect(described_class.valid_path?(+"a\0weird\255path")).to be_falsey
end
end
diff --git a/spec/validators/public_url_validator_spec.rb b/spec/validators/public_url_validator_spec.rb
index 3cbf1002730..c81232d9b78 100644
--- a/spec/validators/public_url_validator_spec.rb
+++ b/spec/validators/public_url_validator_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe PublicUrlValidator do
diff --git a/spec/validators/variable_duplicates_validator_spec.rb b/spec/validators/variable_duplicates_validator_spec.rb
index 0b71a67f94d..f48ebee7e0e 100644
--- a/spec/validators/variable_duplicates_validator_spec.rb
+++ b/spec/validators/variable_duplicates_validator_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe VariableDuplicatesValidator do
diff --git a/spec/validators/x509_certificate_credentials_validator_spec.rb b/spec/validators/x509_certificate_credentials_validator_spec.rb
index 24ef68c1fab..2a5a322622f 100644
--- a/spec/validators/x509_certificate_credentials_validator_spec.rb
+++ b/spec/validators/x509_certificate_credentials_validator_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe X509CertificateCredentialsValidator do
diff --git a/spec/views/admin/application_settings/_repository_storage.html.haml_spec.rb b/spec/views/admin/application_settings/_repository_storage.html.haml_spec.rb
new file mode 100644
index 00000000000..3029bfb6df5
--- /dev/null
+++ b/spec/views/admin/application_settings/_repository_storage.html.haml_spec.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'admin/application_settings/_repository_storage.html.haml' do
+ let(:app_settings) { build(:application_setting) }
+ let(:storages) do
+ {
+ "mepmep" => { "path" => "/tmp" },
+ "foobar" => { "path" => "/tmp" }
+ }
+ end
+
+ before do
+ assign(:application_setting, app_settings)
+ stub_storage_settings(storages)
+ end
+
+ context 'when multiple storages are available' do
+ it 'lists them all' do
+ render
+
+ storages.keys.each do |storage_name|
+ expect(rendered).to have_content(storage_name)
+ end
+ end
+ end
+end
diff --git a/spec/views/admin/dashboard/index.html.haml_spec.rb b/spec/views/admin/dashboard/index.html.haml_spec.rb
index 0e8b7c82d3a..93fedde6e96 100644
--- a/spec/views/admin/dashboard/index.html.haml_spec.rb
+++ b/spec/views/admin/dashboard/index.html.haml_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe 'admin/dashboard/index.html.haml' do
@@ -15,6 +17,7 @@ describe 'admin/dashboard/index.html.haml' do
allow(view).to receive(:admin?).and_return(true)
allow(view).to receive(:current_application_settings).and_return(Gitlab::CurrentSettings.current_application_settings)
+ allow(view).to receive(:show_license_breakdown?).and_return(false)
end
it "shows version of GitLab Workhorse" do
diff --git a/spec/views/admin/sessions/new.html.haml_spec.rb b/spec/views/admin/sessions/new.html.haml_spec.rb
new file mode 100644
index 00000000000..57255748988
--- /dev/null
+++ b/spec/views/admin/sessions/new.html.haml_spec.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'admin/sessions/new.html.haml' do
+ context 'admin has password set' do
+ before do
+ allow(view).to receive(:password_authentication_enabled_for_web?).and_return(true)
+ end
+
+ it "shows enter password form" do
+ render
+
+ expect(rendered).to have_css('#login-pane.active')
+ expect(rendered).to have_selector('input[name="password"]')
+ end
+ end
+
+ context 'admin has no password set' do
+ before do
+ allow(view).to receive(:password_authentication_enabled_for_web?).and_return(false)
+ end
+
+ it "warns authentication not possible" do
+ render
+
+ expect(rendered).not_to have_css('#login-pane')
+ expect(rendered).to have_content 'No authentication methods configured'
+ end
+ end
+end
diff --git a/spec/views/ci/status/_badge.html.haml_spec.rb b/spec/views/ci/status/_badge.html.haml_spec.rb
index 49f57969239..59db828a0c7 100644
--- a/spec/views/ci/status/_badge.html.haml_spec.rb
+++ b/spec/views/ci/status/_badge.html.haml_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe 'ci/status/_badge' do
diff --git a/spec/views/dashboard/projects/_blank_state_admin_welcome.haml_spec.rb b/spec/views/dashboard/projects/_blank_state_admin_welcome.haml_spec.rb
index 2f58eec86dc..bc92278bb22 100644
--- a/spec/views/dashboard/projects/_blank_state_admin_welcome.haml_spec.rb
+++ b/spec/views/dashboard/projects/_blank_state_admin_welcome.haml_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe 'dashboard/projects/_blank_state_admin_welcome.html.haml' do
diff --git a/spec/views/dashboard/projects/_nav.html.haml_spec.rb b/spec/views/dashboard/projects/_nav.html.haml_spec.rb
index cbdd3c0acc3..61b6bfef120 100644
--- a/spec/views/dashboard/projects/_nav.html.haml_spec.rb
+++ b/spec/views/dashboard/projects/_nav.html.haml_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe 'dashboard/projects/_nav.html.haml' do
diff --git a/spec/views/devise/shared/_signin_box.html.haml_spec.rb b/spec/views/devise/shared/_signin_box.html.haml_spec.rb
index 0563984a03c..f8867477603 100644
--- a/spec/views/devise/shared/_signin_box.html.haml_spec.rb
+++ b/spec/views/devise/shared/_signin_box.html.haml_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe 'devise/shared/_signin_box' do
@@ -8,6 +10,7 @@ describe 'devise/shared/_signin_box' do
allow(view).to receive(:current_application_settings).and_return(Gitlab::CurrentSettings.current_application_settings)
allow(view).to receive(:captcha_enabled?).and_return(false)
allow(view).to receive(:captcha_on_login_required?).and_return(false)
+ allow(view).to receive(:experiment_enabled?).and_return(false)
end
it 'is shown when Crowd is enabled' do
diff --git a/spec/views/errors/access_denied.html.haml_spec.rb b/spec/views/errors/access_denied.html.haml_spec.rb
index bde2f6f0169..b2e82847ab9 100644
--- a/spec/views/errors/access_denied.html.haml_spec.rb
+++ b/spec/views/errors/access_denied.html.haml_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe 'errors/access_denied' do
diff --git a/spec/views/events/event/_push.html.haml_spec.rb b/spec/views/events/event/_push.html.haml_spec.rb
index f5634de4916..d33a8aa86fc 100644
--- a/spec/views/events/event/_push.html.haml_spec.rb
+++ b/spec/views/events/event/_push.html.haml_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe 'events/event/_push.html.haml' do
@@ -26,6 +28,23 @@ describe 'events/event/_push.html.haml' do
expect(rendered).not_to have_link(event.ref_name)
end
end
+
+ context 'ref_count is more than 1' do
+ let(:payload) do
+ build_stubbed(
+ :push_event_payload,
+ event: event,
+ ref_count: 4,
+ ref_type: :branch
+ )
+ end
+
+ it 'includes the count in the text' do
+ render partial: 'events/event/push', locals: { event: event }
+
+ expect(rendered).to include('4 branches')
+ end
+ end
end
context 'with a tag' do
@@ -51,5 +70,22 @@ describe 'events/event/_push.html.haml' do
expect(rendered).not_to have_link(event.ref_name)
end
end
+
+ context 'ref_count is more than 1' do
+ let(:payload) do
+ build_stubbed(
+ :push_event_payload,
+ event: event,
+ ref_count: 4,
+ ref_type: :tag
+ )
+ end
+
+ it 'includes the count in the text' do
+ render partial: 'events/event/push', locals: { event: event }
+
+ expect(rendered).to include('4 tags')
+ end
+ end
end
end
diff --git a/spec/views/groups/_home_panel.html.haml_spec.rb b/spec/views/groups/_home_panel.html.haml_spec.rb
index 91c5ca261b9..8960dfa67db 100644
--- a/spec/views/groups/_home_panel.html.haml_spec.rb
+++ b/spec/views/groups/_home_panel.html.haml_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe 'groups/_home_panel' do
diff --git a/spec/views/groups/edit.html.haml_spec.rb b/spec/views/groups/edit.html.haml_spec.rb
index 0da3470433c..9fc850841ee 100644
--- a/spec/views/groups/edit.html.haml_spec.rb
+++ b/spec/views/groups/edit.html.haml_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe 'groups/edit.html.haml' do
diff --git a/spec/views/help/instance_configuration.html.haml_spec.rb b/spec/views/help/instance_configuration.html.haml_spec.rb
index 18628ddebf7..81d569b5cf4 100644
--- a/spec/views/help/instance_configuration.html.haml_spec.rb
+++ b/spec/views/help/instance_configuration.html.haml_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe 'help/instance_configuration' do
diff --git a/spec/views/layouts/_head.html.haml_spec.rb b/spec/views/layouts/_head.html.haml_spec.rb
index d7f24950e6f..e9b3334fffc 100644
--- a/spec/views/layouts/_head.html.haml_spec.rb
+++ b/spec/views/layouts/_head.html.haml_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe 'layouts/_head' do
@@ -5,6 +7,7 @@ describe 'layouts/_head' do
before do
allow(view).to receive(:current_application_settings).and_return(Gitlab::CurrentSettings.current_application_settings)
+ allow(view).to receive(:experiment_enabled?).and_return(false)
end
it 'escapes HTML-safe strings in page_title' do
diff --git a/spec/views/layouts/nav/sidebar/_admin.html.haml_spec.rb b/spec/views/layouts/nav/sidebar/_admin.html.haml_spec.rb
index bf63021a7fa..9ec65dba586 100644
--- a/spec/views/layouts/nav/sidebar/_admin.html.haml_spec.rb
+++ b/spec/views/layouts/nav/sidebar/_admin.html.haml_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe 'layouts/nav/sidebar/_admin' do
diff --git a/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb b/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb
index b627b9dba59..7decfa58153 100644
--- a/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb
+++ b/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe 'layouts/nav/sidebar/_project' do
diff --git a/spec/views/notify/pipeline_failed_email.html.haml_spec.rb b/spec/views/notify/pipeline_failed_email.html.haml_spec.rb
index bf633a118ca..b821b015c97 100644
--- a/spec/views/notify/pipeline_failed_email.html.haml_spec.rb
+++ b/spec/views/notify/pipeline_failed_email.html.haml_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe 'notify/pipeline_failed_email.html.haml' do
@@ -33,6 +35,8 @@ describe 'notify/pipeline_failed_email.html.haml' do
expect(rendered).to have_content "##{pipeline.id}"
expect(rendered).to have_content pipeline.user.name
end
+
+ it_behaves_like 'correct pipeline information for pipelines for merge requests'
end
context 'pipeline without user' do
diff --git a/spec/views/notify/pipeline_failed_email.text.erb_spec.rb b/spec/views/notify/pipeline_failed_email.text.erb_spec.rb
index 060274eb56a..d15969acf83 100644
--- a/spec/views/notify/pipeline_failed_email.text.erb_spec.rb
+++ b/spec/views/notify/pipeline_failed_email.text.erb_spec.rb
@@ -36,4 +36,6 @@ describe 'notify/pipeline_failed_email.text.erb' do
expect(rendered).to have_content(pipeline.user.name)
expect(rendered).to have_content("/-/jobs/#{job.id}/raw")
end
+
+ it_behaves_like 'correct pipeline information for pipelines for merge requests'
end
diff --git a/spec/views/notify/pipeline_success_email.html.haml_spec.rb b/spec/views/notify/pipeline_success_email.html.haml_spec.rb
index 46a6c908863..fbf33b7ec35 100644
--- a/spec/views/notify/pipeline_success_email.html.haml_spec.rb
+++ b/spec/views/notify/pipeline_success_email.html.haml_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe 'notify/pipeline_success_email.html.haml' do
@@ -33,6 +35,8 @@ describe 'notify/pipeline_success_email.html.haml' do
expect(rendered).to have_content "##{pipeline.id}"
expect(rendered).to have_content pipeline.user.name
end
+
+ it_behaves_like 'correct pipeline information for pipelines for merge requests'
end
context 'pipeline without user' do
diff --git a/spec/views/notify/pipeline_success_email.text.erb_spec.rb b/spec/views/notify/pipeline_success_email.text.erb_spec.rb
new file mode 100644
index 00000000000..ba4633bc346
--- /dev/null
+++ b/spec/views/notify/pipeline_success_email.text.erb_spec.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'notify/pipeline_success_email.text.erb' do
+ let(:user) { create(:user, developer_projects: [project]) }
+ let(:project) { create(:project, :repository) }
+ let(:merge_request) { create(:merge_request, :simple, source_project: project) }
+
+ let(:pipeline) do
+ create(:ci_pipeline,
+ :success,
+ project: project,
+ user: user,
+ ref: project.default_branch,
+ sha: project.commit.sha)
+ end
+
+ before do
+ assign(:project, project)
+ assign(:pipeline, pipeline)
+ assign(:merge_request, merge_request)
+ end
+
+ it_behaves_like 'correct pipeline information for pipelines for merge requests'
+end
diff --git a/spec/views/profiles/show.html.haml_spec.rb b/spec/views/profiles/show.html.haml_spec.rb
index 9e7ac0b84fa..592b3a56ba3 100644
--- a/spec/views/profiles/show.html.haml_spec.rb
+++ b/spec/views/profiles/show.html.haml_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe 'profiles/show' do
diff --git a/spec/views/projects/_home_panel.html.haml_spec.rb b/spec/views/projects/_home_panel.html.haml_spec.rb
index 12925a5ab07..4d5b369e88e 100644
--- a/spec/views/projects/_home_panel.html.haml_spec.rb
+++ b/spec/views/projects/_home_panel.html.haml_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe 'projects/_home_panel' do
diff --git a/spec/views/projects/artifacts/_artifact.html.haml_spec.rb b/spec/views/projects/artifacts/_artifact.html.haml_spec.rb
new file mode 100644
index 00000000000..460b63efa2f
--- /dev/null
+++ b/spec/views/projects/artifacts/_artifact.html.haml_spec.rb
@@ -0,0 +1,74 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe "projects/artifacts/_artifact.html.haml" do
+ let(:project) { create(:project) }
+
+ describe 'delete button' do
+ before do
+ create(:ci_build, :artifacts, project: project)
+
+ allow(view).to receive(:current_user).and_return(user)
+ assign(:project, project)
+ end
+
+ context 'with admin' do
+ let(:user) { build(:admin) }
+
+ it 'has a delete button' do
+ render_partial
+
+ expect(rendered).to have_link('Delete artifacts', href: project_artifact_path(project, project.job_artifacts.first))
+ end
+ end
+
+ context 'with owner' do
+ let(:user) { create(:user) }
+ let(:project) { build(:project, namespace: user.namespace) }
+
+ it 'has a delete button' do
+ render_partial
+
+ expect(rendered).to have_link('Delete artifacts', href: project_artifact_path(project, project.job_artifacts.first))
+ end
+ end
+
+ context 'with master' do
+ let(:user) { create(:user) }
+
+ it 'has a delete button' do
+ allow_any_instance_of(ProjectTeam).to receive(:max_member_access).and_return(Gitlab::Access::MASTER)
+ render_partial
+
+ expect(rendered).to have_link('Delete artifacts', href: project_artifact_path(project, project.job_artifacts.first))
+ end
+ end
+
+ context 'with developer' do
+ let(:user) { build(:user) }
+
+ it 'has no delete button' do
+ project.add_developer(user)
+ render_partial
+
+ expect(rendered).not_to have_link('Delete artifacts')
+ end
+ end
+
+ context 'with reporter' do
+ let(:user) { build(:user) }
+
+ it 'has no delete button' do
+ project.add_reporter(user)
+ render_partial
+
+ expect(rendered).not_to have_link('Delete artifacts')
+ end
+ end
+ end
+
+ def render_partial
+ render partial: 'projects/artifacts/artifact', collection: project.job_artifacts, as: :artifact
+ end
+end
diff --git a/spec/views/projects/blob/_viewer.html.haml_spec.rb b/spec/views/projects/blob/_viewer.html.haml_spec.rb
index 95f7f87d37b..a798a72fa76 100644
--- a/spec/views/projects/blob/_viewer.html.haml_spec.rb
+++ b/spec/views/projects/blob/_viewer.html.haml_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe 'projects/blob/_viewer.html.haml' do
diff --git a/spec/views/projects/buttons/_dropdown.html.haml_spec.rb b/spec/views/projects/buttons/_dropdown.html.haml_spec.rb
index 8b9aab30286..2bc1de040d5 100644
--- a/spec/views/projects/buttons/_dropdown.html.haml_spec.rb
+++ b/spec/views/projects/buttons/_dropdown.html.haml_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe 'projects/buttons/_dropdown' do
diff --git a/spec/views/projects/ci/lints/show.html.haml_spec.rb b/spec/views/projects/ci/lints/show.html.haml_spec.rb
index 2f0cd38c14a..ea67478ff98 100644
--- a/spec/views/projects/ci/lints/show.html.haml_spec.rb
+++ b/spec/views/projects/ci/lints/show.html.haml_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe 'projects/ci/lints/show' do
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 1086546c10d..07c530670d2 100644
--- a/spec/views/projects/commit/_commit_box.html.haml_spec.rb
+++ b/spec/views/projects/commit/_commit_box.html.haml_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe 'projects/commit/_commit_box.html.haml' do
diff --git a/spec/views/projects/commit/branches.html.haml_spec.rb b/spec/views/projects/commit/branches.html.haml_spec.rb
index b9d4dc80fe0..36da489a84f 100644
--- a/spec/views/projects/commit/branches.html.haml_spec.rb
+++ b/spec/views/projects/commit/branches.html.haml_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe 'projects/commit/branches.html.haml' do
diff --git a/spec/views/projects/commit/show.html.haml_spec.rb b/spec/views/projects/commit/show.html.haml_spec.rb
index d07099489e5..af28a8f9193 100644
--- a/spec/views/projects/commit/show.html.haml_spec.rb
+++ b/spec/views/projects/commit/show.html.haml_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe 'projects/commit/show.html.haml' do
diff --git a/spec/views/projects/commits/_commit.html.haml_spec.rb b/spec/views/projects/commits/_commit.html.haml_spec.rb
index 6bf1b5fd2d0..669bb596de5 100644
--- a/spec/views/projects/commits/_commit.html.haml_spec.rb
+++ b/spec/views/projects/commits/_commit.html.haml_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe 'projects/commits/_commit.html.haml' do
diff --git a/spec/views/projects/diffs/_stats.html.haml_spec.rb b/spec/views/projects/diffs/_stats.html.haml_spec.rb
index c7d2f85747c..eb853596f98 100644
--- a/spec/views/projects/diffs/_stats.html.haml_spec.rb
+++ b/spec/views/projects/diffs/_stats.html.haml_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe 'projects/diffs/_stats.html.haml' do
diff --git a/spec/views/projects/diffs/_viewer.html.haml_spec.rb b/spec/views/projects/diffs/_viewer.html.haml_spec.rb
index 8ac32db5585..1d5d6e1e78d 100644
--- a/spec/views/projects/diffs/_viewer.html.haml_spec.rb
+++ b/spec/views/projects/diffs/_viewer.html.haml_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe 'projects/diffs/_viewer.html.haml' do
diff --git a/spec/views/projects/edit.html.haml_spec.rb b/spec/views/projects/edit.html.haml_spec.rb
index 5c6b2e4b042..f576093ab45 100644
--- a/spec/views/projects/edit.html.haml_spec.rb
+++ b/spec/views/projects/edit.html.haml_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe 'projects/edit' do
diff --git a/spec/views/projects/environments/terminal.html.haml_spec.rb b/spec/views/projects/environments/terminal.html.haml_spec.rb
index d2e47225226..b0b08a84f8d 100644
--- a/spec/views/projects/environments/terminal.html.haml_spec.rb
+++ b/spec/views/projects/environments/terminal.html.haml_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe 'projects/environments/terminal' do
diff --git a/spec/views/projects/imports/new.html.haml_spec.rb b/spec/views/projects/imports/new.html.haml_spec.rb
index 11fe144d1d2..6cca369b9f6 100644
--- a/spec/views/projects/imports/new.html.haml_spec.rb
+++ b/spec/views/projects/imports/new.html.haml_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "spec_helper"
describe "projects/imports/new.html.haml" do
diff --git a/spec/views/projects/issues/_related_branches.html.haml_spec.rb b/spec/views/projects/issues/_related_branches.html.haml_spec.rb
index 5cff7694029..a6817e3fdbf 100644
--- a/spec/views/projects/issues/_related_branches.html.haml_spec.rb
+++ b/spec/views/projects/issues/_related_branches.html.haml_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe 'projects/issues/_related_branches' do
diff --git a/spec/views/projects/jobs/_build.html.haml_spec.rb b/spec/views/projects/jobs/_build.html.haml_spec.rb
index 1d58891036e..681df77ea99 100644
--- a/spec/views/projects/jobs/_build.html.haml_spec.rb
+++ b/spec/views/projects/jobs/_build.html.haml_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe 'projects/ci/jobs/_build' do
diff --git a/spec/views/projects/jobs/_generic_commit_status.html.haml_spec.rb b/spec/views/projects/jobs/_generic_commit_status.html.haml_spec.rb
index dc2ffc9dc47..f193ce0a552 100644
--- a/spec/views/projects/jobs/_generic_commit_status.html.haml_spec.rb
+++ b/spec/views/projects/jobs/_generic_commit_status.html.haml_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe 'projects/generic_commit_statuses/_generic_commit_status.html.haml' do
diff --git a/spec/views/projects/jobs/show.html.haml_spec.rb b/spec/views/projects/jobs/show.html.haml_spec.rb
index e06a9ecb98b..903a16d9e53 100644
--- a/spec/views/projects/jobs/show.html.haml_spec.rb
+++ b/spec/views/projects/jobs/show.html.haml_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe 'projects/jobs/show' do
diff --git a/spec/views/projects/merge_requests/_commits.html.haml_spec.rb b/spec/views/projects/merge_requests/_commits.html.haml_spec.rb
index a7628548de6..71d74b06f85 100644
--- a/spec/views/projects/merge_requests/_commits.html.haml_spec.rb
+++ b/spec/views/projects/merge_requests/_commits.html.haml_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe 'projects/merge_requests/_commits.html.haml' do
diff --git a/spec/views/projects/merge_requests/creations/_new_submit.html.haml_spec.rb b/spec/views/projects/merge_requests/creations/_new_submit.html.haml_spec.rb
index 88c4b52b3a6..f7db4a4d614 100644
--- a/spec/views/projects/merge_requests/creations/_new_submit.html.haml_spec.rb
+++ b/spec/views/projects/merge_requests/creations/_new_submit.html.haml_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe 'projects/merge_requests/creations/_new_submit.html.haml' do
diff --git a/spec/views/projects/merge_requests/diffs/_diffs.html.haml_spec.rb b/spec/views/projects/merge_requests/diffs/_diffs.html.haml_spec.rb
index e7c40421f1f..a82ef3c04b5 100644
--- a/spec/views/projects/merge_requests/diffs/_diffs.html.haml_spec.rb
+++ b/spec/views/projects/merge_requests/diffs/_diffs.html.haml_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe 'projects/merge_requests/diffs/_diffs.html.haml' do
diff --git a/spec/views/projects/merge_requests/edit.html.haml_spec.rb b/spec/views/projects/merge_requests/edit.html.haml_spec.rb
index 0a3a46210ed..74d9067076c 100644
--- a/spec/views/projects/merge_requests/edit.html.haml_spec.rb
+++ b/spec/views/projects/merge_requests/edit.html.haml_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe 'projects/merge_requests/edit.html.haml' do
diff --git a/spec/views/projects/merge_requests/show.html.haml_spec.rb b/spec/views/projects/merge_requests/show.html.haml_spec.rb
index 23cb319a202..6023527cb28 100644
--- a/spec/views/projects/merge_requests/show.html.haml_spec.rb
+++ b/spec/views/projects/merge_requests/show.html.haml_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe 'projects/merge_requests/show.html.haml' do
diff --git a/spec/views/projects/notes/_more_actions_dropdown.html.haml_spec.rb b/spec/views/projects/notes/_more_actions_dropdown.html.haml_spec.rb
index ae47f364296..a07523a4423 100644
--- a/spec/views/projects/notes/_more_actions_dropdown.html.haml_spec.rb
+++ b/spec/views/projects/notes/_more_actions_dropdown.html.haml_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe 'projects/notes/_more_actions_dropdown' do
diff --git a/spec/views/projects/pages_domains/show.html.haml_spec.rb b/spec/views/projects/pages_domains/show.html.haml_spec.rb
index da27a04bfe9..ba0544a49b0 100644
--- a/spec/views/projects/pages_domains/show.html.haml_spec.rb
+++ b/spec/views/projects/pages_domains/show.html.haml_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe 'projects/pages_domains/show' do
diff --git a/spec/views/projects/pipeline_schedules/_pipeline_schedule.html.haml_spec.rb b/spec/views/projects/pipeline_schedules/_pipeline_schedule.html.haml_spec.rb
index 5d60d6bc5e7..daf799ec13f 100644
--- a/spec/views/projects/pipeline_schedules/_pipeline_schedule.html.haml_spec.rb
+++ b/spec/views/projects/pipeline_schedules/_pipeline_schedule.html.haml_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe 'projects/pipeline_schedules/_pipeline_schedule' do
diff --git a/spec/views/projects/pipelines/_stage.html.haml_spec.rb b/spec/views/projects/pipelines/_stage.html.haml_spec.rb
index e40e16e742b..591602859c3 100644
--- a/spec/views/projects/pipelines/_stage.html.haml_spec.rb
+++ b/spec/views/projects/pipelines/_stage.html.haml_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe 'projects/pipelines/_stage' do
diff --git a/spec/views/projects/services/_form.haml_spec.rb b/spec/views/projects/services/_form.haml_spec.rb
index 06e159f103b..272ac97604a 100644
--- a/spec/views/projects/services/_form.haml_spec.rb
+++ b/spec/views/projects/services/_form.haml_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe 'projects/services/_form' do
diff --git a/spec/views/projects/settings/ci_cd/_autodevops_form.html.haml_spec.rb b/spec/views/projects/settings/ci_cd/_autodevops_form.html.haml_spec.rb
index 697e44be065..94a85486cfa 100644
--- a/spec/views/projects/settings/ci_cd/_autodevops_form.html.haml_spec.rb
+++ b/spec/views/projects/settings/ci_cd/_autodevops_form.html.haml_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe 'projects/settings/ci_cd/_autodevops_form' do
diff --git a/spec/views/projects/tags/index.html.haml_spec.rb b/spec/views/projects/tags/index.html.haml_spec.rb
index 34355e27544..66be085c290 100644
--- a/spec/views/projects/tags/index.html.haml_spec.rb
+++ b/spec/views/projects/tags/index.html.haml_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe 'projects/tags/index' do
@@ -23,6 +25,11 @@ describe 'projects/tags/index' do
expect(rendered).to have_button('Last updated')
end
+ it 'renders links to the Releases page for tags associated with a release' do
+ render
+ expect(rendered).to have_link(release.name, href: project_releases_path(project, anchor: release.tag))
+ end
+
context 'when the most recent build for a tag has artifacts' do
let!(:build) { create(:ci_build, :success, :artifacts, pipeline: pipeline) }
diff --git a/spec/views/projects/tree/_tree_header.html.haml_spec.rb b/spec/views/projects/tree/_tree_header.html.haml_spec.rb
new file mode 100644
index 00000000000..4b71ea9ffe3
--- /dev/null
+++ b/spec/views/projects/tree/_tree_header.html.haml_spec.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'projects/tree/_tree_header' do
+ let(:project) { create(:project, :repository) }
+ let(:current_user) { create(:user) }
+ let(:repository) { project.repository }
+
+ before do
+ assign(:project, project)
+ assign(:repository, repository)
+ assign(:id, File.join('master', ''))
+ assign(:ref, 'master')
+
+ allow(view).to receive(:current_user).and_return(current_user)
+ allow(view).to receive(:can_collaborate_with_project?) { true }
+ end
+
+ it 'does not render the WebIDE button when user cannot create fork or cannot open MR' do
+ allow(view).to receive(:can?) { false }
+
+ render
+
+ expect(rendered).not_to have_link('Web IDE')
+ end
+
+ it 'renders the WebIDE button when user can create fork and can open MR in project' do
+ allow(view).to receive(:can?) { true }
+
+ render
+
+ expect(rendered).to have_link('Web IDE')
+ end
+
+ it 'opens a popup confirming a fork if the user can create fork/MR but cannot collaborate with the project' do
+ allow(view).to receive(:can?) { true }
+ allow(view).to receive(:can_collaborate_with_project?) { false }
+
+ render
+
+ expect(rendered).to have_link('Web IDE', href: '#modal-confirm-fork')
+ end
+end
diff --git a/spec/views/projects/tree/_tree_row.html.haml_spec.rb b/spec/views/projects/tree/_tree_row.html.haml_spec.rb
index 3353b7665e2..ff2fe8aeb6c 100644
--- a/spec/views/projects/tree/_tree_row.html.haml_spec.rb
+++ b/spec/views/projects/tree/_tree_row.html.haml_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe 'projects/tree/_tree_row' do
diff --git a/spec/views/projects/tree/show.html.haml_spec.rb b/spec/views/projects/tree/show.html.haml_spec.rb
index 5bb0173ab89..960cf42a793 100644
--- a/spec/views/projects/tree/show.html.haml_spec.rb
+++ b/spec/views/projects/tree/show.html.haml_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe 'projects/tree/show' do
diff --git a/spec/views/shared/milestones/_issuable.html.haml_spec.rb b/spec/views/shared/milestones/_issuable.html.haml_spec.rb
index 0a3f877cae0..3c2b7c6305a 100644
--- a/spec/views/shared/milestones/_issuable.html.haml_spec.rb
+++ b/spec/views/shared/milestones/_issuable.html.haml_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe 'shared/milestones/_issuable.html.haml' do
diff --git a/spec/views/shared/milestones/_issuables.html.haml_spec.rb b/spec/views/shared/milestones/_issuables.html.haml_spec.rb
index 24b55338db3..f77c14a687b 100644
--- a/spec/views/shared/milestones/_issuables.html.haml_spec.rb
+++ b/spec/views/shared/milestones/_issuables.html.haml_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe 'shared/milestones/_issuables.html.haml' do
diff --git a/spec/views/shared/milestones/_top.html.haml_spec.rb b/spec/views/shared/milestones/_top.html.haml_spec.rb
index f2ee8be5857..944dfc8a27b 100644
--- a/spec/views/shared/milestones/_top.html.haml_spec.rb
+++ b/spec/views/shared/milestones/_top.html.haml_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe 'shared/milestones/_top.html.haml' do
diff --git a/spec/views/shared/notes/_form.html.haml_spec.rb b/spec/views/shared/notes/_form.html.haml_spec.rb
index 0189f926a5f..d354c2f0100 100644
--- a/spec/views/shared/notes/_form.html.haml_spec.rb
+++ b/spec/views/shared/notes/_form.html.haml_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe 'shared/notes/_form' do
diff --git a/spec/views/shared/projects/_project.html.haml_spec.rb b/spec/views/shared/projects/_project.html.haml_spec.rb
index dc223861037..b123be42074 100644
--- a/spec/views/shared/projects/_project.html.haml_spec.rb
+++ b/spec/views/shared/projects/_project.html.haml_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe 'shared/projects/_project.html.haml' do
diff --git a/spec/workers/create_evidence_worker_spec.rb b/spec/workers/create_evidence_worker_spec.rb
new file mode 100644
index 00000000000..364b2098251
--- /dev/null
+++ b/spec/workers/create_evidence_worker_spec.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe CreateEvidenceWorker do
+ let!(:release) { create(:release) }
+
+ it 'creates a new Evidence' do
+ expect { described_class.new.perform(release.id) }.to change(Evidence, :count).by(1)
+ end
+end
diff --git a/spec/workers/deployments/success_worker_spec.rb b/spec/workers/deployments/success_worker_spec.rb
index 1c68922b03d..7f2816d7535 100644
--- a/spec/workers/deployments/success_worker_spec.rb
+++ b/spec/workers/deployments/success_worker_spec.rb
@@ -8,8 +8,8 @@ describe Deployments::SuccessWorker do
context 'when successful deployment' do
let(:deployment) { create(:deployment, :success) }
- it 'executes UpdateDeploymentService' do
- expect(UpdateDeploymentService)
+ it 'executes Deployments::AfterCreateService' do
+ expect(Deployments::AfterCreateService)
.to receive(:new).with(deployment).and_call_original
subject
@@ -19,8 +19,8 @@ describe Deployments::SuccessWorker do
context 'when canceled deployment' do
let(:deployment) { create(:deployment, :canceled) }
- it 'does not execute UpdateDeploymentService' do
- expect(UpdateDeploymentService).not_to receive(:new)
+ it 'does not execute Deployments::AfterCreateService' do
+ expect(Deployments::AfterCreateService).not_to receive(:new)
subject
end
@@ -29,8 +29,8 @@ describe Deployments::SuccessWorker do
context 'when deploy record does not exist' do
let(:deployment) { nil }
- it 'does not execute UpdateDeploymentService' do
- expect(UpdateDeploymentService).not_to receive(:new)
+ it 'does not execute Deployments::AfterCreateService' do
+ expect(Deployments::AfterCreateService).not_to receive(:new)
subject
end
diff --git a/spec/workers/every_sidekiq_worker_spec.rb b/spec/workers/every_sidekiq_worker_spec.rb
index 8fddd8540ef..b7ba4d61723 100644
--- a/spec/workers/every_sidekiq_worker_spec.rb
+++ b/spec/workers/every_sidekiq_worker_spec.rb
@@ -35,4 +35,32 @@ describe 'Every Sidekiq worker' do
expect(config_queues).to include(queue).or(include(queue_namespace))
end
end
+
+ describe "feature category declarations" do
+ let(:feature_categories) do
+ YAML.load_file(Rails.root.join('config', 'feature_categories.yml')).map(&:to_sym).to_set
+ end
+
+ # All Sidekiq worker classes should declare a valid `feature_category`
+ # or explicitely be excluded with the `feature_category_not_owned!` annotation.
+ # Please see doc/development/sidekiq_style_guide.md#Feature-Categorization for more details.
+ it 'has a feature_category or feature_category_not_owned! attribute', :aggregate_failures do
+ Gitlab::SidekiqConfig.workers.each do |worker|
+ expect(worker.get_feature_category).to be_a(Symbol), "expected #{worker.inspect} to declare a feature_category or feature_category_not_owned!"
+ end
+ end
+
+ # All Sidekiq worker classes should declare a valid `feature_category`.
+ # The category should match a value in `config/feature_categories.yml`.
+ # Please see doc/development/sidekiq_style_guide.md#Feature-Categorization for more details.
+ it 'has a feature_category that maps to a value in feature_categories.yml', :aggregate_failures do
+ workers_with_feature_categories = Gitlab::SidekiqConfig.workers
+ .select(&:get_feature_category)
+ .reject(&:feature_category_not_owned?)
+
+ workers_with_feature_categories.each do |worker|
+ expect(feature_categories).to include(worker.get_feature_category), "expected #{worker.inspect} to declare a valid feature_category, but got #{worker.get_feature_category}"
+ end
+ end
+ end
end
diff --git a/spec/workers/hashed_storage/migrator_worker_spec.rb b/spec/workers/hashed_storage/migrator_worker_spec.rb
index a318cdd003e..12c1a26104e 100644
--- a/spec/workers/hashed_storage/migrator_worker_spec.rb
+++ b/spec/workers/hashed_storage/migrator_worker_spec.rb
@@ -4,6 +4,7 @@ require 'spec_helper'
describe HashedStorage::MigratorWorker do
subject(:worker) { described_class.new }
+
let(:projects) { create_list(:project, 2, :legacy_storage, :empty_repo) }
let(:ids) { projects.map(&:id) }
diff --git a/spec/workers/hashed_storage/rollbacker_worker_spec.rb b/spec/workers/hashed_storage/rollbacker_worker_spec.rb
index 4055f380978..5fcb1adf9ae 100644
--- a/spec/workers/hashed_storage/rollbacker_worker_spec.rb
+++ b/spec/workers/hashed_storage/rollbacker_worker_spec.rb
@@ -4,6 +4,7 @@ require 'spec_helper'
describe HashedStorage::RollbackerWorker do
subject(:worker) { described_class.new }
+
let(:projects) { create_list(:project, 2, :empty_repo) }
let(:ids) { projects.map(&:id) }
diff --git a/spec/workers/namespaceless_project_destroy_worker_spec.rb b/spec/workers/namespaceless_project_destroy_worker_spec.rb
index 4fbda37e268..e6686328291 100644
--- a/spec/workers/namespaceless_project_destroy_worker_spec.rb
+++ b/spec/workers/namespaceless_project_destroy_worker_spec.rb
@@ -9,7 +9,6 @@ describe NamespacelessProjectDestroyWorker do
before do
# Stub after_save callbacks that will fail when Project has no namespace
- allow_any_instance_of(Project).to receive(:ensure_storage_path_exists).and_return(nil)
allow_any_instance_of(Project).to receive(:update_project_statistics).and_return(nil)
end
diff --git a/spec/workers/new_release_worker_spec.rb b/spec/workers/new_release_worker_spec.rb
new file mode 100644
index 00000000000..9010c36f795
--- /dev/null
+++ b/spec/workers/new_release_worker_spec.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe NewReleaseWorker do
+ let(:release) { create(:release) }
+
+ it 'sends a new release notification' do
+ expect_any_instance_of(NotificationService).to receive(:send_new_release_notifications).with(release)
+
+ described_class.new.perform(release.id)
+ end
+end
diff --git a/spec/workers/object_pool/destroy_worker_spec.rb b/spec/workers/object_pool/destroy_worker_spec.rb
index ef74f0ba87c..52d457b4b71 100644
--- a/spec/workers/object_pool/destroy_worker_spec.rb
+++ b/spec/workers/object_pool/destroy_worker_spec.rb
@@ -16,7 +16,9 @@ describe ObjectPool::DestroyWorker do
subject { described_class.new }
it 'requests Gitaly to remove the object pool' do
- expect(Gitlab::GitalyClient).to receive(:call).with(pool.shard_name, :object_pool_service, :delete_object_pool, Object)
+ expect(Gitlab::GitalyClient).to receive(:call)
+ .with(pool.shard_name, :object_pool_service, :delete_object_pool,
+ Object, timeout: Gitlab::GitalyClient.long_timeout)
subject.perform(pool.id)
end
diff --git a/spec/workers/post_receive_spec.rb b/spec/workers/post_receive_spec.rb
index c8a0c22b0e8..34aaa9bb1e9 100644
--- a/spec/workers/post_receive_spec.rb
+++ b/spec/workers/post_receive_spec.rb
@@ -43,6 +43,7 @@ describe PostReceive do
before do
allow_any_instance_of(Gitlab::GitPostReceive).to receive(:identify).and_return(empty_project.owner)
+ # Need to mock here so we can expect calls on project
allow(Gitlab::GlRepository).to receive(:parse).and_return([empty_project, Gitlab::GlRepository::PROJECT])
end
@@ -60,33 +61,55 @@ describe PostReceive do
end
end
+ shared_examples 'not updating remote mirrors' do
+ it 'does not schedule an update' do
+ expect(project).not_to receive(:has_remote_mirror?)
+ expect(project).not_to receive(:mark_stuck_remote_mirrors_as_failed!)
+ expect(project).not_to receive(:update_remote_mirrors)
+
+ perform
+ end
+ end
+
context 'empty changes' do
it "does not call any PushService but runs after project hooks" do
- expect(Git::BranchPushService).not_to receive(:new)
- expect(Git::TagPushService).not_to receive(:new)
+ expect(Git::ProcessRefChangesService).not_to receive(:new)
expect_next_instance_of(SystemHooksService) { |service| expect(service).to receive(:execute_hooks) }
perform(changes: "")
end
+
+ it_behaves_like 'not updating remote mirrors'
end
context 'unidentified user' do
let!(:key_id) { "" }
it 'returns false' do
- expect(Git::BranchPushService).not_to receive(:new)
- expect(Git::TagPushService).not_to receive(:new)
+ expect(Git::ProcessRefChangesService).not_to receive(:new)
expect(perform).to be false
end
end
context 'with changes' do
+ let(:push_service) { double(execute: true) }
+
before do
allow_any_instance_of(Gitlab::GitPostReceive).to receive(:identify).and_return(project.owner)
allow(Gitlab::GlRepository).to receive(:parse).and_return([project, Gitlab::GlRepository::PROJECT])
end
+ shared_examples 'updating remote mirrors' do
+ it 'schedules an update if the project had mirrors' do
+ expect(project).to receive(:has_remote_mirror?).and_return(true)
+ expect(project).to receive(:mark_stuck_remote_mirrors_as_failed!)
+ expect(project).to receive(:update_remote_mirrors)
+
+ perform
+ end
+ end
+
context "branches" do
let(:changes) do
<<~EOF
@@ -102,19 +125,17 @@ describe PostReceive do
end
it 'expires the status cache' do
- expect(project).to receive(:empty_repo?).and_return(true)
+ expect(project.repository).to receive(:empty?).and_return(true)
expect(project.repository).to receive(:expire_status_cache)
perform
end
- it 'calls Git::BranchPushService' do
- expect_any_instance_of(Git::BranchPushService) do |service|
+ it 'calls Git::ProcessRefChangesService' do
+ expect_next_instance_of(Git::ProcessRefChangesService) do |service|
expect(service).to receive(:execute).and_return(true)
end
- expect(Git::TagPushService).not_to receive(:new)
-
perform
end
@@ -125,6 +146,8 @@ describe PostReceive do
perform
end
+ it_behaves_like 'updating remote mirrors'
+
context 'with a default branch' do
let(:changes) do
<<~EOF
@@ -149,8 +172,6 @@ describe PostReceive do
654321 210987 refs/tags/tag1
654322 210986 refs/tags/tag2
654323 210985 refs/tags/tag3
- 654324 210984 refs/tags/tag4
- 654325 210983 refs/tags/tag5
EOF
end
@@ -164,23 +185,19 @@ describe PostReceive do
perform
end
- it "only invalidates tags once" do
- expect(project.repository).to receive(:repository_event).exactly(5).times.with(:push_tag).and_call_original
+ it 'only invalidates tags once' do
+ expect(project.repository).to receive(:repository_event).exactly(3).times.with(:push_tag).and_call_original
expect(project.repository).to receive(:expire_caches_for_tags).once.and_call_original
expect(project.repository).to receive(:expire_tags_cache).once.and_call_original
perform
end
- it "calls Git::TagPushService" do
- expect(Git::BranchPushService).not_to receive(:new)
-
- expect_any_instance_of(Git::TagPushService) do |service|
+ it 'calls Git::ProcessRefChangesService' do
+ expect_next_instance_of(Git::ProcessRefChangesService) do |service|
expect(service).to receive(:execute).and_return(true)
end
- expect(Git::BranchPushService).not_to receive(:new)
-
perform
end
@@ -190,83 +207,20 @@ describe PostReceive do
perform
end
+
+ it_behaves_like 'updating remote mirrors'
end
context "merge-requests" do
let(:changes) { "123456 789012 refs/merge-requests/123" }
it "does not call any of the services" do
- expect(Git::BranchPushService).not_to receive(:new)
- expect(Git::TagPushService).not_to receive(:new)
+ expect(Git::ProcessRefChangesService).not_to receive(:new)
perform
end
- end
-
- context "gitlab-ci.yml" do
- let(:changes) do
- <<-EOF.strip_heredoc
- 123456 789012 refs/heads/feature
- 654321 210987 refs/tags/tag
- 123456 789012 refs/heads/feature2
- 123458 789013 refs/heads/feature3
- 123459 789015 refs/heads/feature4
- EOF
- end
-
- let(:changes_count) { changes.lines.count }
-
- subject { perform }
-
- context "with valid .gitlab-ci.yml" do
- before do
- stub_ci_pipeline_to_return_yaml_file
-
- allow_any_instance_of(Project)
- .to receive(:commit)
- .and_return(project.commit)
-
- allow_any_instance_of(Repository)
- .to receive(:branch_exists?)
- .and_return(true)
- end
-
- context 'when git_push_create_all_pipelines is disabled' do
- before do
- stub_feature_flags(git_push_create_all_pipelines: false)
- end
-
- it "creates pipeline for branches and tags" do
- subject
-
- expect(Ci::Pipeline.pluck(:ref)).to contain_exactly("feature", "tag", "feature2", "feature3")
- end
-
- it "creates exactly #{described_class::PIPELINE_PROCESS_LIMIT} pipelines" do
- expect(changes_count).to be > described_class::PIPELINE_PROCESS_LIMIT
-
- expect { subject }.to change { Ci::Pipeline.count }.by(described_class::PIPELINE_PROCESS_LIMIT)
- end
- end
-
- context 'when git_push_create_all_pipelines is enabled' do
- before do
- stub_feature_flags(git_push_create_all_pipelines: true)
- end
-
- it "creates all pipelines" do
- expect { subject }.to change { Ci::Pipeline.count }.by(changes_count)
- end
- end
- end
- context "does not create a Ci::Pipeline" do
- before do
- stub_ci_pipeline_yaml_file(nil)
- end
-
- it { expect { subject }.not_to change { Ci::Pipeline.count } }
- end
+ it_behaves_like 'not updating remote mirrors'
end
context 'after project changes hooks' do
@@ -277,7 +231,7 @@ describe PostReceive do
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)
- expect_next_instance_of(Git::BranchPushService) do |service|
+ expect_next_instance_of(Git::ProcessRefChangesService) do |service|
expect(service).to receive(:execute).and_return(true)
end
end
@@ -300,6 +254,11 @@ describe PostReceive do
describe '#process_wiki_changes' do
let(:gl_repository) { "wiki-#{project.id}" }
+ before do
+ # Need to mock here so we can expect calls on project
+ allow(Gitlab::GlRepository).to receive(:parse).and_return([project, Gitlab::GlRepository::WIKI])
+ end
+
it 'updates project activity' do
# Force Project#set_timestamps_for_create to initialize timestamps
project
@@ -314,6 +273,28 @@ describe PostReceive do
.and change(project, :last_repository_updated_at)
end
end
+
+ context "branches" do
+ let(:changes) do
+ <<~EOF
+ 123456 789012 refs/heads/tést1
+ 123456 789012 refs/heads/tést2
+ EOF
+ end
+
+ it 'expires the branches cache' do
+ expect(project.wiki.repository).to receive(:expire_branches_cache).once
+
+ perform
+ end
+
+ it 'expires the status cache' do
+ expect(project.wiki.repository).to receive(:empty?).and_return(true)
+ expect(project.wiki.repository).to receive(:expire_status_cache)
+
+ perform
+ end
+ end
end
context "webhook" do
diff --git a/spec/workers/prune_old_events_worker_spec.rb b/spec/workers/prune_old_events_worker_spec.rb
index f1eef1923bf..14235bde070 100644
--- a/spec/workers/prune_old_events_worker_spec.rb
+++ b/spec/workers/prune_old_events_worker_spec.rb
@@ -6,12 +6,12 @@ describe PruneOldEventsWorker do
describe '#perform' do
let(:user) { create(:user) }
- let!(:expired_event) { create(:event, :closed, author: user, created_at: 25.months.ago) }
+ let!(:expired_event) { create(:event, :closed, author: user, created_at: 37.months.ago) }
let!(:not_expired_1_day_event) { create(:event, :closed, author: user, created_at: 1.day.ago) }
let!(:not_expired_13_month_event) { create(:event, :closed, author: user, created_at: 13.months.ago) }
- let!(:not_expired_2_years_event) { create(:event, :closed, author: user, created_at: 2.years.ago) }
+ let!(:not_expired_3_years_event) { create(:event, :closed, author: user, created_at: 3.years.ago) }
- it 'prunes events older than 2 years' do
+ it 'prunes events older than 3 years' do
expect { subject.perform }.to change { Event.count }.by(-1)
expect(Event.find_by(id: expired_event.id)).to be_nil
end
@@ -26,9 +26,9 @@ describe PruneOldEventsWorker do
expect(not_expired_13_month_event.reload).to be_present
end
- it 'leaves events from 2 years ago' do
+ it 'leaves events from 3 years ago' do
subject.perform
- expect(not_expired_2_years_event).to be_present
+ expect(not_expired_3_years_event).to be_present
end
end
end
diff --git a/spec/workers/repository_check/dispatch_worker_spec.rb b/spec/workers/repository_check/dispatch_worker_spec.rb
index 03efb6a0a80..e4bb0bf4046 100644
--- a/spec/workers/repository_check/dispatch_worker_spec.rb
+++ b/spec/workers/repository_check/dispatch_worker_spec.rb
@@ -30,8 +30,8 @@ describe RepositoryCheck::DispatchWorker do
context 'with unhealthy shard' do
let(:default_shard_name) { 'default' }
let(:unhealthy_shard_name) { 'unhealthy' }
- let(:default_shard) { Gitlab::HealthChecks::Result.new(true, nil, shard: default_shard_name) }
- let(:unhealthy_shard) { Gitlab::HealthChecks::Result.new(false, '14:Connect Failed', shard: unhealthy_shard_name) }
+ let(:default_shard) { Gitlab::HealthChecks::Result.new('gitaly_check', true, nil, shard: default_shard_name) }
+ let(:unhealthy_shard) { Gitlab::HealthChecks::Result.new('gitaly_check', false, '14:Connect Failed', shard: unhealthy_shard_name) }
before do
allow(Gitlab::HealthChecks::GitalyCheck).to receive(:readiness).and_return([default_shard, unhealthy_shard])
diff --git a/spec/workers/update_project_statistics_worker_spec.rb b/spec/workers/update_project_statistics_worker_spec.rb
index a268fd2e4ba..191075f1a52 100644
--- a/spec/workers/update_project_statistics_worker_spec.rb
+++ b/spec/workers/update_project_statistics_worker_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe UpdateProjectStatisticsWorker do